From 9fda1943914f79671c3d2a7ecbc35cc3da66160c Mon Sep 17 00:00:00 2001 From: Apprentice Alf Date: Wed, 19 Dec 2012 13:48:11 +0000 Subject: [PATCH] tools v5.5 Plugins now include unaltered stand-alone scripts, so no longer need to keep separate copies. --- Calibre_Plugins/Ignobleepub ReadMe.txt | 19 +- Calibre_Plugins/Ineptepub ReadMe.txt | 87 +- Calibre_Plugins/Ineptpdf ReadMe.txt | 84 +- Calibre_Plugins/K4MobiDeDRM ReadMe.txt | 95 +- .../K4MobiDeDRM_plugin/__init__.py | 208 +- .../K4MobiDeDRM_plugin/alfcrypto.py | 23 +- Calibre_Plugins/K4MobiDeDRM_plugin/config.py | 3 + .../K4MobiDeDRM_plugin/convert2xml.py | 3 + .../K4MobiDeDRM_plugin/k4mobidedrm.py | 302 ++ .../K4MobiDeDRM_plugin/kgenpids.py | 90 +- .../K4MobiDeDRM_plugin/kindlepid.py | 142 + .../K4MobiDeDRM_plugin/outputfix.py | 45 - Calibre_Plugins/K4MobiDeDRM_plugin/pbkdf2.py | 68 - .../K4MobiDeDRM_plugin/topazextract.py | 322 +- Calibre_Plugins/eReaderPDB2PML ReadMe.txt | 19 +- Calibre_Plugins/eReaderPDB2PML_plugin.zip | Bin 15610 -> 15437 bytes .../eReaderPDB2PML_plugin/__init__.py | 137 +- .../eReaderPDB2PML_plugin/erdr2pml.py | 287 +- .../eReaderPDB2PML_plugin/openssl_des.py | 7 +- .../eReaderPDB2PML_plugin/outputfix.py | 45 - .../eReaderPDB2PML_plugin/pycrypto_des.py | 1 - .../eReaderPDB2PML_plugin/python_des.py | 24 +- Calibre_Plugins/ignobleepub_plugin.zip | Bin 32027 -> 40545 bytes .../ignobleepub_plugin/__init__.py | 296 +- Calibre_Plugins/ignobleepub_plugin/config.py | 43 +- Calibre_Plugins/ignobleepub_plugin/dialogs.py | 18 +- .../ignobleepub_plugin/ignobleepub.py | 420 +++ .../ignobleepub_plugin/ignoblekeygen.py | 157 +- .../ignobleepub_plugin/outputfix.py | 45 - ...txt => plugin-import-name-ignobleepub.txt} | 0 .../ignobleepub_plugin/utilities.py | 113 +- Calibre_Plugins/ignobleepub_plugin/zipfix.py | 1 + Calibre_Plugins/ineptepub_plugin.zip | Bin 29422 -> 32315 bytes Calibre_Plugins/ineptepub_plugin/__init__.py | 490 +-- .../ineptepub_plugin/ineptepub.py | 379 +- Calibre_Plugins/ineptepub_plugin/ineptkey.py | 93 +- Calibre_Plugins/ineptepub_plugin/outputfix.py | 45 - Calibre_Plugins/ineptepub_plugin/zipfix.py | 1 + Calibre_Plugins/ineptpdf_plugin.zip | Bin 26199 -> 29664 bytes Calibre_Plugins/ineptpdf_plugin/__init__.py | 2202 +----------- Calibre_Plugins/ineptpdf_plugin/ineptkey.py | 93 +- .../ineptpdf_plugin/ineptpdf.py | 337 +- Calibre_Plugins/ineptpdf_plugin/outputfix.py | 45 - Calibre_Plugins/k4mobidedrm_plugin.zip | Bin 218296 -> 230884 bytes .../k4mobidedrm_plugin/k4mutils.py | 85 +- .../k4mobidedrm_plugin/k4pcutils.py | 2 + .../k4mobidedrm_plugin/mobidedrm.py | 271 +- DeDRM_Macintosh_Application/DeDRM ReadMe.rtf | 2 +- DeDRM_Macintosh_Application/DeDRM.app.txt | 118 +- .../DeDRM.app/Contents/Info.plist | 8 +- .../Contents/Resources/Scripts/main.scpt | Bin 261580 -> 268574 bytes .../DeDRM.app/Contents/Resources/alfcrypto.py | 23 +- .../DeDRM.app/Contents/Resources/config.py | 3 + .../Contents/Resources/convert2xml.py | 3 + .../DeDRM.app/Contents/Resources/epubtest.py | 169 + .../DeDRM.app/Contents/Resources/erdr2pml.py | 266 +- .../Contents/Resources/ignobleepub.py | 407 ++- .../Contents/Resources/ignoblekeygen.py | 157 +- .../DeDRM.app/Contents/Resources/ineptepub.py | 379 +- .../DeDRM.app/Contents/Resources/ineptkey.py | 93 +- .../DeDRM.app/Contents/Resources/ineptpdf.py | 337 +- .../Contents/Resources/k4mobidedrm.py | 332 +- .../DeDRM.app/Contents/Resources/k4mutils.py | 85 +- .../DeDRM.app/Contents/Resources/k4pcutils.py | 2 + .../DeDRM.app/Contents/Resources/kgenpids.py | 90 +- .../DeDRM.app/Contents/Resources/kindlepid.py | 97 +- .../DeDRM.app/Contents/Resources/mobidedrm.py | 271 +- .../Contents/Resources/topazextract.py | 322 +- .../DeDRM.app/Contents/Resources/zipfix.py | 5 +- .../DeDRM_App/DeDRM_lib/DeDRM_app.pyw | 38 +- .../DeDRM_App/DeDRM_lib/lib/alfcrypto.py | 23 +- .../DeDRM_App/DeDRM_lib/lib/config.py | 3 + .../DeDRM_App/DeDRM_lib/lib/convert2xml.py | 3 + .../DeDRM_App/DeDRM_lib/lib/decryptpdb.py | 2 +- .../DeDRM_App/DeDRM_lib/lib/encodebase64.py | 45 + .../DeDRM_App/DeDRM_lib/lib/epubtest.py | 169 + .../DeDRM_App/DeDRM_lib/lib/erdr2pml.py | 266 +- .../DeDRM_App/DeDRM_lib/lib/ignobleepub.py | 407 ++- .../DeDRM_App/DeDRM_lib/lib/ignoblekeygen.py | 157 +- .../DeDRM_App/DeDRM_lib/lib/ineptepub.py | 379 +- .../DeDRM_App/DeDRM_lib/lib/ineptkey.py | 93 +- .../DeDRM_App/DeDRM_lib/lib/ineptpdf.py | 337 +- .../DeDRM_App/DeDRM_lib/lib/k4mobidedrm.py | 332 +- .../DeDRM_App/DeDRM_lib/lib/k4mutils.py | 85 +- .../DeDRM_App/DeDRM_lib/lib/k4pcutils.py | 2 + .../DeDRM_App/DeDRM_lib/lib/kgenpids.py | 90 +- .../DeDRM_App/DeDRM_lib/lib/kindlepid.py | 142 + .../DeDRM_App/DeDRM_lib/lib/mobidedrm.py | 271 +- .../DeDRM_App/DeDRM_lib/lib/topazextract.py | 322 +- .../DeDRM_App/DeDRM_lib/lib/zipfix.py | 5 +- DeDRM_Windows_Application/DeDRM_ReadMe.txt | 14 +- .../Additional_Tools/FindTopazEbooks.pyw | 217 -- Other_Tools/Additional_Tools/KindlePID.pyw | 146 - Other_Tools/Additional_Tools/Kindleizer.pyw | 170 - Other_Tools/Additional_Tools/MobiDeDRM.pyw | 203 -- Other_Tools/Additional_Tools/lib/kindlefix.py | 172 - Other_Tools/Additional_Tools/lib/kindlepid.py | 91 - Other_Tools/Additional_Tools/lib/mobidedrm.py | 460 --- Other_Tools/Additional_Tools/lib/mobihuff.py | 189 - Other_Tools/Additional_Tools/lib/prc.py | 529 --- .../Additional_Tools/lib/scrolltextwidget.py | 27 - .../Additional_Tools/lib/subasyncio.py | 149 - .../Adobe_PDF_Tools/README_ineptpdf.txt | 18 - Other_Tools/Adobe_PDF_Tools/ineptkey.pyw | 468 --- Other_Tools/Adobe_PDF_Tools/ineptpdf8.pyw | 3160 ----------------- .../Adobe_ePub_Tools/README_ineptepub.txt | 18 - Other_Tools/Adobe_ePub_Tools/ineptkey.pyw | 457 --- .../BN-Dload.user_ReadMe.txt | 22 +- .../README_ignoble_epub.txt | 24 - .../ignobleepub.pyw | 336 -- .../ignoblekey.pyw | 112 - Other_Tools/KindleBooks/KindleBooks.pyw | 261 -- .../KindleBooks/README_KindleBooks.txt | 58 - Other_Tools/KindleBooks/lib/aescbc.py | 568 --- Other_Tools/KindleBooks/lib/alfcrypto.dll | Bin 70144 -> 0 bytes Other_Tools/KindleBooks/lib/alfcrypto.py | 290 -- Other_Tools/KindleBooks/lib/alfcrypto64.dll | Bin 52224 -> 0 bytes Other_Tools/KindleBooks/lib/alfcrypto_src.zip | Bin 17393 -> 0 bytes Other_Tools/KindleBooks/lib/cmbtc_v2.2.py | 900 ----- Other_Tools/KindleBooks/lib/config.py | 59 - Other_Tools/KindleBooks/lib/convert2xml.py | 846 ----- Other_Tools/KindleBooks/lib/flatxml2html.py | 793 ----- Other_Tools/KindleBooks/lib/flatxml2svg.py | 249 -- Other_Tools/KindleBooks/lib/genbook.py | 721 ---- Other_Tools/KindleBooks/lib/genxml.py | 145 - Other_Tools/KindleBooks/lib/getk4pcpids.py | 78 - Other_Tools/KindleBooks/lib/k4mdumpkinfo.py | 333 -- Other_Tools/KindleBooks/lib/k4mobidedrm.py | 238 -- Other_Tools/KindleBooks/lib/k4mutils.py | 730 ---- Other_Tools/KindleBooks/lib/k4pcutils.py | 455 --- Other_Tools/KindleBooks/lib/kgenpids.py | 274 -- .../KindleBooks/lib/libalfcrypto.dylib | Bin 87160 -> 0 bytes Other_Tools/KindleBooks/lib/libalfcrypto32.so | Bin 23859 -> 0 bytes Other_Tools/KindleBooks/lib/libalfcrypto64.so | Bin 33417 -> 0 bytes Other_Tools/KindleBooks/lib/mobidedrm.py | 460 --- .../KindleBooks/lib/scrolltextwidget.py | 27 - Other_Tools/KindleBooks/lib/stylexml2css.py | 266 -- Other_Tools/KindleBooks/lib/subasyncio.py | 148 - Other_Tools/KindleBooks/lib/topazextract.py | 482 --- ...EADME_Kindle_for_iPad_iPhone_iPodTouch.txt | 37 - .../Scuolabook+DRM+Remover+1.0.zip | Bin ...uolabook_DRM_Remover_source_19_11_2012.zip | Bin .../Scuolabook_DRM}/Scuolabook_ReadMe.txt | 0 Other_Tools/ePub_Fixer/README_ePub_Fixer.txt | 17 - Other_Tools/ePub_Fixer/ePub_Fixer.pyw | 196 - .../ePub_Fixer/lib/scrolltextwidget.py | 27 - Other_Tools/ePub_Fixer/lib/subasyncio.py | 149 - Other_Tools/ePub_Fixer/lib/zipfilerugged.py | 1400 -------- Other_Tools/ePub_Fixer/lib/zipfix.py | 158 - Other_Tools/eReader_PDB_Tools/Pml2HTML.pyw | 192 - .../eReader_PDB_Tools/README_eReaderPDB.txt | 21 - .../eReader_PDB_Tools/eReaderPDB2PML.pyw | 215 -- .../eReader_PDB_Tools/eReaderPDB2PMLZ.pyw | 185 - .../lib/eReaderPDB2PML_plugin.py | 137 - Other_Tools/eReader_PDB_Tools/lib/erdr2pml.py | 526 --- .../eReader_PDB_Tools/lib/openssl_des.py | 89 - .../eReader_PDB_Tools/lib/pycrypto_des.py | 30 - .../eReader_PDB_Tools/lib/python_des.py | 220 -- .../eReader_PDB_Tools/lib/scrolltextwidget.py | 27 - .../eReader_PDB_Tools/lib/subasyncio.py | 149 - .../eReader_PDB_Tools/lib/xpml2xhtml.py | 858 ----- ReadMe_First.txt | 228 +- 162 files changed, 7115 insertions(+), 26596 deletions(-) create mode 100644 Calibre_Plugins/K4MobiDeDRM_plugin/k4mobidedrm.py create mode 100644 Calibre_Plugins/K4MobiDeDRM_plugin/kindlepid.py delete mode 100644 Calibre_Plugins/K4MobiDeDRM_plugin/outputfix.py delete mode 100644 Calibre_Plugins/K4MobiDeDRM_plugin/pbkdf2.py delete mode 100644 Calibre_Plugins/eReaderPDB2PML_plugin/outputfix.py create mode 100644 Calibre_Plugins/ignobleepub_plugin/ignobleepub.py rename Other_Tools/Barnes_and_Noble_EPUB_Tools/ignoblekeygen.pyw => Calibre_Plugins/ignobleepub_plugin/ignoblekeygen.py (56%) delete mode 100644 Calibre_Plugins/ignobleepub_plugin/outputfix.py rename Calibre_Plugins/ignobleepub_plugin/{plugin-import-name-ignoble_epub.txt => plugin-import-name-ignobleepub.txt} (100%) rename Other_Tools/Adobe_ePub_Tools/ineptepub.pyw => Calibre_Plugins/ineptepub_plugin/ineptepub.py (53%) delete mode 100644 Calibre_Plugins/ineptepub_plugin/outputfix.py rename Other_Tools/Adobe_PDF_Tools/ineptpdf.pyw => Calibre_Plugins/ineptpdf_plugin/ineptpdf.py (88%) delete mode 100644 Calibre_Plugins/ineptpdf_plugin/outputfix.py create mode 100644 DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/epubtest.py create mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/encodebase64.py create mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/epubtest.py create mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kindlepid.py delete mode 100644 Other_Tools/Additional_Tools/FindTopazEbooks.pyw delete mode 100644 Other_Tools/Additional_Tools/KindlePID.pyw delete mode 100644 Other_Tools/Additional_Tools/Kindleizer.pyw delete mode 100644 Other_Tools/Additional_Tools/MobiDeDRM.pyw delete mode 100644 Other_Tools/Additional_Tools/lib/kindlefix.py delete mode 100644 Other_Tools/Additional_Tools/lib/kindlepid.py delete mode 100644 Other_Tools/Additional_Tools/lib/mobidedrm.py delete mode 100644 Other_Tools/Additional_Tools/lib/mobihuff.py delete mode 100644 Other_Tools/Additional_Tools/lib/prc.py delete mode 100644 Other_Tools/Additional_Tools/lib/scrolltextwidget.py delete mode 100644 Other_Tools/Additional_Tools/lib/subasyncio.py delete mode 100644 Other_Tools/Adobe_PDF_Tools/README_ineptpdf.txt delete mode 100644 Other_Tools/Adobe_PDF_Tools/ineptkey.pyw delete mode 100644 Other_Tools/Adobe_PDF_Tools/ineptpdf8.pyw delete mode 100644 Other_Tools/Adobe_ePub_Tools/README_ineptepub.txt delete mode 100644 Other_Tools/Adobe_ePub_Tools/ineptkey.pyw delete mode 100644 Other_Tools/Barnes_and_Noble_EPUB_Tools/README_ignoble_epub.txt delete mode 100644 Other_Tools/Barnes_and_Noble_EPUB_Tools/ignobleepub.pyw delete mode 100644 Other_Tools/Barnes_and_Noble_EPUB_Tools/ignoblekey.pyw delete mode 100644 Other_Tools/KindleBooks/KindleBooks.pyw delete mode 100644 Other_Tools/KindleBooks/README_KindleBooks.txt delete mode 100644 Other_Tools/KindleBooks/lib/aescbc.py delete mode 100644 Other_Tools/KindleBooks/lib/alfcrypto.dll delete mode 100644 Other_Tools/KindleBooks/lib/alfcrypto.py delete mode 100644 Other_Tools/KindleBooks/lib/alfcrypto64.dll delete mode 100644 Other_Tools/KindleBooks/lib/alfcrypto_src.zip delete mode 100644 Other_Tools/KindleBooks/lib/cmbtc_v2.2.py delete mode 100644 Other_Tools/KindleBooks/lib/config.py delete mode 100644 Other_Tools/KindleBooks/lib/convert2xml.py delete mode 100644 Other_Tools/KindleBooks/lib/flatxml2html.py delete mode 100644 Other_Tools/KindleBooks/lib/flatxml2svg.py delete mode 100644 Other_Tools/KindleBooks/lib/genbook.py delete mode 100644 Other_Tools/KindleBooks/lib/genxml.py delete mode 100644 Other_Tools/KindleBooks/lib/getk4pcpids.py delete mode 100644 Other_Tools/KindleBooks/lib/k4mdumpkinfo.py delete mode 100644 Other_Tools/KindleBooks/lib/k4mobidedrm.py delete mode 100644 Other_Tools/KindleBooks/lib/k4mutils.py delete mode 100644 Other_Tools/KindleBooks/lib/k4pcutils.py delete mode 100644 Other_Tools/KindleBooks/lib/kgenpids.py delete mode 100644 Other_Tools/KindleBooks/lib/libalfcrypto.dylib delete mode 100644 Other_Tools/KindleBooks/lib/libalfcrypto32.so delete mode 100644 Other_Tools/KindleBooks/lib/libalfcrypto64.so delete mode 100644 Other_Tools/KindleBooks/lib/mobidedrm.py delete mode 100644 Other_Tools/KindleBooks/lib/scrolltextwidget.py delete mode 100644 Other_Tools/KindleBooks/lib/stylexml2css.py delete mode 100644 Other_Tools/KindleBooks/lib/subasyncio.py delete mode 100644 Other_Tools/KindleBooks/lib/topazextract.py delete mode 100644 Other_Tools/README_Kindle_for_iPad_iPhone_iPodTouch.txt rename {Scuolabook_DRM => Other_Tools/Scuolabook_DRM}/Scuolabook+DRM+Remover+1.0.zip (100%) rename {Scuolabook_DRM => Other_Tools/Scuolabook_DRM}/Scuolabook_DRM_Remover_source_19_11_2012.zip (100%) rename {Scuolabook_DRM => Other_Tools/Scuolabook_DRM}/Scuolabook_ReadMe.txt (100%) delete mode 100644 Other_Tools/ePub_Fixer/README_ePub_Fixer.txt delete mode 100644 Other_Tools/ePub_Fixer/ePub_Fixer.pyw delete mode 100644 Other_Tools/ePub_Fixer/lib/scrolltextwidget.py delete mode 100644 Other_Tools/ePub_Fixer/lib/subasyncio.py delete mode 100644 Other_Tools/ePub_Fixer/lib/zipfilerugged.py delete mode 100644 Other_Tools/ePub_Fixer/lib/zipfix.py delete mode 100644 Other_Tools/eReader_PDB_Tools/Pml2HTML.pyw delete mode 100644 Other_Tools/eReader_PDB_Tools/README_eReaderPDB.txt delete mode 100644 Other_Tools/eReader_PDB_Tools/eReaderPDB2PML.pyw delete mode 100644 Other_Tools/eReader_PDB_Tools/eReaderPDB2PMLZ.pyw delete mode 100644 Other_Tools/eReader_PDB_Tools/lib/eReaderPDB2PML_plugin.py delete mode 100644 Other_Tools/eReader_PDB_Tools/lib/erdr2pml.py delete mode 100644 Other_Tools/eReader_PDB_Tools/lib/openssl_des.py delete mode 100644 Other_Tools/eReader_PDB_Tools/lib/pycrypto_des.py delete mode 100644 Other_Tools/eReader_PDB_Tools/lib/python_des.py delete mode 100644 Other_Tools/eReader_PDB_Tools/lib/scrolltextwidget.py delete mode 100644 Other_Tools/eReader_PDB_Tools/lib/subasyncio.py delete mode 100644 Other_Tools/eReader_PDB_Tools/lib/xpml2xhtml.py diff --git a/Calibre_Plugins/Ignobleepub ReadMe.txt b/Calibre_Plugins/Ignobleepub ReadMe.txt index 3eb916b..dd6a41d 100644 --- a/Calibre_Plugins/Ignobleepub ReadMe.txt +++ b/Calibre_Plugins/Ignobleepub ReadMe.txt @@ -1,17 +1,19 @@ -Ignoble Epub DeDRM - ignobleepub_v02.4_plugin.zip +Ignoble Epub DeDRM - ignobleepub_v02.5_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. +All credit given to i♥cabbages for the original standalone scripts. I had the much easier job of converting them to a calibre plugin. 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. -Installation: +Installation +------------ -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.4_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.5_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog. -Configuration: +Customization +------------- 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. @@ -46,7 +48,8 @@ At the bottom-left of the plugin's customization dialog, you will see a button l 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). -Troubleshooting: +Troubleshooting +--------------- If you find that it's not working for you (imported ebooks still have DRM), you can save a lot of time and trouble by first deleting the DRMed ebook from calibre and then trying to add the ebook 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. ;) @@ -64,4 +67,4 @@ Now copy the output from the terminal window. On Windows, you must use the window menu (little icon at left of window bar) to select all the text and then to copy it. On Macintosh and Linux, just use the normal text select and copy commands. -Paste the information into a comment at my blog, describing your problem. +Paste the information into a comment at my blog, http://apprenticealf.wordpress.com/ describing your problem. diff --git a/Calibre_Plugins/Ineptepub ReadMe.txt b/Calibre_Plugins/Ineptepub ReadMe.txt index 9dfdf57..0620c5f 100644 --- a/Calibre_Plugins/Ineptepub ReadMe.txt +++ b/Calibre_Plugins/Ineptepub ReadMe.txt @@ -1,25 +1,26 @@ -Inept Epub DeDRM - ineptepub_v01.9_plugin.zip +Inept Epub DeDRM - ineptepub_v02.0_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. +All credit given to i♥cabbages for the original standalone scripts. I had the much easier job of converting them to a Calibre plugin. This plugin is meant to decrypt Adobe Digital Edition 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. -Installation: +Installation +------------ -Go to Calibre's Preferences page. Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Cahnge calibre behavior". Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (ineptepub_v01.9_plugin.zip) and click the 'Add' button. you're done. - -Please note: Calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added. +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. -Configuration: + +Customization +------------- When first run, the plugin will attempt to find your Adobe Digital Editions installation (on Windows and Mac OS). If successful, it will create 'calibre-adeptkey[number].der' file(s) and save them in Calibre's configuration directory. It will use those files and any other '*.der' files in any decryption attempts. If there is already at least one 'calibre-adept*.der' file in the directory, the plugin won't attempt to find the Adobe Digital Editions installation keys again. So if you have Adobe Digital Editions installation installed on the same machine as Calibre... you are ready to go. If not... keep reading. -If you already have keyfiles generated with I♥Cabbages' ineptkey.pyw script, you can put those keyfiles in Calibre's configuration directory. The easiest way to find the correct directory is to go to Calibre's Preferences page... click on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre configuration directory' button. Paste your keyfiles in there. Just make sure that they have different names and are saved with the '.der' extension (like the ineptkey script produces). This directory isn't touched when upgrading Calibre, so it's quite safe to leave them there. +If you already have keyfiles generated with i♥cabbages' ineptkey.pyw script, you can put those keyfiles in Calibre's configuration directory. The easiest way to find the correct directory is to go to Calibre's Preferences page... click on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre configuration directory' button. Paste your keyfiles in there. Just make sure that they have different names and are saved with the '.der' extension (like the ineptkey script produces). This directory isn't touched when upgrading Calibre, so it's quite safe to leave them there. Since there is no Linux version of Adobe Digital Editions, Linux users will have to obtain a keyfile through other methods and put the file in Calibre's configuration directory. @@ -29,7 +30,8 @@ All keyfiles with a '.der' extension found in Calibre's configuration directory ** NOTE ** There is no plugin customization data for the Inept Epub DeDRM plugin. -Troubleshooting: +Troubleshooting +--------------- If you find that it's not working for you (imported ebooks still have DRM), you can save a lot of time and trouble by first deleting the DRMed ebook from calibre and then trying to add the ebook 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. ;) @@ -47,4 +49,67 @@ Now copy the output from the terminal window. On Windows, you must use the window menu (little icon at left of window bar) to select all the text and then to copy it. On Macintosh and Linux, just use the normal text select and copy commands. -Paste the information into a comment at my blog, describing your problem. \ No newline at end of file +Paste the information into a comment at my blog, http://apprenticealf.wordpress.com/ describing your problem. + + +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!) + + +1. download the most recent version of wine from winehq.org (1.3.29 in my case) + +For debian users: + +to get a recent version of wine I decited to use aptosid (2011-02, xfce) +(because I’m used to debian) +install aptosid and upgrade it (see aptosid site for detaild instructions) + + +2. properly install Wine (see the Wine site for details) + +For debian users: + +cd to this dir and install the packages as root: +‘dpkg -i *.deb’ +you will get some error messages, which can be ignored. +again as root use +‘apt-get -f install’ to correct this errors + +3. python 2.7 should already be installed on your system but you may need the following additional python package + +'apt-get install python-tk’ + +4. all programms need to be installed as normal user. All these programm are installed the same way: +‘wine ‘ +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) + +b) ActivePython-2.7.2.5-win32-x86.msi (from: http://www.activestate.com/activepython/downloads) + +c) Win32OpenSSL_Light-0_9_8r.exe (from: http://www.slproweb.com/) + +d) pycrypto-2.3.win32-py2.7.msi (from: http://www.voidspace.org.uk/python/modules.shtml) + +5. now get and unpack the very latest tools_vX.X (from Apprentice Alf) in the users drive_c of wine +(~/.wine/drive_c/) + +6. start ADE with: +‘wine digitaleditions.exe’ or from the start menue wine-adobe-digital.. + +7. register this instance of ADE with your adobeID and close it + change to the tools_vX.X dir: +cd ~/.wine/drive_c/tools_vX.X/Other_Tools/ + +8. create the adeptkey.der with: +‘wine python ineptkey.py’ (only need once!) +(key will be here: ~/.wine/drive_c/tools_vX.X/Other_Tools/adeptkey.der) + +9. Use ADE running under Wine to dowload all of your purchased ePub ebooks + +10. install the ineptepub and ineptpdf plugins from the tools as discribed in the readmes. + +11. copy the adeptkey.der into the config dir of calibre (~/.config/calibre in debian). Your ADE books imported to calibre will automatically be freed from DRM. + diff --git a/Calibre_Plugins/Ineptpdf ReadMe.txt b/Calibre_Plugins/Ineptpdf ReadMe.txt index ab5a510..180068c 100644 --- a/Calibre_Plugins/Ineptpdf ReadMe.txt +++ b/Calibre_Plugins/Ineptpdf ReadMe.txt @@ -1,23 +1,25 @@ -Inept PDF Plugin - ineptpdf_v01.8_plugin.zip +Inept PDF Plugin - ineptpdf_v01.9_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. +All credit given to i♥cabbages for the original standalone scripts. I had the much easier job of converting them to a Calibre plugin. This plugin is meant to decrypt Adobe Digital Edition PDFs 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, PyCrypto and/or OpenSSL already installed, but they aren't necessary. -Installation: +Installation +------------ -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 (ineptpdf_v01.8_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 (ineptpdf_v01.9_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog. -Configuration: +Customization +------------- When first run, the plugin will attempt to find your Adobe Digital Editions installation (on Windows and Mac OS). If successful, it will create 'calibre-adeptkey[number].der' file(s) and save them in Calibre's configuration directory. It will use those files and any other '*.der' files in any decryption attempts. If there is already at least one 'calibre-adept*.der' file in the directory, the plugin won't attempt to find the Adobe Digital Editions installation keys again. So if you have Adobe Digital Editions installation installed on the same machine as Calibre... you are ready to go. If not... keep reading. -If you already have keyfiles generated with I♥Cabbages' ineptkey.pyw script, you can put those keyfiles in Calibre's configuration directory. The easiest way to find the correct directory is to go to Calibre's Preferences page... click on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre configuration directory' button. Paste your keyfiles in there. Just make sure that +If you already have keyfiles generated with i♥cabbages' ineptkey.pyw script, you can put those keyfiles in Calibre's configuration directory. The easiest way to find the correct directory is to go to Calibre's Preferences page... click on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre configuration directory' button. Paste your keyfiles in there. Just make sure that they have different names and are saved with the '.der' extension (like the ineptkey script produces). This directory isn't touched when upgrading Calibre, so it's quite safe to leave them there. Since there is no Linux version of Adobe Digital Editions, Linux users will have to obtain a keyfile through other methods and put the file in Calibre's configuration directory. @@ -27,7 +29,8 @@ All keyfiles with a '.der' extension found in Calibre's configuration directory ** NOTE ** There is no plugin customization data for the Inept PDF plugin. -Troubleshooting: +Troubleshooting +--------------- If you find that it's not working for you (imported ebooks still have DRM), you can save a lot of time and trouble by first deleting the DRMed ebook from calibre and then trying to add the ebook 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. ;) @@ -45,4 +48,67 @@ Now copy the output from the terminal window. On Windows, you must use the window menu (little icon at left of window bar) to select all the text and then to copy it. On Macintosh and Linux, just use the normal text select and copy commands. -Paste the information into a comment at my blog, describing your problem. +Paste the information into a comment at my blog, http://apprenticealf.wordpress.com/ describing your problem. + + +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!) + + +1. download the most recent version of wine from winehq.org (1.3.29 in my case) + +For debian users: + +to get a recent version of wine I decited to use aptosid (2011-02, xfce) +(because I’m used to debian) +install aptosid and upgrade it (see aptosid site for detaild instructions) + + +2. properly install Wine (see the Wine site for details) + +For debian users: + +cd to this dir and install the packages as root: +‘dpkg -i *.deb’ +you will get some error messages, which can be ignored. +again as root use +‘apt-get -f install’ to correct this errors + +3. python 2.7 should already be installed on your system but you may need the following additional python package + +'apt-get install python-tk’ + +4. all programms need to be installed as normal user. All these programm are installed the same way: +‘wine ‘ +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) + +b) ActivePython-2.7.2.5-win32-x86.msi (from: http://www.activestate.com/activepython/downloads) + +c) Win32OpenSSL_Light-0_9_8r.exe (from: http://www.slproweb.com/) + +d) pycrypto-2.3.win32-py2.7.msi (from: http://www.voidspace.org.uk/python/modules.shtml) + +5. now get and unpack the very latest tools_vX.X (from Apprentice Alf) in the users drive_c of wine +(~/.wine/drive_c/) + +6. start ADE with: +‘wine digitaleditions.exe’ or from the start menue wine-adobe-digital.. + +7. register this instance of ADE with your adobeID and close it + change to the tools_vX.X dir: +cd ~/.wine/drive_c/tools_vX.X/Other_Tools/ + +8. create the adeptkey.der with: +‘wine python ineptkey.py’ (only need once!) +(key will be here: ~/.wine/drive_c/tools_vX.X/Other_Tools/adeptkey.der) + +9. Use ADE running under Wine to dowload all of your purchased ePub ebooks + +10. install the ineptepub and ineptpdf plugins from the tools as discribed in the readmes. + +11. copy the adeptkey.der into the config dir of calibre (~/.config/calibre in debian). Your ADE books imported to calibre will automatically be freed from DRM. + diff --git a/Calibre_Plugins/K4MobiDeDRM ReadMe.txt b/Calibre_Plugins/K4MobiDeDRM ReadMe.txt index d080d49..f083b9f 100644 --- a/Calibre_Plugins/K4MobiDeDRM ReadMe.txt +++ b/Calibre_Plugins/K4MobiDeDRM ReadMe.txt @@ -1,38 +1,37 @@ -K4MobiDeDRM_v04.7_plugin.zip +Kindle and Mobipocket Plugin - K4MobiDeDRM_v04.10_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. Plugin for K4PC, K4Mac, eInk Kindles and Mobipocket. -This plugin supersedes MobiDeDRM, K4DeDRM, and K4PCDeDRM and K4X plugins. If you install this plugin, those plugins should be removed. +This plugin supersedes MobiDeDRM, K4DeDRM, and K4PCDeDRM and K4X plugins. If you install this plugin, those plugins should be removed, as should any earlier versions of this plugin. -This plugin is meant to remove the DRM from .prc, .mobi, .azw, .azw1, .azw3, .azw4 and .tpz ebooks. Calibre can then convert them to whatever format you desire. It is meant to function without having to install any dependencies except for Calibre being on your same machine and in the same account as your "Kindle for PC" or "Kindle for Mac" application if you are going to remove the DRM from those types of books. +This plugin is meant to remove the DRM from .prc, .mobi, .azw, .azw1, .azw3, .azw4 and .tpz ebooks. Calibre can then convert them to whatever format you desire. It is meant to function without having to install any dependencies except for Calibre being on your same machine and in the same account as your "Kindle for PC" or "Kindle for Mac" application if you are going to remove the DRM from books from those programs. -Installation: +Installation +------------ -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 (K4MobiDeDRM_v04.7_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.10_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. -Configuration: +Customization +------------- Highlight the plugin (K4MobiDeDRM under the "File type plugins" category) and click the "Customize Plugin" button on Calibre's Preferences->Plugins page. -If you have an eInk Kindle enter the 16 character serial number (these all begin a "B") in the serial numbers field. The easiest way to make sure that you have the serial number right is to copy it from your Amazon account pages (the "Manage Your Devices" page). If you have more than one eInk Kindle, you can enter multiple serial numbers separated by commas. +If you have an eInk Kindle enter the 16 character serial number (these all begin a "B" or a "9") in the serial numbers field. The easiest way to make sure that you have the serial number right is to copy it from your Amazon account pages (the "Manage Your Devices" page). If you have more than one eInk Kindle, you can enter multiple serial numbers separated by commas. If you have Mobipocket books, enter your 8 or 10 digit PID in the Mobipocket PIDs field. If you have more than one PID, separate them with commas. These configuration steps are not needed if you only want to decode "Kindle for PC" or "Kindle for Mac" books. -Linux Systems Only: - -If you install Kindle for PC in Wine, the plugin should be able to decode files from that Kindle for PC installation under Wine. You might need to enter a Wine Prefix if it's not already set in your Environment variables. - - -Troubleshooting: +Troubleshooting +--------------- If you find that it's not working for you (imported ebooks still have DRM), you can save a lot of time and trouble by first deleting the DRMed ebook from calibre and then trying to add the ebook 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. ;) @@ -50,6 +49,74 @@ Now copy the output from the terminal window. On Windows, you must use the window menu (little icon at left of window bar) to select all the text and then to copy it. On Macintosh and Linux, just use the normal text select and copy commands. -Paste the information into a comment at my blog, describing your problem. +Paste the information into a comment at my blog, http://apprenticealf.wordpress.com/ describing your problem. + +Linux Systems Only +----------------- + +If you install Kindle for PC in Wine, the plugin should be able to decode files from that Kindle for PC installation under Wine. You might need to enter a Wine Prefix if it's not already set in your Environment variables. You will need to install Python and PyCrypto under Wine as detailed below. In addition, some people who have successfully used the plugin in this way have commented as follows: + +Here are the instructions for using Kindle for PC on Linux under Wine. (Thank you Eyeless and Pete) + +1. upgrade to very recent versions of Wine; This has been tested with Wine 1.3.15 – 1.3.2X. It may work with earlier versions but no promises. It does not work with wine 1.2.X versions. + +If you have not already installed Kindle for PC under wine, follow steps 2 and 3 otherwise jump to step 4 + +2. Some versions of winecfg have a bug in setting the volume serial number, so create a .windows-serial file at root of drive_c to set a proper windows volume serial number (8 digit hex value for unsigned integer). +cd ~ +cd .wine +cd drive_c +echo deadbeef > .windows-serial + +Replace "deadbeef" with whatever hex value you want but I would stay away from the default setting of "ffffffff" which does not seem to work. BTW: deadbeef is itself a valid possible hex value if you want to use it + +3. Only ***after*** setting the volume serial number properly – download and install under wine K4PC version for Windows. Register it and download from your Archive one of your Kindle ebooks. Versions known to work are K4PC 1.7.1 and earlier. Later version may work but no promises. + + +FIRST user +---------- +Hi everyone, I struggled to get this working on Ubuntu 12.04. Here are the secrets for everyone: + +1. Make sure your Wine installation is set up to be 32 bit. 64 bit is not going to work! To do this, remove your .wine directory (or use a different wineprefix). Then use WINEARCH=win32 winecfg + +2. But wait, you can’t install Kindle yet. It won’t work. You need to do: winetricks -q vcrun2008 or else you’ll get an error: unimplemented function msvcp90.dll . + +3. Now download and install Kindle for PC and download your content as normal. + +4. Now download and install Python 2.7 32 bit for Windows from python.org, 32 bit, install it the usual way, and you can now run the Kindle DRM tools. + +SECOND USER +----------- +It took a while to figure out that I needed wine 32 bit, plus Python 27 32 bit, plus the winetricks, to get all this working together but once it’s done, it’s great and I can read my Kindle content on my Nook Color running Cyanogenmod!!! +Linux Systems Only: +For all of the following wine installs, use WINEARCH=win32 if you are on x86_64. Also remember that in order to execute a *.msi file, you have to run ‘WINEARCH=win32 wine msiexec /i xxxxx.msi’. +1. Install Kindle for PC with wine. +2. Install ActivePython 2.7.x (Windows x86) with wine from here: http://www.activestate.com/activepython/downloads +3. Install the pycrypto (Windows 32 bit for Python 2.7) module with wine from here: http://www.voidspace.org.uk/python/modules.shtml#pycrypto +4. Install the K4MobiDeDRM plugin into your _Linux_ Calibre installation +Now all Kindle books downloaded from Kindle for PC in Wine will be automatically de-DRM’d when they are added to your _Linux_ Calibre. As always, you can troubleshoot problems by adding a book from the terminal using ‘calibredb add xxxx’. + +Or something like that! Hope that helps someone out. + + +Installing Python on Windows +---------------------------- +I strongly recommend fully installing ActiveState’s Active Python, free Community Edition for Windows (x86) 32 bits. This is a free, full version of the Python. It comes with some important additional modules that are not included in the bare-bones version from www.python.org unless you choose to install everything. + +1. Download ActivePython 2.7.X for Windows (x86) (or later 2.7 version for Windows (x86) ) from http://www.activestate.com/activepython/downloads. Do not download the ActivePython 2.7.X for Windows (64-bit, x64) verson, even if you are running 64-bit Windows. + +2. When it has finished downloading, run the installer. Accept the default options. + + +Installing PyCrypto on Windows +------------------------------ +PyCrypto is a set of encryption/decryption routines that work with Python. The sources are freely available, and compiled versions are available from several sources. You must install a version that is for 32-bit Windows and Python 2.7. I recommend the installer linked from Michael Foord’s blog. + +1. Download PyCrypto 2.1 for 32bit Windows and Python 2.7 from http://www.voidspace.org.uk/python/modules.shtml#pycrypto + +2. When it has finished downloading, unzip it. This will produce a file “pycrypto-2.1.0.win32-py2.7.exe”. + +3. Double-click “pycrypto-2.1.0.win32-py2.7.exe” to run it. Accept the default options. + diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/__init__.py b/Calibre_Plugins/K4MobiDeDRM_plugin/__init__.py index 75f6d21..d28db60 100644 --- a/Calibre_Plugins/K4MobiDeDRM_plugin/__init__.py +++ b/Calibre_Plugins/K4MobiDeDRM_plugin/__init__.py @@ -1,30 +1,69 @@ #!/usr/bin/env python -# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +# -*- coding: utf-8 -*- from __future__ import with_statement - -from calibre.customize import FileTypePlugin -from calibre.gui2 import is_ok_to_use_qt -from calibre.utils.config import config_dir -from calibre.constants import iswindows, isosx -# from calibre.ptempfile import PersistentTemporaryDirectory +__license__ = 'GPL v3' +__docformat__ = 'restructuredtext en' -import sys -import os -import re +# Released under the terms of the GNU General Public Licence, version 3 +# +# +# Requires Calibre version 0.7.55 or higher. +# +# All credit given to The Dark Reverser for the original mobidedrm script. +# Thanks to all those who've worked on the scripts since 2008 to improve +# the support for formats and sources. +# +# Revision history: +# 0.4.8 - Major code change to use unaltered k4mobidedrm.py 4.8 and later +# 0.4.9 - typo fix +# 0.4.10 - Another Topaz Fix (class added to page and group and region) + +""" +Decrypt Amazon Kindle and Mobipocket encrypted ebooks. +""" + +PLUGIN_NAME = u"Kindle and Mobipocket DeDRM" +PLUGIN_VERSION_TUPLE = (0, 4, 10) +PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE]) + +import sys, os, re import time from zipfile import ZipFile +from calibre.customize import FileTypePlugin +from calibre.constants import iswindows, isosx +from calibre.gui2 import is_ok_to_use_qt +from calibre.utils.config import config_dir + +# 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) + + class K4DeDRM(FileTypePlugin): - name = 'Kindle and Mobipocket DeDRM' # Name of the plugin - description = 'Removes DRM from eInk Kindle, Kindle 4 Mac and Kindle 4 PC ebooks, and from Mobipocket ebooks. Provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, mdlnx, ApprenticeAlf, etc.' + name = PLUGIN_NAME + description = u"Removes DRM from eInk Kindle, Kindle 4 Mac and Kindle 4 PC ebooks, and from Mobipocket ebooks. Provided by the work of many including The Dark Reverser, DiapDealer, SomeUpdates, i♥cabbages, CMBDTC, Skindle, mdlnx, ApprenticeAlf, and probably others." supported_platforms = ['osx', 'windows', 'linux'] # Platforms this plugin will run on - author = 'DiapDealer, SomeUpdates, mdlnx, Apprentice Alf' # The author of this plugin - version = (0, 4, 7) # The version number of this plugin + author = u"DiapDealer, SomeUpdates, mdlnx, Apprentice Alf and The Dark Reverser" + version = PLUGIN_VERSION_TUPLE file_types = set(['prc','mobi','azw','azw1','azw3','azw4','tpz']) # The file types that this plugin will be applied to on_import = True # Run this plugin during the import - priority = 520 # run this plugin before earlier versions + priority = 521 # run this plugin before earlier versions minimum_calibre_version = (0, 7, 55) def initialize(self): @@ -37,45 +76,39 @@ class K4DeDRM(FileTypePlugin): so the CDLL stuff will work in the alfcrypto.py script. """ if iswindows: - names = ['alfcrypto.dll','alfcrypto64.dll'] + names = [u"alfcrypto.dll",u"alfcrypto64.dll"] elif isosx: - names = ['libalfcrypto.dylib'] + names = [u"libalfcrypto.dylib"] else: - names = ['libalfcrypto32.so','libalfcrypto64.so','alfcrypto.py','alfcrypto.dll','alfcrypto64.dll','getk4pcpids.py','mobidedrm.py','kgenpids.py','k4pcutils.py','topazextract.py','outputfix.py'] + names = [u"libalfcrypto32.so",u"libalfcrypto64.so",u"alfcrypto.py",u"alfcrypto.dll",u"alfcrypto64.dll",u"getk4pcpids.py",u"k4mobidedrm.py",u"mobidedrm.py",u"kgenpids.py",u"k4pcutils.py",u"topazextract.py"] lib_dict = self.load_resources(names) - self.alfdir = os.path.join(config_dir, 'alfcrypto') + self.alfdir = os.path.join(config_dir,u"alfcrypto") if not os.path.exists(self.alfdir): os.mkdir(self.alfdir) for entry, data in lib_dict.items(): file_path = os.path.join(self.alfdir, entry) - with open(file_path,'wb') as f: - f.write(data) + open(file_path,'wb').write(data) def run(self, path_to_ebook): + # make sure any unicode output gets converted safely with 'replace' + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) + + starttime = time.time() + print u"{0} v{1}: Trying to decrypt {2}.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)) + # add the alfcrypto directory to sys.path so alfcrypto.py # will be able to locate the custom lib(s) for CDLL import. sys.path.insert(0, self.alfdir) # Had to move these imports here so the custom libs can be # extracted to the appropriate places beforehand these routines # look for them. - from calibre_plugins.k4mobidedrm import kgenpids, topazextract, mobidedrm, outputfix + from calibre_plugins.k4mobidedrm import k4mobidedrm - if sys.stdout.encoding == None: - sys.stdout = outputfix.getwriter('utf-8')(sys.stdout) - else: - sys.stdout = outputfix.getwriter(sys.stdout.encoding)(sys.stdout) - if sys.stderr.encoding == None: - sys.stderr = outputfix.getwriter('utf-8')(sys.stderr) - else: - sys.stderr = outputfix.getwriter(sys.stderr.encoding)(sys.stderr) - - plug_ver = '.'.join(str(self.version).strip('()').replace(' ', '').split(',')) k4 = True pids = [] serials = [] kInfoFiles = [] - starttime = time.time() - print "K4MobiDeDRM plugin v{0:s}: Starting".format(plug_ver) self.config() @@ -87,7 +120,7 @@ class K4DeDRM(FileTypePlugin): pids.append(pid) else: if len(pid) > 0: - print "'%s' is not a valid Mobipocket PID." % pid + print u"{0} v{1}: \'{2}\' is not a valid Mobipocket PID.".format(PLUGIN_NAME, PLUGIN_VERSION, pid) # For linux, get PIDs by calling the right routines under WINE if sys.platform.startswith('linux'): @@ -98,15 +131,15 @@ class K4DeDRM(FileTypePlugin): serialstringlistt = self.serials_string.split(',') for serial in serialstringlistt: serial = str(serial).replace(" ","") - if len(serial) == 16 and serial[0] == 'B': + if len(serial) == 16 and serial[0] in ['B','9']: serials.append(serial) else: if len(serial) > 0: - print "'%s' is not a valid Kindle serial number." % serial + print u"{0} v{1}: \'{2}\' is not a valid eInk Kindle serial number.".format(PLUGIN_NAME, PLUGIN_VERSION, serial) # Load any kindle info files (*.info) included Calibre's config directory. try: - print 'K4MobiDeDRM v%s: Calibre configuration directory = %s' % (plug_ver, config_dir) + print u"{0} v{1}: Calibre configuration directory is {2}".format(PLUGIN_NAME, PLUGIN_VERSION, config_dir) files = os.listdir(config_dir) filefilter = re.compile("\.info$|\.kinf$", re.IGNORECASE) files = filter(filefilter.search, files) @@ -114,67 +147,29 @@ class K4DeDRM(FileTypePlugin): for filename in files: fpath = os.path.join(config_dir, filename) kInfoFiles.append(fpath) - print 'K4MobiDeDRM v%s: Kindle info/kinf file %s found in config folder.' % (plug_ver, filename) - except IOError: - print 'K4MobiDeDRM v%s: Error reading kindle info/kinf files from config directory.' % plug_ver + print u"{0} v{1}: Kindle info/kinf file {2} found in config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, filename) + except IOError, e: + print u"{0} v{1}: Error \'{2}\' reading kindle info/kinf files from config directory.".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0]) pass - mobi = True - magic3 = file(path_to_ebook,'rb').read(3) - if magic3 == 'TPZ': - mobi = False - - bookname = os.path.splitext(os.path.basename(path_to_ebook))[0] - - if mobi: - mb = mobidedrm.MobiBook(path_to_ebook) - else: - mb = topazextract.TopazBook(path_to_ebook) - - title = mb.getBookTitle() - md1, md2 = mb.getPIDMetaInfo() - pids.extend(kgenpids.getPidList(md1, md2, k4, serials, kInfoFiles)) - print "K4MobiDeDRM plugin v{2:s}: Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(pids),plug_ver) - try: - mb.processBook(pids) - - except mobidedrm.DrmException, e: + book = k4mobidedrm.GetDecryptedBook(path_to_ebook,kInfoFiles,serials,pids,starttime) + except Exception, e: #if you reached here then no luck raise and exception if is_ok_to_use_qt(): from PyQt4.Qt import QMessageBox - d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM v%s Plugin" % plug_ver, "Error: " + str(e) + "... %s\n" % path_to_ebook) + d = QMessageBox(QMessageBox.Warning, u"{0} v{1}".format(PLUGIN_NAME, PLUGIN_VERSION), u"Error after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime)) d.show() d.raise_() d.exec_() - raise Exception("K4MobiDeDRM plugin v{1:s} Error: {2:s} after {0:.1f} seconds".format(time.time()-starttime,plug_ver,str(e))) - except topazextract.TpzDRMError, e: - #if you reached here then no luck raise and exception - if is_ok_to_use_qt(): - from PyQt4.Qt import QMessageBox - d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM v%s Plugin" % plug_ver, "Error: " + str(e) + "... %s\n" % path_to_ebook) - d.show() - d.raise_() - d.exec_() - raise Exception("K4MobiDeDRM plugin v{1:s} Error: {2:s} after {0:.1f} seconds".format(time.time()-starttime,plug_ver,str(e))) + raise Exception(u"{0} v{1}: Error after {3:.1f} seconds: {2}".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0],time.time()-starttime)) - print "K4MobiDeDRM plugin v{1:s}: Successfully decrypted book after {0:.1f} seconds".format(time.time()-starttime,plug_ver) - if mobi: - if mb.getPrintReplica(): - of = self.temporary_file(bookname+'.azw4') - print 'K4MobiDeDRM plugin v%s: Print Replica format detected.' % plug_ver - elif mb.getMobiVersion() >= 8: - print 'K4MobiDeDRM plugin v%s: Stand-alone KF8 format detected.' % plug_ver - of = self.temporary_file(bookname+'.azw3') - else: - of = self.temporary_file(bookname+'.mobi') - mb.getMobiFile(of.name) - print "K4MobiDeDRM plugin v{1:s}: Saved decrypted book after {0:.1f} seconds".format(time.time()-starttime,plug_ver) - else: - of = self.temporary_file(bookname+'.htmlz') - mb.getHTMLZip(of.name) - mb.cleanup() - print "K4MobiDeDRM plugin v{1:s}: Saved decrypted Topaz HTMLZ after {0:.1f} seconds".format(time.time()-starttime,plug_ver) + + 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()) + book.getFile(of.name) + book.cleanup() return of.name def WINEgetPIDs(self, infile): @@ -185,40 +180,36 @@ class K4DeDRM(FileTypePlugin): import subasyncio from subasyncio import Process - print " Getting PIDs from WINE" + print u" Getting PIDs from Wine" - outfile = os.path.join(self.alfdir + 'winepids.txt') + outfile = os.path.join(self.alfdir + u"winepids.txt") # Remove any previous winepids.txt file. if os.path.exists(outfile): os.remove(outfile) - cmdline = 'wine python.exe ' \ - + '"'+self.alfdir + '/getk4pcpids.py"' \ - + ' "' + infile + '"' \ - + ' "' + outfile + '"' - + cmdline = u"wine python.exe \"{0}/getk4pcpids.py\" \"{1}\" \"{2}\"".format(self.alfdir,infile,outfile) env = os.environ - print "My wine_prefix from tweaks is ", self.wine_prefix + print u"wine_prefix from tweaks is \'{0}\'".format(self.wine_prefix) if ("WINEPREFIX" in env): - print "Using WINEPREFIX from the environment: ", env["WINEPREFIX"] + print u"Using WINEPREFIX from the environment instead: \'{0}\'".format(env["WINEPREFIX"]) elif (self.wine_prefix is not None): - env['WINEPREFIX'] = self.wine_prefix - print "Using WINEPREFIX from tweaks: ", self.wine_prefix + env["WINEPREFIX"] = self.wine_prefix + print u"Using WINEPREFIX from tweaks \'{0}\'".format(self.wine_prefix) else: - print "No wine prefix used" + print u"No wine prefix used." - print cmdline + print u"Trying command: {0}".format(cmdline) try: cmdline = cmdline.encode(sys.getfilesystemencoding()) p2 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=sys.stdout, stderr=STDOUT, close_fds=False) result = p2.wait("wait") except Exception, e: - print "WINE subprocess error ", str(e) + print u"WINE subprocess error: {0}".format(e.args[0]) return [] - print "WINE subprocess returned ", result + print u"WINE subprocess returned {0}".format(result) WINEpids = [] if os.path.exists(outfile): @@ -229,13 +220,14 @@ class K4DeDRM(FileTypePlugin): customvalue = customvalue.strip() if len(customvalue) == 10 or len(customvalue) == 8: WINEpids.append(customvalue) + print u"Found PID '{0}'".format(customvalue) else: - print "'%s' is not a valid PID." % customvalue + print u"'{0}' is not a valid PID.".format(customvalue) except Exception, e: - print "Error parsing winepids.txt: ", str(e) + print u"Error parsing winepids.txt: {0}".format(e.args[0]) return [] - else: - print "No PIDs generated by Wine Python subprocess." + if len(WINEpids) == 0: + print u"No PIDs generated by Wine Python subprocess." return WINEpids def is_customizable(self): diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/alfcrypto.py b/Calibre_Plugins/K4MobiDeDRM_plugin/alfcrypto.py index e25a0c8..b1b0606 100644 --- a/Calibre_Plugins/K4MobiDeDRM_plugin/alfcrypto.py +++ b/Calibre_Plugins/K4MobiDeDRM_plugin/alfcrypto.py @@ -1,11 +1,18 @@ -#! /usr/bin/env python +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# crypto library mainly by some_updates + +# pbkdf2.py pbkdf2 code taken from pbkdf2.py +# pbkdf2.py Copyright © 2004 Matt Johnston +# pbkdf2.py Copyright © 2009 Daniel Holth +# pbkdf2.py This code may be freely used and modified for any purpose. import sys, os import hmac from struct import pack import hashlib - # interface to needed routines libalfcrypto def _load_libalfcrypto(): import ctypes @@ -26,8 +33,8 @@ def _load_libalfcrypto(): name_of_lib = 'libalfcrypto32.so' else: name_of_lib = 'libalfcrypto64.so' - - libalfcrypto = sys.path[0] + os.sep + name_of_lib + + libalfcrypto = os.path.join(sys.path[0],name_of_lib) if not os.path.isfile(libalfcrypto): raise Exception('libalfcrypto not found') @@ -55,7 +62,7 @@ def _load_libalfcrypto(): # # int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key); # - # + # # void AES_cbc_encrypt(const unsigned char *in, unsigned char *out, # const unsigned long length, const AES_KEY *key, # unsigned char *ivec, const int enc); @@ -147,7 +154,7 @@ def _load_libalfcrypto(): topazCryptoDecrypt(ctx, data, out, len(data)) return out.raw - print "Using Library AlfCrypto DLL/DYLIB/SO" + print u"Using Library AlfCrypto DLL/DYLIB/SO" return (AES_CBC, Pukall_Cipher, Topaz_Cipher) @@ -164,8 +171,7 @@ def _load_python_alfcrypto(): sum2 = 0; keyXorVal = 0; if len(key)!=16: - print "Bad key length!" - return None + raise Exception('Pukall_Cipher: Bad key length.') wkey = [] for i in xrange(8): wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1])) @@ -234,6 +240,7 @@ def _load_python_alfcrypto(): cleartext = self.aes.decrypt(iv + data) return cleartext + print u"Using Library AlfCrypto Python" return (AES_CBC, Pukall_Cipher, Topaz_Cipher) diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/config.py b/Calibre_Plugins/K4MobiDeDRM_plugin/config.py index 9825878..9521540 100644 --- a/Calibre_Plugins/K4MobiDeDRM_plugin/config.py +++ b/Calibre_Plugins/K4MobiDeDRM_plugin/config.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + from PyQt4.Qt import QWidget, QVBoxLayout, QLabel, QLineEdit from calibre.utils.config import JSONConfig diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/convert2xml.py b/Calibre_Plugins/K4MobiDeDRM_plugin/convert2xml.py index c412d7b..0f64a1b 100644 --- a/Calibre_Plugins/K4MobiDeDRM_plugin/convert2xml.py +++ b/Calibre_Plugins/K4MobiDeDRM_plugin/convert2xml.py @@ -230,6 +230,7 @@ class PageParser(object): 'empty' : (1, 'snippets', 1, 0), 'page' : (1, 'snippets', 1, 0), + 'page.class' : (1, 'scalar_text', 0, 0), 'page.pageid' : (1, 'scalar_text', 0, 0), 'page.pagelabel' : (1, 'scalar_text', 0, 0), 'page.type' : (1, 'scalar_text', 0, 0), @@ -238,11 +239,13 @@ class PageParser(object): 'page.startID' : (1, 'scalar_number', 0, 0), 'group' : (1, 'snippets', 1, 0), + 'group.class' : (1, 'scalar_text', 0, 0), 'group.type' : (1, 'scalar_text', 0, 0), 'group._tag' : (1, 'scalar_text', 0, 0), 'group.orientation': (1, 'scalar_text', 0, 0), 'region' : (1, 'snippets', 1, 0), + 'region.class' : (1, 'scalar_text', 0, 0), 'region.type' : (1, 'scalar_text', 0, 0), 'region.x' : (1, 'scalar_number', 0, 0), 'region.y' : (1, 'scalar_number', 0, 0), diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/k4mobidedrm.py b/Calibre_Plugins/K4MobiDeDRM_plugin/k4mobidedrm.py new file mode 100644 index 0000000..8adb107 --- /dev/null +++ b/Calibre_Plugins/K4MobiDeDRM_plugin/k4mobidedrm.py @@ -0,0 +1,302 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from __future__ import with_statement + +# ignobleepub.pyw, version 3.6 +# Copyright © 2009-2012 by DiapDealer et al. + +# engine to remove drm from Kindle for Mac and Kindle for PC books +# for personal use for archiving and converting your ebooks + +# PLEASE DO NOT PIRATE EBOOKS! + +# We want all authors and publishers, and eBook stores to live +# long and prosperous lives but at the same time we just want to +# be able to read OUR books on whatever device we want and to keep +# readable for a long, long time + +# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle, +# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates +# and many many others +# Special thanks to The Dark Reverser for MobiDeDrm and CMBDTC for cmbdtc_dump +# from which this script borrows most unashamedly. + + +# Changelog +# 1.0 - Name change to k4mobidedrm. Adds Mac support, Adds plugin code +# 1.1 - Adds support for additional kindle.info files +# 1.2 - Better error handling for older Mobipocket +# 1.3 - Don't try to decrypt Topaz books +# 1.7 - Add support for Topaz books and Kindle serial numbers. Split code. +# 1.9 - Tidy up after Topaz, minor exception changes +# 2.1 - Topaz fix and filename sanitizing +# 2.2 - Topaz Fix and minor Mac code fix +# 2.3 - More Topaz fixes +# 2.4 - K4PC/Mac key generation fix +# 2.6 - Better handling of non-K4PC/Mac ebooks +# 2.7 - Better trailing bytes handling in mobidedrm +# 2.8 - Moved parsing of kindle.info files to mac & pc util files. +# 3.1 - Updated for new calibre interface. Now __init__ in plugin. +# 3.5 - Now support Kindle for PC/Mac 1.6 +# 3.6 - Even better trailing bytes handling in mobidedrm +# 3.7 - Add support for Amazon Print Replica ebooks. +# 3.8 - Improved Topaz support +# 4.1 - Improved Topaz support and faster decryption with alfcrypto +# 4.2 - Added support for Amazon's KF8 format ebooks +# 4.4 - Linux calls to Wine added, and improved configuration dialog +# 4.5 - Linux works again without Wine. Some Mac key file search changes +# 4.6 - First attempt to handle unicode properly +# 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. + +__version__ = '4.8' + + +import sys, os, re +import csv +import getopt +import re +import traceback +import time +import htmlentitydefs + +class DrmException(Exception): + pass + +if 'calibre' in sys.modules: + inCalibre = True +else: + inCalibre = False + +if inCalibre: + from calibre_plugins.k4mobidedrm import mobidedrm + from calibre_plugins.k4mobidedrm import topazextract + from calibre_plugins.k4mobidedrm import kgenpids +else: + import mobidedrm + import topazextract + import kgenpids + +# 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"mobidedrm.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] + +# cleanup unicode filenames +# borrowed from calibre from calibre/src/calibre/__init__.py +# added in removal of control (<32) chars +# and removal of . at start and end +# 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"\'") + # 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 + name = re.sub(ur"\s", u" ", name).strip() + # remove leading dots + while len(name)>0 and name[0] == u".": + name = name[1:] + # remove trailing dots (Windows doesn't like them) + if name.endswith(u'.'): + name = name[:-1] + return name + +# must be passed unicode +def unescape(text): + def fixup(m): + text = m.group(0) + if text[:2] == u"&#": + # character reference + try: + if text[:3] == u"&#x": + return unichr(int(text[3:-1], 16)) + else: + return unichr(int(text[2:-1])) + except ValueError: + pass + else: + # named entity + try: + text = unichr(htmlentitydefs.name2codepoint[text[1:-1]]) + except KeyError: + pass + return text # leave as is + return re.sub(u"&#?\w+;", fixup, text) + +def GetDecryptedBook(infile, kInfoFiles, serials, pids, starttime = time.time()): + # handle the obvious cases at the beginning + if not os.path.isfile(infile): + raise DRMException (u"Input file does not exist.") + + mobi = True + magic3 = file(infile,'rb').read(3) + if magic3 == 'TPZ': + mobi = False + + if mobi: + mb = mobidedrm.MobiBook(infile) + else: + mb = topazextract.TopazBook(infile) + + bookname = unescape(mb.getBookTitle()) + print u"Decrypting {1} ebook: {0}".format(bookname, mb.getBookType()) + + # extend PID list with book-specific PIDs + md1, md2 = mb.getPIDMetaInfo() + pids.extend(kgenpids.getPidList(md1, md2, serials, kInfoFiles)) + print u"Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(pids)) + + try: + mb.processBook(pids) + except: + mb.cleanup + raise + + print u"Decryption succeeded after {0:.1f} seconds".format(time.time()-starttime) + return mb + + +# infile, outdir and kInfoFiles should be unicode strings +def decryptBook(infile, outdir, kInfoFiles, serials, pids): + starttime = time.time() + print "Starting decryptBook routine." + try: + 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) + return 1 + + # if we're saving to the same folder as the original, use file name_ + # if to a different folder, use book name + if os.path.normcase(os.path.normpath(outdir)) == os.path.normcase(os.path.normpath(os.path.dirname(infile))): + outfilename = os.path.splitext(os.path.basename(infile))[0] + else: + outfilename = cleanup_name(book.getBookTitle()) + + # avoid excessively long file names + if len(outfilename)>150: + outfilename = outfilename[:150] + + outfilename = outfilename+u"_nodrm" + outfile = os.path.join(outdir, outfilename + book.getBookExtension()) + + book.getFile(outfile) + print u"Saved decrypted book {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename) + + if book.getBookType()==u"Topaz": + zipname = os.path.join(outdir, outfilename + u"_SVG.zip") + book.getSVGZip(zipname) + print u"Saved SVG ZIP Archive for {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename) + + # remove internal temporary directory of Topaz pieces + book.cleanup() + + +def usage(progname): + print u"Removes DRM protection from Mobipocket, Amazon KF8, Amazon Print Replica and Amazon Topaz ebooks" + print u"Usage:" + print u" {0} [-k ] [-p ] [-s ] ".format(progname) + +# +# Main +# +def cli_main(argv=unicode_argv()): + progname = os.path.basename(argv[0]) + print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__) + + try: + opts, args = getopt.getopt(sys.argv[1:], "k:p:s:") + except getopt.GetoptError, err: + print u"Error in options or arguments: {0}".format(err.args[0]) + usage(progname) + sys.exit(2) + if len(args)<2: + usage(progname) + sys.exit(2) + + infile = args[0] + outdir = args[1] + kInfoFiles = [] + serials = [] + pids = [] + + for o, a in opts: + if o == "-k": + if a == None : + raise DrmException("Invalid parameter for -k") + kInfoFiles.append(a) + if o == "-p": + if a == None : + raise DrmException("Invalid parameter for -p") + pids = a.split(',') + if o == "-s": + if a == None : + raise DrmException("Invalid parameter for -s") + serials = a.split(',') + + # try with built in Kindle Info files if not on Linux + k4 = not sys.platform.startswith('linux') + + return decryptBook(infile, outdir, kInfoFiles, serials, pids) + + +if __name__ == '__main__': + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) + sys.exit(cli_main()) diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/kgenpids.py b/Calibre_Plugins/K4MobiDeDRM_plugin/kgenpids.py index b0fbaa4..c5de9b9 100644 --- a/Calibre_Plugins/K4MobiDeDRM_plugin/kgenpids.py +++ b/Calibre_Plugins/K4MobiDeDRM_plugin/kgenpids.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- from __future__ import with_statement import sys @@ -17,26 +18,24 @@ global charMap4 if 'calibre' in sys.modules: inCalibre = True -else: - inCalibre = False - -if inCalibre: - if sys.platform.startswith('win'): + from calibre.constants import iswindows, isosx + if iswindows: from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString - - if sys.platform.startswith('darwin'): + if isosx: from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString else: - if sys.platform.startswith('win'): + inCalibre = False + iswindows = sys.platform.startswith('win') + isosx = sys.platform.startswith('darwin') + if iswindows: from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString - - if sys.platform.startswith('darwin'): + if isosx: from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString -charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" -charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" -charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" +charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M' +charMap3 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' +charMap4 = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789' # crypto digestroutines import hashlib @@ -54,7 +53,7 @@ def SHA1(message): # Encode the bytes in data with the characters in map def encode(data, map): - result = "" + result = '' for char in data: value = ord(char) Q = (value ^ 0x80) // len(map) @@ -69,14 +68,14 @@ def encodeHash(data,map): # Decode the string in data with the characters in map. Returns the decoded bytes def decode(data,map): - result = "" + result = '' for i in range (0,len(data)-1,2): high = map.find(data[i]) low = map.find(data[i+1]) if (high == -1) or (low == -1) : break value = (((high * len(map)) ^ 0x80) & 0xFF) + low - result += pack("B",value) + result += pack('B',value) return result # @@ -98,7 +97,7 @@ def getSixBitsFromBitField(bitField,offset): # 8 bits to six bits encoding from hash to generate PID string def encodePID(hash): global charMap3 - PID = "" + PID = '' for position in range (0,8): PID += charMap3[getSixBitsFromBitField(hash,position)] return PID @@ -129,7 +128,7 @@ def generatePidSeed(table,dsn) : def generateDevicePID(table,dsn,nbRoll): global charMap4 seed = generatePidSeed(table,dsn) - pidAscii = "" + pidAscii = '' pid = [(seed >>24) &0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF,(seed>>24) & 0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF] index = 0 for counter in range (0,nbRoll): @@ -176,28 +175,31 @@ def pidFromSerial(s, l): # Parse the EXTH header records and use the Kindle serial number to calculate the book pid. -def getKindlePid(pidlst, rec209, token, serialnum): +def getKindlePids(rec209, token, serialnum): + pids=[] + # Compute book PID pidHash = SHA1(serialnum+rec209+token) bookPID = encodePID(pidHash) bookPID = checksumPid(bookPID) - pidlst.append(bookPID) + pids.append(bookPID) # compute fixed pid for old pre 2.5 firmware update pid as well - bookPID = pidFromSerial(serialnum, 7) + "*" - bookPID = checksumPid(bookPID) - pidlst.append(bookPID) + kindlePID = pidFromSerial(serialnum, 7) + "*" + kindlePID = checksumPid(kindlePID) + pids.append(kindlePID) - return pidlst + return pids # parse the Kindleinfo file to calculate the book pid. -keynames = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"] +keynames = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber'] -def getK4Pids(pidlst, rec209, token, kInfoFile): +def getK4Pids(rec209, token, kInfoFile): global charMap1 kindleDatabase = None + pids = [] try: kindleDatabase = getDBfromFile(kInfoFile) except Exception, message: @@ -206,17 +208,17 @@ def getK4Pids(pidlst, rec209, token, kInfoFile): pass if kindleDatabase == None : - return pidlst + return pids try: # Get the Mazama Random number - MazamaRandomNumber = kindleDatabase["MazamaRandomNumber"] + MazamaRandomNumber = kindleDatabase['MazamaRandomNumber'] # Get the kindle account token - kindleAccountToken = kindleDatabase["kindle.account.tokens"] + kindleAccountToken = kindleDatabase['kindle.account.tokens'] except KeyError: - print "Keys not found in " + kInfoFile - return pidlst + print u"Keys not found in {0}".format(os.path.basename(kInfoFile)) + return pids # Get the ID string used encodedIDString = encodeHash(GetIDString(),charMap1) @@ -231,7 +233,7 @@ def getK4Pids(pidlst, rec209, token, kInfoFile): table = generatePidEncryptionTable() devicePID = generateDevicePID(table,DSN,4) devicePID = checksumPid(devicePID) - pidlst.append(devicePID) + pids.append(devicePID) # Compute book PIDs @@ -239,36 +241,38 @@ def getK4Pids(pidlst, rec209, token, kInfoFile): pidHash = SHA1(DSN+kindleAccountToken+rec209+token) bookPID = encodePID(pidHash) bookPID = checksumPid(bookPID) - pidlst.append(bookPID) + pids.append(bookPID) # variant 1 pidHash = SHA1(kindleAccountToken+rec209+token) bookPID = encodePID(pidHash) bookPID = checksumPid(bookPID) - pidlst.append(bookPID) + pids.append(bookPID) # variant 2 pidHash = SHA1(DSN+rec209+token) bookPID = encodePID(pidHash) bookPID = checksumPid(bookPID) - pidlst.append(bookPID) + pids.append(bookPID) - return pidlst + return pids -def getPidList(md1, md2, k4 = True, serials=[], kInfoFiles=[]): +def getPidList(md1, md2, serials=[], kInfoFiles=[]): pidlst = [] if kInfoFiles is None: kInfoFiles = [] - if k4: + if serials is None: + serials = [] + if iswindows or isosx: kInfoFiles.extend(getKindleInfoFiles()) for infoFile in kInfoFiles: try: - pidlst = getK4Pids(pidlst, md1, md2, infoFile) - except Exception, message: - print("Error getting PIDs from " + infoFile + ": " + message) + pidlst.extend(getK4Pids(md1, md2, infoFile)) + except Exception, e: + print u"Error getting PIDs from {0}: {1}".format(os.path.basename(infoFile),e.args[0]) for serialnum in serials: try: - pidlst = getKindlePid(pidlst, md1, md2, serialnum) + pidlst.extend(getKindlePids(md1, md2, serialnum)) except Exception, message: - print("Error getting PIDs from " + serialnum + ": " + message) + print u"Error getting PIDs from serial number {0}: {1}".format(serialnum ,e.args[0]) return pidlst diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/kindlepid.py b/Calibre_Plugins/K4MobiDeDRM_plugin/kindlepid.py new file mode 100644 index 0000000..38c5e4e --- /dev/null +++ b/Calibre_Plugins/K4MobiDeDRM_plugin/kindlepid.py @@ -0,0 +1,142 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Mobipocket PID calculator v0.4 for Amazon Kindle. +# Copyright (c) 2007, 2009 Igor Skochinsky +# History: +# 0.1 Initial release +# 0.2 Added support for generating PID for iPhone (thanks to mbp) +# 0.3 changed to autoflush stdout, fixed return code usage +# 0.3 updated for unicode + +import sys +import binascii + +# 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"mobidedrm.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] + +if sys.hexversion >= 0x3000000: + print 'This script is incompatible with Python 3.x. Please install Python 2.7.x.' + sys.exit(2) + +letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789' + +def crc32(s): + return (~binascii.crc32(s,-1))&0xFFFFFFFF + +def checksumPid(s): + crc = crc32(s) + crc = crc ^ (crc >> 16) + res = s + l = len(letters) + for i in (0,1): + b = crc & 0xff + pos = (b // l) ^ (b % l) + res += letters[pos%l] + crc >>= 8 + + return res + + +def pidFromSerial(s, l): + crc = crc32(s) + + arr1 = [0]*l + for i in xrange(len(s)): + arr1[i%l] ^= ord(s[i]) + + crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff] + for i in xrange(l): + arr1[i] ^= crc_bytes[i&3] + + pid = '' + for i in xrange(l): + b = arr1[i] & 0xff + pid+=letters[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))] + + return pid + +def cli_main(argv=unicode_argv()): + print u"Mobipocket PID calculator for Amazon Kindle. Copyright © 2007, 2009 Igor Skochinsky" + if len(sys.argv)==2: + serial = sys.argv[1] + else: + print u"Usage: kindlepid.py /" + return 1 + if len(serial)==16: + if serial.startswith("B"): + print u"Kindle serial number detected" + else: + print u"Warning: unrecognized serial number. Please recheck input." + return 1 + pid = pidFromSerial(serial.encode("utf-8"),7)+'*' + print u"Mobipocket PID for Kindle serial#{0} is {1} ".format(serial,checksumPid(pid)) + return 0 + elif len(serial)==40: + print u"iPhone serial number (UDID) detected" + pid = pidFromSerial(serial.encode("utf-8"),8) + print u"Mobipocket PID for iPhone serial#{0} is {1} ".format(serial,checksumPid(pid)) + return 0 + print u"Warning: unrecognized serial number. Please recheck input." + return 1 + + +if __name__ == "__main__": + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) + sys.exit(cli_main()) diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/outputfix.py b/Calibre_Plugins/K4MobiDeDRM_plugin/outputfix.py deleted file mode 100644 index 906c6e9..0000000 --- a/Calibre_Plugins/K4MobiDeDRM_plugin/outputfix.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Adapted and simplified from the kitchen project -# -# Kitchen Project Copyright (c) 2012 Red Hat, Inc. -# -# kitchen is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# kitchen is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with kitchen; if not, see -# -# Authors: -# Toshio Kuratomi -# Seth Vidal -# -# Portions of code taken from yum/i18n.py and -# python-fedora: fedora/textutils.py - -import codecs - -# returns a char string unchanged -# returns a unicode string converted to a char string of the passed encoding -# return the empty string for anything else -def getwriter(encoding): - class _StreamWriter(codecs.StreamWriter): - def __init__(self, stream): - codecs.StreamWriter.__init__(self, stream, 'replace') - - def encode(self, msg, errors='replace'): - if isinstance(msg, basestring): - if isinstance(msg, str): - return (msg, len(msg)) - return (msg.encode(self.encoding, 'replace'), len(msg)) - return ('',0) - - _StreamWriter.encoding = encoding - return _StreamWriter diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/pbkdf2.py b/Calibre_Plugins/K4MobiDeDRM_plugin/pbkdf2.py deleted file mode 100644 index 65220a9..0000000 --- a/Calibre_Plugins/K4MobiDeDRM_plugin/pbkdf2.py +++ /dev/null @@ -1,68 +0,0 @@ -# A simple implementation of pbkdf2 using stock python modules. See RFC2898 -# for details. Basically, it derives a key from a password and salt. - -# Copyright 2004 Matt Johnston -# Copyright 2009 Daniel Holth -# This code may be freely used and modified for any purpose. - -# Revision history -# v0.1 October 2004 - Initial release -# v0.2 8 March 2007 - Make usable with hashlib in Python 2.5 and use -# v0.3 "" the correct digest_size rather than always 20 -# v0.4 Oct 2009 - Rescue from chandler svn, test and optimize. - -import sys -import hmac -from struct import pack -try: - # only in python 2.5 - import hashlib - sha = hashlib.sha1 - md5 = hashlib.md5 - sha256 = hashlib.sha256 -except ImportError: # pragma: NO COVERAGE - # fallback - import sha - import md5 - -# this is what you want to call. -def pbkdf2( password, salt, itercount, keylen, hashfn = sha ): - try: - # depending whether the hashfn is from hashlib or sha/md5 - digest_size = hashfn().digest_size - except TypeError: # pragma: NO COVERAGE - digest_size = hashfn.digest_size - # l - number of output blocks to produce - l = keylen / digest_size - if keylen % digest_size != 0: - l += 1 - - h = hmac.new( password, None, hashfn ) - - T = "" - for i in range(1, l+1): - T += pbkdf2_F( h, salt, itercount, i ) - - return T[0: keylen] - -def xorstr( a, b ): - if len(a) != len(b): - raise ValueError("xorstr(): lengths differ") - return ''.join((chr(ord(x)^ord(y)) for x, y in zip(a, b))) - -def prf( h, data ): - hm = h.copy() - hm.update( data ) - return hm.digest() - -# Helper as per the spec. h is a hmac which has been created seeded with the -# password, it will be copy()ed and not modified. -def pbkdf2_F( h, salt, itercount, blocknum ): - U = prf( h, salt + pack('>i',blocknum ) ) - T = U - - for i in range(2, itercount+1): - U = prf( h, U ) - T = xorstr( T, U ) - - return T diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/topazextract.py b/Calibre_Plugins/K4MobiDeDRM_plugin/topazextract.py index bf2ad47..a343922 100644 --- a/Calibre_Plugins/K4MobiDeDRM_plugin/topazextract.py +++ b/Calibre_Plugins/K4MobiDeDRM_plugin/topazextract.py @@ -1,43 +1,90 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- -class Unbuffered: +# topazextract.py, version ? +# Mostly written by some_updates based on code from many others + +__version__ = '4.8' + +import sys +import os, csv, getopt +import zlib, zipfile, tempfile, shutil +import traceback +from struct import pack +from struct import unpack +from alfcrypto import Topaz_Cipher + +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) -import sys +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"mobidedrm.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] if 'calibre' in sys.modules: inCalibre = True -else: - inCalibre = False - -buildXML = False - -import os, csv, getopt -import zlib, zipfile, tempfile, shutil -from struct import pack -from struct import unpack -from alfcrypto import Topaz_Cipher - -class TpzDRMError(Exception): - pass - - -# local support routines -if inCalibre: from calibre_plugins.k4mobidedrm import kgenpids else: + inCalibre = False import kgenpids + +class DrmException(Exception): + pass + + # recursive zip creation support routine def zipUpDir(myzip, tdir, localname): currentdir = tdir - if localname != "": + if localname != u"": currentdir = os.path.join(currentdir,localname) list = os.listdir(currentdir) for file in list: @@ -73,7 +120,7 @@ def bookReadEncodedNumber(fo): # Get a length prefixed string from file def bookReadString(fo): stringLength = bookReadEncodedNumber(fo) - return unpack(str(stringLength)+"s",fo.read(stringLength))[0] + return unpack(str(stringLength)+'s',fo.read(stringLength))[0] # # crypto routines @@ -112,13 +159,13 @@ def decryptRecord(data,PID): # Try to decrypt a dkey record (contains the bookPID) def decryptDkeyRecord(data,PID): record = decryptRecord(data,PID) - fields = unpack("3sB8sB8s3s",record) - if fields[0] != "PID" or fields[5] != "pid" : - raise TpzDRMError("Didn't find PID magic numbers in record") + fields = unpack('3sB8sB8s3s',record) + if fields[0] != 'PID' or fields[5] != 'pid' : + raise DrmException(u"Didn't find PID magic numbers in record") elif fields[1] != 8 or fields[3] != 8 : - raise TpzDRMError("Record didn't contain correct length fields") + raise DrmException(u"Record didn't contain correct length fields") elif fields[2] != PID : - raise TpzDRMError("Record didn't contain PID") + raise DrmException(u"Record didn't contain PID") return fields[4] # Decrypt all dkey records (contain the book PID) @@ -131,11 +178,11 @@ def decryptDkeyRecords(data,PID): try: key = decryptDkeyRecord(data[1:length+1],PID) records.append(key) - except TpzDRMError: + except DrmException: pass data = data[1+length:] if len(records) == 0: - raise TpzDRMError("BookKey Not Found") + raise DrmException(u"BookKey Not Found") return records @@ -148,9 +195,9 @@ class TopazBook: self.bookHeaderRecords = {} self.bookMetadata = {} self.bookKey = None - magic = unpack("4s",self.fo.read(4))[0] + magic = unpack('4s',self.fo.read(4))[0] if magic != 'TPZ0': - raise TpzDRMError("Parse Error : Invalid Header, not a Topaz file") + raise DrmException(u"Parse Error : Invalid Header, not a Topaz file") self.parseTopazHeaders() self.parseMetadata() @@ -167,7 +214,7 @@ class TopazBook: # Read and parse one header record at the current book file position and return the associated data # [[offset,decompressedLength,compressedLength],...] if ord(self.fo.read(1)) != 0x63: - raise TpzDRMError("Parse Error : Invalid Header") + raise DrmException(u"Parse Error : Invalid Header") tag = bookReadString(self.fo) record = bookReadHeaderRecordData() return [tag,record] @@ -177,15 +224,15 @@ class TopazBook: # print result[0], result[1] self.bookHeaderRecords[result[0]] = result[1] if ord(self.fo.read(1)) != 0x64 : - raise TpzDRMError("Parse Error : Invalid Header") + raise DrmException(u"Parse Error : Invalid Header") self.bookPayloadOffset = self.fo.tell() def parseMetadata(self): # Parse the metadata record from the book payload and return a list of [key,values] - self.fo.seek(self.bookPayloadOffset + self.bookHeaderRecords["metadata"][0][0]) + self.fo.seek(self.bookPayloadOffset + self.bookHeaderRecords['metadata'][0][0]) tag = bookReadString(self.fo) - if tag != "metadata" : - raise TpzDRMError("Parse Error : Record Names Don't Match") + if tag != 'metadata' : + raise DrmException(u"Parse Error : Record Names Don't Match") flags = ord(self.fo.read(1)) nbRecords = ord(self.fo.read(1)) # print nbRecords @@ -210,7 +257,7 @@ class TopazBook: title = '' if 'Title' in self.bookMetadata: title = self.bookMetadata['Title'] - return title + return title.decode('utf-8') def setBookKey(self, key): self.bookKey = key @@ -223,13 +270,13 @@ class TopazBook: try: recordOffset = self.bookHeaderRecords[name][index][0] except: - raise TpzDRMError("Parse Error : Invalid Record, record not found") + raise DrmException("Parse Error : Invalid Record, record not found") self.fo.seek(self.bookPayloadOffset + recordOffset) tag = bookReadString(self.fo) if tag != name : - raise TpzDRMError("Parse Error : Invalid Record, record name doesn't match") + raise DrmException("Parse Error : Invalid Record, record name doesn't match") recordIndex = bookReadEncodedNumber(self.fo) if recordIndex < 0 : @@ -237,7 +284,7 @@ class TopazBook: recordIndex = -recordIndex -1 if recordIndex != index : - raise TpzDRMError("Parse Error : Invalid Record, index doesn't match") + raise DrmException("Parse Error : Invalid Record, index doesn't match") if (self.bookHeaderRecords[name][index][2] > 0): compressed = True @@ -250,7 +297,7 @@ class TopazBook: ctx = topazCryptoInit(self.bookKey) record = topazCryptoDecrypt(record,ctx) else : - raise TpzDRMError("Error: Attempt to decrypt without bookKey") + raise DrmException("Error: Attempt to decrypt without bookKey") if compressed: record = zlib.decompress(record) @@ -262,12 +309,12 @@ class TopazBook: fixedimage=True try: keydata = self.getBookPayloadRecord('dkey', 0) - except TpzDRMError, e: - print "no dkey record found, book may not be encrypted" - print "attempting to extrct files without a book key" + except DrmException, e: + print u"no dkey record found, book may not be encrypted" + print u"attempting to extrct files without a book key" self.createBookDirectory() self.extractFiles() - print "Successfully Extracted Topaz contents" + print u"Successfully Extracted Topaz contents" if inCalibre: from calibre_plugins.k4mobidedrm import genbook else: @@ -275,7 +322,7 @@ class TopazBook: rv = genbook.generateBook(self.outdir, raw, fixedimage) if rv == 0: - print "\nBook Successfully generated" + print u"Book Successfully generated." return rv # try each pid to decode the file @@ -283,25 +330,25 @@ class TopazBook: for pid in pidlst: # use 8 digit pids here pid = pid[0:8] - print "\nTrying: ", pid + print u"Trying: {0}".format(pid) bookKeys = [] data = keydata try: bookKeys+=decryptDkeyRecords(data,pid) - except TpzDRMError, e: + except DrmException, e: pass else: bookKey = bookKeys[0] - print "Book Key Found!" + print u"Book Key Found! ({0})".format(bookKey.encode('hex')) break if not bookKey: - raise TpzDRMError("Topaz Book. No key found in " + str(len(pidlst)) + " keys tried. Read the FAQs at Alf's blog. Only if none apply, report this failure for help.") + raise DrmException(u"No key found in {0:d} keys tried. Read the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(len(pidlst))) self.setBookKey(bookKey) self.createBookDirectory() self.extractFiles() - print "Successfully Extracted Topaz contents" + print u"Successfully Extracted Topaz contents" if inCalibre: from calibre_plugins.k4mobidedrm import genbook else: @@ -309,7 +356,7 @@ class TopazBook: rv = genbook.generateBook(self.outdir, raw, fixedimage) if rv == 0: - print "\nBook Successfully generated" + print u"Book Successfully generated" return rv def createBookDirectory(self): @@ -317,16 +364,16 @@ class TopazBook: # create output directory structure if not os.path.exists(outdir): os.makedirs(outdir) - destdir = os.path.join(outdir,'img') + destdir = os.path.join(outdir,u"img") if not os.path.exists(destdir): os.makedirs(destdir) - destdir = os.path.join(outdir,'color_img') + destdir = os.path.join(outdir,u"color_img") if not os.path.exists(destdir): os.makedirs(destdir) - destdir = os.path.join(outdir,'page') + destdir = os.path.join(outdir,u"page") if not os.path.exists(destdir): os.makedirs(destdir) - destdir = os.path.join(outdir,'glyphs') + destdir = os.path.join(outdir,u"glyphs") if not os.path.exists(destdir): os.makedirs(destdir) @@ -334,149 +381,148 @@ class TopazBook: outdir = self.outdir for headerRecord in self.bookHeaderRecords: name = headerRecord - if name != "dkey" : - ext = '.dat' - if name == 'img' : ext = '.jpg' - if name == 'color' : ext = '.jpg' - print "\nProcessing Section: %s " % name + if name != 'dkey': + ext = u".dat" + if name == 'img': ext = u".jpg" + if name == 'color' : ext = u".jpg" + print u"Processing Section: {0}\n. . .".format(name), for index in range (0,len(self.bookHeaderRecords[name])) : - fnum = "%04d" % index - fname = name + fnum + ext + fname = u"{0}{1:04d}{2}".format(name,index,ext) destdir = outdir if name == 'img': - destdir = os.path.join(outdir,'img') + destdir = os.path.join(outdir,u"img") if name == 'color': - destdir = os.path.join(outdir,'color_img') + destdir = os.path.join(outdir,u"color_img") if name == 'page': - destdir = os.path.join(outdir,'page') + destdir = os.path.join(outdir,u"page") if name == 'glyphs': - destdir = os.path.join(outdir,'glyphs') + destdir = os.path.join(outdir,u"glyphs") outputFile = os.path.join(destdir,fname) - print ".", + print u".", record = self.getBookPayloadRecord(name,index) if record != '': file(outputFile, 'wb').write(record) - print " " + print u" " - def getHTMLZip(self, zipname): + def getFile(self, zipname): htmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False) - htmlzip.write(os.path.join(self.outdir,'book.html'),'book.html') - htmlzip.write(os.path.join(self.outdir,'book.opf'),'book.opf') - if os.path.isfile(os.path.join(self.outdir,'cover.jpg')): - htmlzip.write(os.path.join(self.outdir,'cover.jpg'),'cover.jpg') - htmlzip.write(os.path.join(self.outdir,'style.css'),'style.css') - zipUpDir(htmlzip, self.outdir, 'img') + htmlzip.write(os.path.join(self.outdir,u"book.html"),u"book.html") + htmlzip.write(os.path.join(self.outdir,u"book.opf"),u"book.opf") + if os.path.isfile(os.path.join(self.outdir,u"cover.jpg")): + htmlzip.write(os.path.join(self.outdir,u"cover.jpg"),u"cover.jpg") + htmlzip.write(os.path.join(self.outdir,u"style.css"),u"style.css") + zipUpDir(htmlzip, self.outdir, u"img") htmlzip.close() + def getBookType(self): + return u"Topaz" + + def getBookExtension(self): + return u".htmlz" + def getSVGZip(self, zipname): svgzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False) - svgzip.write(os.path.join(self.outdir,'index_svg.xhtml'),'index_svg.xhtml') - zipUpDir(svgzip, self.outdir, 'svg') - zipUpDir(svgzip, self.outdir, 'img') + svgzip.write(os.path.join(self.outdir,u"index_svg.xhtml"),u"index_svg.xhtml") + zipUpDir(svgzip, self.outdir, u"svg") + zipUpDir(svgzip, self.outdir, u"img") svgzip.close() - def getXMLZip(self, zipname): - xmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False) - targetdir = os.path.join(self.outdir,'xml') - zipUpDir(xmlzip, targetdir, '') - zipUpDir(xmlzip, self.outdir, 'img') - xmlzip.close() - def cleanup(self): if os.path.isdir(self.outdir): shutil.rmtree(self.outdir, True) def usage(progname): - print "Removes DRM protection from Topaz ebooks and extract the contents" - print "Usage:" - print " %s [-k ] [-p ] [-s ] " % progname - + print u"Removes DRM protection from Topaz ebooks and extracts the contents" + print u"Usage:" + print u" {0} [-k ] [-p ] [-s ] ".format(progname) # Main -def main(argv=sys.argv): - global buildXML +def cli_main(argv=unicode_argv()): progname = os.path.basename(argv[0]) - k4 = False - pids = [] - serials = [] - kInfoFiles = [] + print u"TopazExtract v{0}.".format(__version__) try: - opts, args = getopt.getopt(sys.argv[1:], "k:p:s:") + opts, args = getopt.getopt(sys.argv[1:], "k:p:s:x") except getopt.GetoptError, err: - print str(err) + print u"Error in options or arguments: {0}".format(err.args[0]) usage(progname) return 1 if len(args)<2: usage(progname) return 1 - for o, a in opts: - if o == "-k": - if a == None : - print "Invalid parameter for -k" - return 1 - kInfoFiles.append(a) - if o == "-p": - if a == None : - print "Invalid parameter for -p" - return 1 - pids = a.split(',') - if o == "-s": - if a == None : - print "Invalid parameter for -s" - return 1 - serials = a.split(',') - k4 = True - infile = args[0] outdir = args[1] - if not os.path.isfile(infile): - print "Input File Does Not Exist" + print u"Input File {0} Does Not Exist.".format(infile) return 1 + if not os.path.exists(outdir): + print u"Output Directory {0} Does Not Exist.".format(outdir) + return 1 + + kInfoFiles = [] + serials = [] + pids = [] + + for o, a in opts: + if o == '-k': + if a == None : + raise DrmException("Invalid parameter for -k") + kInfoFiles.append(a) + if o == '-p': + if a == None : + raise DrmException("Invalid parameter for -p") + pids = a.split(',') + if o == '-s': + if a == None : + raise DrmException("Invalid parameter for -s") + serials = [serial.replace(" ","") for serial in a.split(',')] + bookname = os.path.splitext(os.path.basename(infile))[0] tb = TopazBook(infile) title = tb.getBookTitle() - print "Processing Book: ", title - keysRecord, keysRecordRecord = tb.getPIDMetaInfo() - pids.extend(kgenpids.getPidList(keysRecord, keysRecordRecord, k4, serials, kInfoFiles)) + print u"Processing Book: {0}".format(title) + md1, md2 = tb.getPIDMetaInfo() + pids.extend(kgenpids.getPidList(md1, md2, serials, kInfoFiles)) try: - print "Decrypting Book" + print u"Decrypting Book" tb.processBook(pids) - print " Creating HTML ZIP Archive" - zipname = os.path.join(outdir, bookname + '_nodrm' + '.htmlz') - tb.getHTMLZip(zipname) + print u" Creating HTML ZIP Archive" + zipname = os.path.join(outdir, bookname + u"_nodrm.htmlz") + tb.getFile(zipname) - print " Creating SVG ZIP Archive" - zipname = os.path.join(outdir, bookname + '_SVG' + '.zip') + print u" Creating SVG ZIP Archive" + zipname = os.path.join(outdir, bookname + u"_SVG.zip") tb.getSVGZip(zipname) - if buildXML: - print " Creating XML ZIP Archive" - zipname = os.path.join(outdir, bookname + '_XML' + '.zip') - tb.getXMLZip(zipname) - # removing internal temporary directory of pieces tb.cleanup() - except TpzDRMError, e: - print str(e) - # tb.cleanup() + except DrmException, e: + print u"Decryption failed\n{0}".format(traceback.format_exc()) + + try: + tb.cleanup() + except: + pass return 1 except Exception, e: - print str(e) - # tb.cleanup + print u"Decryption failed\m{0}".format(traceback.format_exc()) + try: + tb.cleanup() + except: + pass return 1 return 0 if __name__ == '__main__': - sys.stdout=Unbuffered(sys.stdout) - sys.exit(main()) + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) + sys.exit(cli_main()) diff --git a/Calibre_Plugins/eReaderPDB2PML ReadMe.txt b/Calibre_Plugins/eReaderPDB2PML ReadMe.txt index a4d3e81..69a07ff 100644 --- a/Calibre_Plugins/eReaderPDB2PML ReadMe.txt +++ b/Calibre_Plugins/eReaderPDB2PML ReadMe.txt @@ -1,26 +1,27 @@ -eReader PDB2PML - eReaderPDB2PML_v07_plugin.zip +eReader PDB2PML - eReaderPDB2PML_v08_plugin.zip +=============================================== All credit given to The Dark Reverser for the original standalone script. I had the much easier job of converting it to a Calibre plugin. This plugin is meant to convert secure Ereader files (PDB) to unsecured PMLZ files. Calibre can then convert it to whatever format you desire. It is meant to function without having to install any dependencies... other than having Calibre installed, of course. I've included the psyco libraries (compiled for each platform) for speed. If your system can use them, great! Otherwise, they won't be used and things will just work slower. -Installation: +Installation +------------ -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 (eReaderPDB2PML_v07_plugin.zip) and click the 'Add' button. You're done. +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 (eReaderPDB2PML_v08_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog. -Please note: Calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added. - - -Configuration: +Customization +------------- Highlight the plugin (eReader PDB 2 PML under the "File type plugins" category) and click the "Customize Plugin" button on Calibre's Preferences->Plugins page. Enter your name and last 8 digits of the credit card number separated by a comma: Your Name,12341234 If you've purchased books with more than one credit card, separate the info with a colon: Your Name,12341234:Other Name,23452345 -Troubleshooting: +Troubleshooting +--------------- If you find that it's not working for you (imported ebooks still have DRM), you can save a lot of time and trouble by first deleting the DRMed ebook from calibre and then trying to add the ebook 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. ;) @@ -38,4 +39,4 @@ Now copy the output from the terminal window. On Windows, you must use the window menu (little icon at left of window bar) to select all the text and then to copy it. On Macintosh and Linux, just use the normal text select and copy commands. -Paste the information into a comment at my blog, describing your problem. \ No newline at end of file +Paste the information into a comment at my blog, http://apprenticealf.wordpress.com/ describing your problem. diff --git a/Calibre_Plugins/eReaderPDB2PML_plugin.zip b/Calibre_Plugins/eReaderPDB2PML_plugin.zip index 0282220edfbb8c93f3ae1cf557b1d3ff93054025..1cee255a89c4c82d26176ef2aab4ec5b563d5ae2 100644 GIT binary patch delta 14493 zcma*ObCl+4(}(-CZQGc(ZBN_wv~5p6ZQHhOyQe*E+qS2D_I}?F`~A-Oq*Ljq0p8 z@6M^X-D(80pX^ty0L#D9Y<7aH_VTv*3jLXKs$21r=E=DZl%wKD{*}Db&Xj82i?7!M z1ag+c3Z~Ljs}t*mi#X6a9bs^#=|CR^eWRamyxE-`LhvR_V(KzX_0=AY>P}2oOvkS0 zHL6;O==RI%rO&DyhJud4x5GbzHzokqN5V;XCZSsUD4fw8OkzSR&*Sm zq`x4xtTQaJrfrI6Le;L`r#TK*4_N&T933x?drYDCAp9epOOLG^*Y*~gldzetj(mg+ z1*tdVG!Dnj9^izO3M-}b4&>yJ842fCxiTp#L-i2-Z>=h zv$7;jgI7%B= zB%+I~Sfk!%0Ar$Hib`2TE-A_yjKcAYHd^a8XH?R)h%Z1tbl+BPx5y!C+TMlA1#E&L zIt@lNU2@@%t`OQp+@4NJVCe-*&|E{gyL@HRrY6eNH@3J`8FEL$a`Lr;2No>NGdiI$ zEG-D%6r071&FfxHSGXLK_bHM_;F1C=B$cOth1!?O8%Wlq5?ix#srKZ-M&?py@5D<% zpS=dKGD(=0x8N6~xHWs*is0kYsj{yn{uL*~uDN;B6VQ$nZ2%JI7?xk6oKyBz8JxgG zqQCk_ElPR2i!Ib}%2?Z2`rRQqXLT8uuJEN?c_{atW{Quj9iAIVc;T@mkkPkjo2QX= zj=e1~cgrMl=1Qg7-nNWZ-HPl?JD>2p46ivNfJAxHr42=`fWo~T9|%2!HFa}K@v%2N z6ncKo%sc;pbm4HJP)i^kj)!n^3SJo3T3{Ecy^e_s4(;(o#|1eP2jV|7CZCBxky1bS z!1QzKNoi&-|0u1Tq^6hv=zmnn_Y281ZJ&YXmrQ3KVK$B)sDiApUqRE_vIT>6B;=(w zGb8bEnDVwT{Iv6X^#u*tpV4?FnTDAOX)q}tHOqbNxJ%XVes*q2Jyt`X(DlWtK>?N0 zIxd6VqdI33a_isNr0L?TJ38^$3STpBZ7%N>Tog8Gy1|J_7M+cBfNQifCR*t`-!XBE zE!FW0{OCcT7pZNG>c+;u@t}JIu@QPu1dbRES*}VC-3X%*dr<0J=gXTOp_jjhLvU>$QG$=V}yZ z!`mh`S=5z&j`1%EuoFD_8_juWcIxJ7P&jH2DTH#^)qV)K6-_ebPTfIDb5`C8gP%kp z8Yc=|lpbSr7%)e&+4LM(ZG5y{dc?~ObhGEsKomuAm?MZkK$#fGrXENLEK1o#ZfJ3O zO;l0q-~v%qSL(PP6gB@0Fv18N;qkdqra+}4)3phJUCPe`R>#5;Ux}TxSZ`{~s9Ky% zS@I57mhVjHW`!2-btUtWG4m&Lj^y^Jfy_lUB&}5RxmmW2gpF{9TiTNhmLGM}k2)%2 z^eC5NWOp&iKt%u_EcdvW$SoIA+y( zPaeK$fGE%h{=tRVLsUPUZ!@Z&{~CeRGH)C*wZYt&1&Wv%4V4O$B=-_UtpEtb?dwM% zb5{w?dm2I15vRt)&=)RFyQ%?W<;-jT62 zZ#2J-I)zk*aD!a{9|Uu}sMFa^PyhpWy73%+3a9{xu>UxDfFhJ*9<6{)j0=&At6n4* zQP6m52Ht|Q)OM}K#e1W@3GS&O`|nc97|4^B$>CmG_4%?JGa4)f>)?c=cPy5yPa`3D z+v<@W)aTG%VWO2%1z_SW80)Nh6Y!mjXkj7mM<8<2-;`@EdXEr4?HFzv(?-^eIzD=! zjV7?0woTGUc?RJn?XD@dMOJS=_Y(U4a5u;W0`bVID%&oPNm(n$>uV*q=X|vVdC0R% zaBbDVCd-mU66EV&6cs2lLC#Kwv?W~~x^MjQ~4B?CvXu334HA! zh$-9IRO`0WoyT}=RscO>k)ZJHA)X?o->s(&n1{mMXC5g?yMlivn$e78_v%P&mg5r9 z0&S$0%3OqdAaIWIvprL+axR@svDV=&h* zMZJ`;CFmxI4LA}cbrLdDp)zC*Aw2L(l^1-Sv%s~qo^Bf2rom~hIrTjp?V`4f3`f@j z&k%l7)1bA0DWd~8^S*}-sla-+vN~$p0x>V<_n&5#CfufnR4%{lG^%i^Wa<-RgiSVr zMy0^Iiyx>w#a3*x%U8a7`=(cZ6qd-%#c_-5C?)99v{#5IlWI@bAwJy~uBwZ(MLu`Pf)e!?#hc|?>=|9`g{sM>oK#P4 zh!wH9B=g|PoJNF`6>Dz_TBE$mc z+adajIL5nmIDh{n8;(;yhb6C$ zZH$pRTobM-68f@U zi7(X)8Bz~`g5X2OiEVGGZIL{qKI1-9dnTM;rGuCO6z3@0RW&;?VSYb6Jh(sm*Z`la z&|exollb<>5GIy}Smat-RVgJf!CyALFP%>srY~hvbNa}Hh8U6C$XJ9l01p8epBn_k z-CFT)0H|Z4Hz6Vj39_Q&YKdyf=&&(=H7G%N|6^J~Y0aDz@g$*C@@`X*n`G$YLZ9JT zzUTYLMm5|NK+-Eq%bqVOz!(?KA{R(Zmtq#i>Yy)|DK?cv#m*3gA;B#zh)gdJLNbHe zM+S2E{hr-v*&x1+m&D+YDf}Go@*NI^mR21&Zkbkn+Znb9LMH|cg<=Vmb%{=1z2y|Tgcq}q@k4$)yZQZlJ3n5D(A*5J zwJ;l2L@L;cELTBs#hQQISPt;Z$?G#)*#AVQ$UkXxj~WF+3Lceco2$>{3-T8M1nN-l z???(M$jOpA%Rnc1(YcGPjL^3&|8w@^7XVV|W0iX|0iky&Z?nW3rzLtoJ%9nCs51A1 zPdK{>C$jhp{*&k%Z)5E6H#xXG*jnK{@kC=D!!-+kA-~FR4h&_?fzm+LF*D=Xf!?{W z?n7Utfgj(*g`S;~nT>gy2Gh&kYQN8-Bq)yu4@3BSUVMb!b3QgwgEvpJTqSm<2g(C@ z@lB|4nTR#SWrPymg)yZ?U^!M@0dS}chG=L-B_sghab%!I-V}-lopE9vvdx?y)to&m zR2N%Dwy?TXcFlW83QYMcjoWQSL0e2ejiOn8Cz z81BZsj1}}7qyP=gYk=vuH1?)Gp^@4hJ9uoP95bvP&%Jzge)8eR~mVmi?r&x2wN$tL8cc*3if^CCBjgIEB*~e zfy57lJb`v|HyF4H60a-PPHl$c2tBI#^~>NhSwS~34CNTPJK2CbtdK-VR~#k}|JM%b zglsSNSpE)(Tp9VERd}x`2d>vQsgjEC6NtLPjIkn+Sip(d`;u3p@*A!ky48$_KMuba4t2)auxFURcGRSal!6;6iXI6<;91 z_7+JZEkQd!m$8J=Q7|0L3?=*E0I^LJw^kVaLXy%f(8mhCc~q{{N}%-==*VAeV4QTu zZg)p$5Wzrei7z@Ir8fAZw0s10JM4A831vN@WMOlu4`rNd^kz;KFo9Z9W|f`B<_id9 zWO6a%R=yZDtR!db^Twzn5`5jjSe}%^xW0rxJ^*}ypI%VABYOEf1zjqjjg&FUX7!5s zB-9vjK}V5u0coT9KN2h#Cv2cZ{UEQ%RRKct|Irzl%xx zi6v4m3v^sjV@PFKbAx-V3QCL1P4MmL=rpEhjM;EplHGhRr4*5)S+htkPLqb-nD!Ct zP{acX>S*4eu^Imyu>%(nD{VI>ddiP zTl1S9dychw(0RNq>JSnB;CbZ^MV^#wYU{1=iI2z!VNYW+0m?Y((-KO=0fy7q_b)&~ zoH#)-{Jmabig+Wc0O)cI0sU2YYA8EWU@O20?n6rc8*;dVU0cL4vUY-=yB2Cgv)p%x zul5E8C(JnPx>dRWRc8+888-yAVVC(Db86$Eh|vo7OXqVj4; z^AOdN$szi|H1d(Od?XXM2y{k%QicnWDbxL_VB*}%gkT0B7Vl)C34lr?C=BLJ22`L2 zDim(_J-_3MBf=f%$a}#kE$luLSb3J{LpsyqIJ04S32~W7GJf0uagm>aKkkXrRAh_K z=|lea?ckE~w$56uljDX%Oyg1uw)^`^JV_GqB2};@3p-JAFb+TwXUy~P7E%|NyhfUB zPhiwDyUSOU8G@OxC>dY&AK3BnWKST|&bRQVmMHVy)C@*qxwb)l8cXQ0Fw2P=`e zTsY-4+z6Iauv6AqiBy=COpj>e;>(Zo#(5gK1iFz{bj|Cj<=}A3Lr}GYg)dv8x*A+R zQT=)V8AHCT0PmZ_>*xEVB$M2E7T9zkQwSY7K#dksRS=YMVRkc612fCrd<2*)m<{&V z&YxDo`I0a59WKOOy?ef0+fSdZ1*C8)sH}#_iRfx42I{;ls~{_}D7WMCq`>lw{L)BA)f?t7#i}W6{S|E2HoX!+rdeD zrsda-iedMRHvLEbBBK*G1d|5tbM>AabUOj1>Nn(TA5Q zzvCA?sN#C_M0GXV9QZGbO55~SwW5gr%8DOjVXX}ZJo4#m7tuaED*eTwrtfrFvE>rP zwy}`xa3t(DXdWuG;kCe*l}HuAtWzrKNdTwSDmjNVh`}6xXh@sZ5L zAyO7Ac-07fY0)r)@W|9m0C)FnM!J?tNCR4qIv*S^x~PwicMy13Qf6pM`7O|O&9q}t ze6&}M5v4S=Y0j$fG@rTHNZJ&M{ezoJ2lvgDbGJMnpS@dX#Vn)ZsK z%Oc+|x+1h;q9E!Ah2cBH&JM`Ijr|?pY5c9!*e7HWwN=`+)lD2bWbs<#1p^Bq4$QzTJ|crP`a~yoG$%-88wE9i ztK{#H0$ZoOul0f!08d{oPhXc)TvAu%RPw$L3Qdi$bgSvCdn76%$~VE=l4vjHBPKXTsBap5j%iRKIb_(z?{|kMH(0>y!-+zJ_YXJqTBQf- zJUD_Qiwi4FhRS&}wqvO-EyvPk zRk<#9pn*QTwZP{5l2vJdMCQ$mX8~lF7*#Rsjv9fKHPaH<@i(aj`Zj`jS53j3a6XTc zUh8?1R;y8~Z#7RwpCDP_xe{szY-Upv-SxB@ajKr<76(R(jQ5B>V51crPLqhK4fr#) znbbgiRze8~iz>QUG-GpLoW?{JD3oFL4C~+MmLl8*!BtoSc0d0NZx6_B55hrhcdRu; zIx$3)QAcK=3uTHEf)L)WNiCN1cjt_I{Z%*TGQBpnSA1gds<}PE5a?(ktcMv4yado3SD4lw|CB5I z)xX>j6J_8x?bR$eDe86FlL4BFaj|Y16j_l@^W%pWvwAO-mtmZkG|*(BkV8R8a&tzE z?3mDD{M-E&)+`=CU5M4|*w~SF9G|~VSGU42)RLr>Y_}jsxmH6I7qd4IR!M#F9-bCx z%k*l@*52f=F-q!kbONiG=Hzc_jj!iSldPW4I0jc!r#M-4;r+AKyKL-ADyG;a9aTGF zPu%61x5usKWv$w}`sU`%wLjal^OMv3YZyQDGdqypbuqR6v!S&2;+zrcbt3=s4!;-v zIwpkSE&APhFNBC5AFH3mosEMUx#t{M0gf&XQP-o2F%b8)J17a&is}zD1sNrc2K(W& zc0He3YjMqjQ^lTz?u6{!d6cv|l|%x`#RAgx$1A*T-rm9oMGqGPJ?Imoy2|pUw*Vg5 z0gR+^WG2hI2CRnDiEL6U2U0hz3B}cQage))Nk}Aa+DdUAo#h)@t)4+z(^{iFSAn zfw*V@!*oIPes;I+@nk1>GS&@Q)t+gdTFoNp_wGtq^oD@<-0%sZbPTQubHs78YC`mQL#7j0Ju$50t6F~(ali-TDL3& z$!(c~#a++##~O07Uimuiu&s7{umx;Gv$qR3Q@d?a!61IYqLV-+dz;L)bNPeWgXU0& z1=FH>q+Iq$;?J|+ZU|)BAbISop~U#|!=b!-)T7-5>`{}%PgMIKl+aRu!y_??V$<1S z`Rk)HWc-%(GWK&hIr0b~Cb2~m@0pcpKm*?CM*SXo`d|#kZ~3ZD%LX9Vs(t(;NEfHj zTJA+_S>=q|DDCNn+A=!Nf+d-7=g0S!a__Isewghlo5p8OEBGx(;#>DsBK6ErB#^$+g;v6!ohN zvaI8%Q5#Hr$Mm8VL6HprXG1foW)tGM!$&rJvN^URY{irVYxl5 zZd-g}vu0Zhzr0|zUPt=O&c688<3^txI9yE!cH!fB_|OS7PNd3bU|D0_fbwVaIZbq^ zTjgIXWb?ASdS*gxk+j({1e{fm zyTq%d!$lxK6kJ8QLUnLXbgrPlUw%$){ZUS?dNT<5h+#o*;>A~3o8Xx=h3~!%T{3vH z!kY#%5cZ#T*3ey+%LT7{g^bKa|9Ec9-TIjaI0auzM^D6S1KUU~siLqj$W;z4e}`(Sxh5D?6kDR-3xB1ilvO@apAjG0=BHfKW5w)kxO`Qk=V6qt47rJ~ z8c^xH&PzStyo+T25|7Rhlt)m&H1Y$AQ%wXr{Pw7H{vJ`4eV$Q@f3vVLHxi9BtR;@K zt;%O$^V?WQXVf|J;E~DWMpz^HfqAiFqlmM%sv>%Ih(%$7)>D5ENodsBAUnHQ<$GJB z(v3KdP9Pt{!djda+QzoS0fj8y^o)EgOM( z-&{7WXO?5o#Q)hjXsNDOf~E79Bil;fe2lVBsNtCa9z+s2Rn`!GYPSe6_*Vqyz}=Q#%LD~JdGY#&;lE%mbBCr7@6FSh}tvPx4Je&yqXnt+WY% z&+5x0zMO7fD&p|kvr_C&P5t6eL`nRfml11;R;lXIyW5!)_KPcWMTE?T;*$jGPiC`c z6{Vz>=O(artKiGPLXgqUHT1Bf5|3Sy;9c>>yc|HaeNs1C4dScOWzE(>EnT;#ndpT0 z*Tu&OG7=&-gh&Hh^+!nbN081DzQ>i8t5WObFYIk+a;K zrrb6A8GR8i@dO#wuo3#|cT5M~08O4s%tSWh@(f^M-KX`Z(y0J6&B7rymF4xb^rUDYz0r!1AW4? zmP;4vL_^q~JP6OQ94wb>2Qcs{@N?uw&^u=XeXT_mH#=lsyZQ^;w2V@SC^)P%M^(>o z>qGeQ?!gB5boxFx!bt_@=@^0#TLVX|;P|bX^!Zcp`FJeHT48=B9#9I21ODYa3ghbABne@kEWh0sqU`cKD zl=A-iS{Bp)$~8Q}>yO<f`y$SGcniZzFH&jVoyhWF%;0WqT#<*%$c^+T=aUDFy1y+r&gC3E4Ul1Y4w4Nzy(o*UGH14U&&v&Jq>MREcjy)qxC{$ zYw{aI`okNc{;GR^L59pzaVuonK~~R$1C}N>Mq}UfAdU&2a}MXunxLT64B8U@Pj}1j zMBT`D6+sw5=zb2Bl4g&J>$9U%c+Vo^`<-V%ni5Rt{hGR7#GsG+pW02Q5j2`3&8Jov z7!Y>#iG?r-gygl1r~HY0=k`J!csX5huvDb&8iS>$5A3x0;=N!6?2>&fBHki}Px*wV8&*?Qed$f&4`~>YT}=db zkYPX-5D)~9Q=AejYA4<3m}XZtn%ohm3y2@hR(l}l?A1d)!QQF$GTPG`G477E3J_@K zVTs-k!C??UxwK0w7Mpr-RV_EpF*T3=$f=)c_3;!Yyoe?UF6KLHQp^=DR6-t2#5Tjz zIPRQwPwmm+kyk^tWX_HlAhxkLw^+O0!099xSU|1mX+~5jyt)O2K z?RGtirs9tyzj9Y;cOCbr6ZA}zrstY3ub=o{8bO04g3%{I+0CO2M1?b5;DLp$@b#1A zWfG>OU1A-b-FSKMP-LAKMAeJ7ylgF1Q5=fhrA~!3#ZcI;ra;4xw7Rp+4-{*eBTeg9rY0RhSD%TNd(Bn#W6_))(`nyp~ z%P^;@P5=O?8T_CA?OzRFd`wd(6DJl2TN?%k&;M=@|JBX?yZ;MjpbnmBPsfQne&1L2 z|84-sIvWWkx{;Iqs{tGns-W<9M>$ztDYlRU*>}1&ZC*g=NwHzI)2_9>=G7t7y0Ye} z!tPD67=?w2dyb+0Hn719OlO7SHY-_kffq$q=Twb10pgCljA)p$m{bW^=lf0nigjX#LI*? zo&+hesYw%z?tcU%krI|?#woX`n&L-n3-Jz<4m3oo-35AikwlbEPj9c|wlGYrs|m(1 z)-6t~>Y2aGH2czt64*0+V|kxBIe3y#GwdVYJ2xv-RQqMoIxJxZvqPwMs;;)*mJeZq zbD78w!rFsrS2-r(SQ(JwkasMB`3rgWthEXp=%ztG>(BI3Q<2H_L%Wj+sQpgZ)?gaUZ#UHa>Hml-fVrm@q}L99s!&yg4V){bfK!|<0zAc}h+ ziUE?u29ksiD>ic{4kAv@rB(g zA(cUXi@7-<;PPZ_VQrJH`Xd#Sl(K~#*DQ)ByBmmMvT>Wa;BsK_+;J3=b0OCXywK^L zFJQyRITyvt&@aoru2@87Z#?u}wLV7-7^NyOK23Wp2H9 zjM}I%Xk_0GhN!@D`M`~^=PD?F6!5hnr+oLB+TN<2@+p%wUQi`A+P`G8JnWA)s362! z4f2gvBKM6w+Q=T%q1lvK^V&L4bK2DidVEn?r*~y?@U!Nzcs2ULb$XF@HJvUq<9#7Ap)S z|1m{$YunndkD~m=*4C%)Sd}Z`)29|$kW4}Ga`%OKbNvAY3$B1|5F2taNg%cy=_vT> zzHjK%?HN_lX_7#V>|||Zon~dtTHHL(-QrIf>Rc!PiB+mVx~=A}<@KP=M}ah7!FOMJ z=>AT%uDW>FQq_Q;OPB(L5Rl(L_DQeVomaYxGZ&{A*Yk9FINj6Df*~1}#5%@$_)b{e zGSPrewC@zRxiwO0#l;`sTCaIP3q#sSj1MCi(7PY6xFzNBDJ7+eR!3<_pVqha?LE1! z)=<#zV=OG{2FV6?1zv~*06`Q*&9vu_0%MK31YQL}jOCYwB%uo2!G?B3L-$cs%*zw% z4`he223-dy#b_b2pk05^q!49>8| zZ@DeeM%QYtN}!`hL0t(E)IV1E+S}MWjc+i1XR=XX4smEJjeTBKbAj_~WLE5ocZ^oT zq$Xm51V1WA90~^#Xo)JKu)^aoi_xKwxmLj3rfY2*A$%8FLb)b3G=u5zOP3f{>qDl& zGfq$$elQOj1ADcM-}KX8w7bYp!7d)f=!B|K;gV)4gN{U-?7?69_HJzD6~oDRfONPA z7mkY?RmLJCRScne%puV?V|^#;L_yqdq4q<(25ka7&m=1mQ5Y3i?d&S&p_#>O& z2)@dx`PL$VT9U-JXs1It)uBEID?3c}TN7(UPBWU|DUD?5pz%{^_I->yYD>S0mSvf}@iS*h}jjuPGbMcH3| z$lrtm+=Z<5P8^$*SS}CeYK92jWt-c|501T;Jy^ps%}lo?mpeNX$y^~Up3jr7tiPnR zTRgFvcRgBFv|)ZhkVsF4bQ=eqF2$`X4hi^%Fs8X+=Yi^dqyn zWjh1?*uD4R!nIpCm8K*+ueGti}Lw3Fu(@3kc)Sv2({HvI-0 zsFEQYg@oq>8Y&Dc+{@x9^$Uj5B$6u|h940PV~rwfC+YrbC>;&yj-8Avaxb4NH2{eV zyx_ctWMn^0G^UdS?MCr9QzO{K#{m}7(BYtHY~V+qBl?h(Bi&=x?_r}1ROP#wP*IM8 z5HL=^@I?B>r?g{{EDsc?EMEkH+M!fEAYr5pnfeezZNfukTlDR=YsxVB?dfvldGF^_ z@OAj~J~O@V`+{atQ(Fut*L~^tW=;5Uk4~I$`&&D5!Uk6#NR7$(wl3~BjEgf`icfO9 z`sjI(QG_7{bJ7a;n^H|mQ#!kp^^rd+N+v@)i>9wl8&fJm{X z5IvE9JliW;cu~UF^)OP6Gm8xr^bxA{TrLjWGOK&&6>&tuGQ17lrV+^>oVq1krheTz zI@d;3_6lWz!MmM=xO>h=jY%17!86!v+vyoqOl4Xx^IPYt=%P2?>b~PxVEmB@34jSk z@_l&XC`Zmyh8K{zRrg6TInoz+Vru;m#ZPH5*i z^(j>p@7fgZ+f+q(XbnZ)-C~-K3lGAG{1InF_FYD(?6+KT1=r|L4B65!J|1#Tpn689 zkA~U$eaTj7C}#p;%6(y+(8_W%lhhkDAnnJphGt=7lIRPQ+oG0WU!G(T4&*GTiV!}} zBcbcvtFTxGg9NA6-D<;rA0GQ;LGB>Ug1qro>l-K-$2|V-nw*q8Y}Q?s5`*)bA3QjP z1W0!5hZ@koQ1rgF7R@(Ut26xM3D#C4mXL5t83h9^k4sz0t2rL=Xvl6;1?-gpG>iEF zCgkukL>1o?v88T5yQRW^~Yu82rN__BFK2#TlXCn54?5coLRcBkse}bjuv@Y{7PJ+8=co9le^eF8?&U>P#*+ zsNZ>)+3+9(yTY^W0Va<(8_5T|>M~f2&W(w$vhKB8EA^uv9Y9=O3i6N1DZ7Oq6=l)X zhFWc@d-JbWeGl0tISX4f`seQX~#aR_T}byA49 zR#>L-9j!%(SZ+M+lDU8Mjngss9pj}{BiUqr>t4Sv&$MR52>e4f=BgcYmc&v&87gOf z(>wS(DtC7Sp98McBb`lQE^~a;GpdAXWLz*_jQB5hQ#NnjdO2L$U!`ag*U8wM=Z0!Gzc~B zORxD5CkT?y*kO;pn`CjfJIn*T-n;guZu_ODpro=p9q z4(`uRrA>cQ5}0(8n)}L5(|TRu*JhDZqSY~?(dDOduB9HKH)MNm0@aNy=1dpM;+N;W z_ku)f2_P&(Hh6sCe zC$8JJq-T8fUPIJ!q`JkU*lIQflJja)ntp2MG4RT8H=0&r+qA<~@l86in$&5+hY`Uzy#~0z>3B7_jn$Pb7uU`rtt&YQ#b(H+K2>5X z+Wd3FzE?OlR^h6VoS;ti<3l&g&7f$;a5^TobGu`4yKta%OM`9Qm{#6#ain_x>(DVH zo?&;k_b*ut1wG(QF}e2_sO?7aw@d~B#Q^)CU(kgAm*-Bz3Z#+?}Y% zgD>_M_5BZ~8~T5k?*HrZpX4?s0RF#_aDY&eKN3u09S{Eh{PXrt!@t4qf0hj3{=;w- z1_1bvlEJ^P{x<;rf1UiZZ1A5~H>djd4Ve9lN@pUn-bQB!Wfd9^J{u%Gz?*Ey?ApM8?0=h&$ zJ`B_U&U^l^;Xj`$!haZAG5`P$o-P*lcK;t?Dat@V{{2W`{u=q906+@&-$(xsZae`n delta 14754 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 diff --git a/Calibre_Plugins/eReaderPDB2PML_plugin/__init__.py b/Calibre_Plugins/eReaderPDB2PML_plugin/__init__.py index b42cc1f..62562a5 100644 --- a/Calibre_Plugins/eReaderPDB2PML_plugin/__init__.py +++ b/Calibre_Plugins/eReaderPDB2PML_plugin/__init__.py @@ -1,9 +1,9 @@ #!/usr/bin/env python -# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +# -*- coding: utf-8 -*- # eReaderPDB2PML_plugin.py -# 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 +# # # All credit given to The Dark Reverser for the original standalone script. # I had the much easier job of converting it to Calibre a plugin. @@ -11,7 +11,7 @@ # This plugin is meant to convert secure Ereader files (PDB) to unsecured PMLZ files. # Calibre can then convert it to whatever format you desire. # It is meant to function without having to install any dependencies... -# other than having Calibre installed, of course. +# other than having Calibre installed, of course. # # Installation: # Go to Calibre's Preferences page... click on the Plugins button. Use the file @@ -36,6 +36,11 @@ # 0.0.5 - updated to the new calibre plugin interface # 0.0.6 - unknown changes # 0.0.7 - improved config dialog processing and fix possible output/unicode problem +# 0.0.8 - Proper fix for unicode problems, separate out erdr2pml from plugin + +PLUGIN_NAME = u"eReader PDB 2 PML" +PLUGIN_VERSION_TUPLE = (0, 0, 8) +PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE]) import sys, os @@ -43,113 +48,77 @@ from calibre.customize import FileTypePlugin from calibre.ptempfile import PersistentTemporaryDirectory from calibre.constants import iswindows, isosx +# 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) + + class eRdrDeDRM(FileTypePlugin): - name = 'eReader PDB 2 PML' # Name of the plugin - description = 'Removes DRM from secure pdb files. \ - Credit given to The Dark Reverser for the original standalone script.' + name = PLUGIN_NAME + description = u"Removes DRM from secure pdb files. Credit given to The Dark Reverser for the original standalone script." supported_platforms = ['linux', 'osx', 'windows'] # Platforms this plugin will run on - author = 'DiapDealer' # The author of this plugin - version = (0, 0, 7) # The version number of this plugin + author = u"DiapDealer, Apprentice Alf and The Dark Reverser" + version = PLUGIN_VERSION_TUPLE file_types = set(['pdb']) # The file types that this plugin will be applied to on_import = True # Run this plugin during the import minimum_calibre_version = (0, 7, 55) + priority = 100 def run(self, path_to_ebook): - from calibre_plugins.erdrpdb2pml import erdr2pml, outputfix - - if sys.stdout.encoding == None: - sys.stdout = outputfix.getwriter('utf-8')(sys.stdout) - else: - sys.stdout = outputfix.getwriter(sys.stdout.encoding)(sys.stdout) - if sys.stderr.encoding == None: - sys.stderr = outputfix.getwriter('utf-8')(sys.stderr) - else: - sys.stderr = outputfix.getwriter(sys.stderr.encoding)(sys.stderr) - - global bookname, erdr2pml - + + # make sure any unicode output gets converted safely with 'replace' + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) + + print u"{0} v{1}: Trying to decrypt {2}.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)) + infile = path_to_ebook bookname = os.path.splitext(os.path.basename(infile))[0] outdir = PersistentTemporaryDirectory() pmlzfile = self.temporary_file(bookname + '.pmlz') - + if self.site_customization: + from calibre_plugins.erdrpdb2pml import erdr2pml + keydata = self.site_customization ar = keydata.split(':') for i in ar: try: name, cc = i.split(',') - #remove spaces at start or end of name, and anywhere in CC - name = name.strip() - cc = cc.replace(" ","") + user_key = erdr2pml.getuser_key(name,cc) except ValueError: - print ' Error parsing user supplied data.' + print u"{0} v{1}: Error parsing user supplied data.".format(PLUGIN_NAME, PLUGIN_VERSION) return path_to_ebook - + try: - print "Processing..." + print u"{0} v{1}: Processing...".format(PLUGIN_NAME, PLUGIN_VERSION) import time start_time = time.time() - pmlfilepath = self.convertEreaderToPml(infile, name, cc, outdir) - - if pmlfilepath and pmlfilepath != 1: - import zipfile - print " Creating PMLZ file" - myZipFile = zipfile.ZipFile(pmlzfile.name,'w',zipfile.ZIP_STORED, False) - list = os.listdir(outdir) - for file in list: - localname = file - filePath = os.path.join(outdir,file) - if os.path.isfile(filePath): - myZipFile.write(filePath, localname) - elif os.path.isdir(filePath): - imageList = os.listdir(filePath) - localimgdir = os.path.basename(filePath) - for image in imageList: - localname = os.path.join(localimgdir,image) - imagePath = os.path.join(filePath,image) - if os.path.isfile(imagePath): - myZipFile.write(imagePath, localname) - myZipFile.close() - end_time = time.time() - search_time = end_time - start_time - print 'elapsed time: %.2f seconds' % (search_time, ) - print "done" + if erdr2pml.decryptBook(infile,pmlzfile.name,True,user_key) == 0: + print u"{0} v{1}: Elapsed time: {2:.2f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-start_time) return pmlzfile.name else: - raise ValueError('Error Creating PML file.') + raise ValueError(u"{0} v{1}: Error Creating PML file.".format(PLUGIN_NAME, PLUGIN_VERSION)) except ValueError, e: - print "Error: %s" % e + print u"{0} v{1}: Error: {2}".format(PLUGIN_NAME, PLUGIN_VERSION,e.args[0]) pass - raise Exception('Couldn\'t decrypt pdb file.') + raise Exception(u"{0} v{1}: Couldn\'t decrypt pdb file. See Apprentice Alf's blog for help.".format(PLUGIN_NAME, PLUGIN_VERSION)) else: - raise Exception('No name and CC# provided.') - - def convertEreaderToPml(self, infile, name, cc, outdir): + raise Exception(u"{0} v{1}: No name and CC# provided.".format(PLUGIN_NAME, PLUGIN_VERSION)) - print " Decoding File" - sect = erdr2pml.Sectionizer(infile, 'PNRdPPrs') - er = erdr2pml.EreaderProcessor(sect, name, cc) - if er.getNumImages() > 0: - print " Extracting images" - #imagedir = bookname + '_img/' - imagedir = 'images/' - imagedirpath = os.path.join(outdir,imagedir) - if not os.path.exists(imagedirpath): - os.makedirs(imagedirpath) - for i in xrange(er.getNumImages()): - name, contents = er.getImage(i) - file(os.path.join(imagedirpath, name), 'wb').write(contents) - - print " Extracting pml" - pml_string = er.getText() - pmlfilename = bookname + ".pml" - try: - file(os.path.join(outdir, pmlfilename),'wb').write(erdr2pml.cleanPML(pml_string)) - return os.path.join(outdir, pmlfilename) - except: - return 1 - def customization_help(self, gui=False): - return 'Enter Account Name & Last 8 digits of Credit Card number (separate with a comma)' + return u"Enter Account Name & Last 8 digits of Credit Card number (separate with a comma, multiple pairs with a colon)" diff --git a/Calibre_Plugins/eReaderPDB2PML_plugin/erdr2pml.py b/Calibre_Plugins/eReaderPDB2PML_plugin/erdr2pml.py index 7fefaf7..239c5ac 100644 --- a/Calibre_Plugins/eReaderPDB2PML_plugin/erdr2pml.py +++ b/Calibre_Plugins/eReaderPDB2PML_plugin/erdr2pml.py @@ -1,8 +1,11 @@ #!/usr/bin/env python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab -# +# -*- coding: utf-8 -*- + # erdr2pml.py +# Copyright © 2008 The Dark Reverser # +# Modified 2008–2012 by some_updates, DiapDealer and Apprentice Alf + # This is a python script. You need a Python interpreter to run it. # For example, ActiveState Python, which exists for windows. # Changelog @@ -16,7 +19,7 @@ # Custom version 0.03 - no change to eReader support, only usability changes # - start of pep-8 indentation (spaces not tab), fix trailing blanks # - version variable, only one place to change -# - added main routine, now callable as a library/module, +# - added main routine, now callable as a library/module, # means tools can add optional support for ereader2html # - outdir is no longer a mandatory parameter (defaults based on input name if missing) # - time taken output to stdout @@ -59,22 +62,14 @@ # 0.18 - on Windows try PyCrypto first and OpenSSL next # 0.19 - Modify the interface to allow use of import # 0.20 - modify to allow use inside new interface for calibre plugins -# 0.21 - Support eReader (drm) version 11. -# - Don't reject dictionary format. +# 0.21 - Support eReader (drm) version 11. +# - Don't reject dictionary format. # - Ignore sidebars for dictionaries (different format?) +# 0.22 - Unicode and plugin support, different image folders for PMLZ and source -__version__='0.21' +__version__='0.22' -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 +import sys, re import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile if 'calibre' in sys.modules: @@ -82,8 +77,66 @@ if 'calibre' in sys.modules: else: inCalibre = False +# 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"mobidedrm.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] + Des = None -if sys.platform.startswith('win'): +if iswindows: # first try with pycrypto if inCalibre: from calibre_plugins.erdrpdb2pml import pycrypto_des @@ -168,17 +221,30 @@ class Sectionizer(object): off = self.sections[section][0] return self.contents[off:end_off] -def sanitizeFileName(s): - r = '' - for c in s: - if c in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-": - r += c - return r +# cleanup unicode filenames +# borrowed from calibre from calibre/src/calibre/__init__.py +# added in removal of control (<32) chars +# and removal of . at start and end +# and with some (heavily edited) code from Paul Durrant's kindlenamer.py +def sanitizeFileName(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"\'") + # 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 + name = re.sub(ur"\s", u" ", name).strip() + # remove leading dots + while len(name)>0 and name[0] == u".": + name = name[1:] + # remove trailing dots (Windows doesn't like them) + if name.endswith(u'.'): + name = name[:-1] + return name def fixKey(key): def fixByte(b): return b ^ ((b ^ (b<<1) ^ (b<<2) ^ (b<<3) ^ (b<<4) ^ (b<<5) ^ (b<<6) ^ (b<<7) ^ 0x80) & 0x80) - return "".join([chr(fixByte(ord(a))) for a in key]) + return "".join([chr(fixByte(ord(a))) for a in key]) def deXOR(text, sp, table): r='' @@ -191,7 +257,7 @@ def deXOR(text, sp, table): return r class EreaderProcessor(object): - def __init__(self, sect, username, creditcard): + def __init__(self, sect, user_key): self.section_reader = sect.loadSection data = self.section_reader(0) version, = struct.unpack('>H', data[0:2]) @@ -212,18 +278,10 @@ class EreaderProcessor(object): for i in xrange(len(data)): j = (j + shuf) % len(data) r[j] = data[i] - assert len("".join(r)) == len(data) + assert len("".join(r)) == len(data) return "".join(r) r = unshuff(input[0:-8], cookie_shuf) - def fixUsername(s): - r = '' - for c in s.lower(): - if (c >= 'a' and c <= 'z' or c >= '0' and c <= '9'): - r += c - return r - - user_key = struct.pack('>LL', binascii.crc32(fixUsername(username)) & 0xffffffff, binascii.crc32(creditcard[-8:])& 0xffffffff) drm_sub_version = struct.unpack('>H', r[0:2])[0] self.num_text_pages = struct.unpack('>H', r[2:4])[0] - 1 self.num_image_pages = struct.unpack('>H', r[26:26+2])[0] @@ -302,7 +360,7 @@ class EreaderProcessor(object): sect = self.section_reader(self.first_image_page + i) name = sect[4:4+32].strip('\0') data = sect[62:] - return sanitizeFileName(name), data + return sanitizeFileName(unicode(name,'windows-1252')), data # def getChapterNamePMLOffsetData(self): @@ -314,7 +372,7 @@ class EreaderProcessor(object): # offname = deXOR(chaps, j, self.xortable) # offset = struct.unpack('>L', offname[0:4])[0] # name = offname[4:].strip('\0') - # cv += '%d|%s\n' % (offset, name) + # cv += '%d|%s\n' % (offset, name) # return cv # def getLinkNamePMLOffsetData(self): @@ -326,7 +384,7 @@ class EreaderProcessor(object): # offname = deXOR(links, j, self.xortable) # offset = struct.unpack('>L', offname[0:4])[0] # name = offname[4:].strip('\0') - # lv += '%d|%s\n' % (offset, name) + # lv += '%d|%s\n' % (offset, name) # return lv # def getExpandedTextSizesData(self): @@ -354,7 +412,7 @@ class EreaderProcessor(object): for i in xrange(self.num_text_pages): logging.debug('get page %d', i) r += zlib.decompress(des.decrypt(self.section_reader(1 + i))) - + # now handle footnotes pages if self.num_footnote_pages > 0: r += '\n' @@ -399,60 +457,53 @@ class EreaderProcessor(object): return r def cleanPML(pml): - # Convert special characters to proper PML code. High ASCII start at (\x80, \a128) and go up to (\xff, \a255) - pml2 = pml - for k in xrange(128,256): - badChar = chr(k) - pml2 = pml2.replace(badChar, '\\a%03d' % k) - return pml2 + # Convert special characters to proper PML code. High ASCII start at (\x80, \a128) and go up to (\xff, \a255) + pml2 = pml + for k in xrange(128,256): + badChar = chr(k) + pml2 = pml2.replace(badChar, '\\a%03d' % k) + return pml2 -def convertEreaderToPml(infile, name, cc, outdir): - if not os.path.exists(outdir): - os.makedirs(outdir) +def decryptBook(infile, outpath, make_pmlz, user_key): bookname = os.path.splitext(os.path.basename(infile))[0] - print " Decoding File" - sect = Sectionizer(infile, 'PNRdPPrs') - er = EreaderProcessor(sect, name, cc) - - if er.getNumImages() > 0: - print " Extracting images" - imagedir = bookname + '_img/' - imagedirpath = os.path.join(outdir,imagedir) - if not os.path.exists(imagedirpath): - os.makedirs(imagedirpath) - for i in xrange(er.getNumImages()): - name, contents = er.getImage(i) - file(os.path.join(imagedirpath, name), 'wb').write(contents) - - print " Extracting pml" - pml_string = er.getText() - pmlfilename = bookname + ".pml" - file(os.path.join(outdir, pmlfilename),'wb').write(cleanPML(pml_string)) - - # bkinfo = er.getBookInfo() - # if bkinfo != '': - # print " Extracting book meta information" - # file(os.path.join(outdir, 'bookinfo.txt'),'wb').write(bkinfo) - - - -def decryptBook(infile, outdir, name, cc, make_pmlz): - if make_pmlz : - # ignore specified outdir, use tempdir instead + if make_pmlz: + # outpath is actually pmlz name + pmlzname = outpath outdir = tempfile.mkdtemp() + imagedirpath = os.path.join(outdir,u"images") + else: + pmlzname = None + outdir = outpath + imagedirpath = os.path.join(outdir,bookname + u"_img") + try: - print "Processing..." - convertEreaderToPml(infile, name, cc, outdir) - if make_pmlz : + if not os.path.exists(outdir): + os.makedirs(outdir) + print u"Decoding File" + sect = Sectionizer(infile, 'PNRdPPrs') + er = EreaderProcessor(sect, user_key) + + if er.getNumImages() > 0: + print u"Extracting images" + if not os.path.exists(imagedirpath): + os.makedirs(imagedirpath) + for i in xrange(er.getNumImages()): + name, contents = er.getImage(i) + file(os.path.join(imagedirpath, name), 'wb').write(contents) + + print u"Extracting pml" + pml_string = er.getText() + pmlfilename = bookname + ".pml" + file(os.path.join(outdir, pmlfilename),'wb').write(cleanPML(pml_string)) + if pmlzname is not None: import zipfile import shutil - print " Creating PMLZ file" - zipname = infile[:-4] + '.pmlz' - myZipFile = zipfile.ZipFile(zipname,'w',zipfile.ZIP_STORED, False) + print u"Creating PMLZ file {0}".format(os.path.basename(pmlzname)) + myZipFile = zipfile.ZipFile(pmlzname,'w',zipfile.ZIP_STORED, False) list = os.listdir(outdir) - for file in list: - localname = file - filePath = os.path.join(outdir,file) + for filename in list: + localname = filename + filePath = os.path.join(outdir,filename) if os.path.isfile(filePath): myZipFile.write(filePath, localname) elif os.path.isdir(filePath): @@ -466,36 +517,46 @@ def decryptBook(infile, outdir, name, cc, make_pmlz): myZipFile.close() # remove temporary directory shutil.rmtree(outdir, True) - print 'output is %s' % zipname + print u"Output is {0}".format(pmlzname) else : - print 'output in %s' % outdir + print u"Output is in {0}".format(outdir) print "done" except ValueError, e: - print "Error: %s" % e + print u"Error: {0}".format(e.args[0]) return 1 return 0 def usage(): - print "Converts DRMed eReader books to PML Source" - print "Usage:" - print " erdr2pml [options] infile.pdb [outdir] \"your name\" credit_card_number " - print " " - print "Options: " - print " -h prints this message" - print " --make-pmlz create PMLZ instead of using output directory" - print " " - print "Note:" - print " if ommitted, outdir defaults based on 'infile.pdb'" - print " It's enough to enter the last 8 digits of the credit card number" + print u"Converts DRMed eReader books to PML Source" + print u"Usage:" + print u" erdr2pml [options] infile.pdb [outpath] \"your name\" credit_card_number" + print u" " + print u"Options: " + print u" -h prints this message" + print u" -p create PMLZ instead of source folder" + print u" --make-pmlz create PMLZ instead of source folder" + print u" " + print u"Note:" + print u" if outpath is ommitted, creates source in 'infile_Source' folder" + print u" if outpath is ommitted and pmlz option, creates PMLZ 'infile.pmlz'" + print u" if source folder created, images are in infile_img folder" + print u" if pmlz file created, images are in images folder" + print u" It's enough to enter the last 8 digits of the credit card number" return +def getuser_key(name,cc): + newname = "".join(c for c in name.lower() if c >= 'a' and c <= 'z' or c >= '0' and c <= '9') + cc = cc.replace(" ","") + return struct.pack('>LL', binascii.crc32(newname) & 0xffffffff,binascii.crc32(cc[-8:])& 0xffffffff) + +def cli_main(argv=unicode_argv()): + print u"eRdr2Pml v{0}. Copyright © 2009–2012 The Dark Reverser et al.".format(__version__) -def main(argv=None): try: - opts, args = getopt.getopt(sys.argv[1:], "h", ["make-pmlz"]) + opts, args = getopt.getopt(argv[1:], "hp", ["make-pmlz"]) except getopt.GetoptError, err: - print str(err) + print err.args[0] usage() return 1 make_pmlz = False @@ -503,25 +564,31 @@ def main(argv=None): if o == "-h": usage() return 0 + elif o == "-p": + make_pmlz = True elif o == "--make-pmlz": make_pmlz = True - - print "eRdr2Pml v%s. Copyright (c) 2009 The Dark Reverser" % __version__ if len(args)!=3 and len(args)!=4: usage() return 1 if len(args)==3: - infile, name, cc = args[0], args[1], args[2] - outdir = infile[:-4] + '_Source' + infile, name, cc = args + if make_pmlz: + outpath = os.path.splitext(infile)[0] + u".pmlz" + else: + outpath = os.path.splitext(infile)[0] + u"_Source" elif len(args)==4: - infile, outdir, name, cc = args[0], args[1], args[2], args[3] + infile, outpath, name, cc = args - return decryptBook(infile, outdir, name, cc, make_pmlz) + print getuser_key(name,cc).encode('hex') + + return decryptBook(infile, outpath, make_pmlz, getuser_key(name,cc)) if __name__ == "__main__": - sys.stdout=Unbuffered(sys.stdout) - sys.exit(main()) + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) + sys.exit(cli_main()) diff --git a/Calibre_Plugins/eReaderPDB2PML_plugin/openssl_des.py b/Calibre_Plugins/eReaderPDB2PML_plugin/openssl_des.py index 8a044fa..a4a40ca 100644 --- a/Calibre_Plugins/eReaderPDB2PML_plugin/openssl_des.py +++ b/Calibre_Plugins/eReaderPDB2PML_plugin/openssl_des.py @@ -18,7 +18,7 @@ def load_libcrypto(): return None libcrypto = CDLL(libcrypto) - + # typedef struct DES_ks # { # union @@ -30,7 +30,7 @@ def load_libcrypto(): # } ks[16]; # } DES_key_schedule; - # just create a big enough place to hold everything + # just create a big enough place to hold everything # it will have alignment of structure so we should be okay (16 byte aligned?) class DES_KEY_SCHEDULE(Structure): _fields_ = [('DES_cblock1', c_char * 16), @@ -61,7 +61,7 @@ def load_libcrypto(): DES_set_key = F(None, 'DES_set_key',[c_char_p, DES_KEY_SCHEDULE_p]) DES_ecb_encrypt = F(None, 'DES_ecb_encrypt',[c_char_p, c_char_p, DES_KEY_SCHEDULE_p, c_int]) - + class DES(object): def __init__(self, key): if len(key) != 8 : @@ -87,4 +87,3 @@ def load_libcrypto(): return ''.join(result) return DES - diff --git a/Calibre_Plugins/eReaderPDB2PML_plugin/outputfix.py b/Calibre_Plugins/eReaderPDB2PML_plugin/outputfix.py deleted file mode 100644 index 906c6e9..0000000 --- a/Calibre_Plugins/eReaderPDB2PML_plugin/outputfix.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Adapted and simplified from the kitchen project -# -# Kitchen Project Copyright (c) 2012 Red Hat, Inc. -# -# kitchen is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# kitchen is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with kitchen; if not, see -# -# Authors: -# Toshio Kuratomi -# Seth Vidal -# -# Portions of code taken from yum/i18n.py and -# python-fedora: fedora/textutils.py - -import codecs - -# returns a char string unchanged -# returns a unicode string converted to a char string of the passed encoding -# return the empty string for anything else -def getwriter(encoding): - class _StreamWriter(codecs.StreamWriter): - def __init__(self, stream): - codecs.StreamWriter.__init__(self, stream, 'replace') - - def encode(self, msg, errors='replace'): - if isinstance(msg, basestring): - if isinstance(msg, str): - return (msg, len(msg)) - return (msg.encode(self.encoding, 'replace'), len(msg)) - return ('',0) - - _StreamWriter.encoding = encoding - return _StreamWriter diff --git a/Calibre_Plugins/eReaderPDB2PML_plugin/pycrypto_des.py b/Calibre_Plugins/eReaderPDB2PML_plugin/pycrypto_des.py index 81502c8..80d7d65 100644 --- a/Calibre_Plugins/eReaderPDB2PML_plugin/pycrypto_des.py +++ b/Calibre_Plugins/eReaderPDB2PML_plugin/pycrypto_des.py @@ -28,4 +28,3 @@ def load_pycrypto(): i += 8 return ''.join(result) return DES - diff --git a/Calibre_Plugins/eReaderPDB2PML_plugin/python_des.py b/Calibre_Plugins/eReaderPDB2PML_plugin/python_des.py index cfb4f59..bd02904 100644 --- a/Calibre_Plugins/eReaderPDB2PML_plugin/python_des.py +++ b/Calibre_Plugins/eReaderPDB2PML_plugin/python_des.py @@ -2,8 +2,8 @@ # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab import sys -ECB = 0 -CBC = 1 +ECB = 0 +CBC = 1 class Des(object): __pc1 = [56, 48, 40, 32, 24, 16, 8, 0, 57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35, @@ -11,13 +11,13 @@ class Des(object): 13, 5, 60, 52, 44, 36, 28, 20, 12, 4, 27, 19, 11, 3] __left_rotations = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1] __pc2 = [13, 16, 10, 23, 0, 4,2, 27, 14, 5, 20, 9, - 22, 18, 11, 3, 25, 7, 15, 6, 26, 19, 12, 1, - 40, 51, 30, 36, 46, 54, 29, 39, 50, 44, 32, 47, - 43, 48, 38, 55, 33, 52, 45, 41, 49, 35, 28, 31] - __ip = [57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3, - 61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7, - 56, 48, 40, 32, 24, 16, 8, 0, 58, 50, 42, 34, 26, 18, 10, 2, - 60, 52, 44, 36, 28, 20, 12, 4, 62, 54, 46, 38, 30, 22, 14, 6] + 22, 18, 11, 3, 25, 7, 15, 6, 26, 19, 12, 1, + 40, 51, 30, 36, 46, 54, 29, 39, 50, 44, 32, 47, + 43, 48, 38, 55, 33, 52, 45, 41, 49, 35, 28, 31] + __ip = [57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3, + 61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7, + 56, 48, 40, 32, 24, 16, 8, 0, 58, 50, 42, 34, 26, 18, 10, 2, + 60, 52, 44, 36, 28, 20, 12, 4, 62, 54, 46, 38, 30, 22, 14, 6] __expansion_table = [31, 0, 1, 2, 3, 4, 3, 4, 5, 6, 7, 8, 7, 8, 9, 10, 11, 12,11, 12, 13, 14, 15, 16, 15, 16, 17, 18, 19, 20,19, 20, 21, 22, 23, 24, @@ -61,8 +61,8 @@ class Des(object): 35, 3, 43, 11, 51, 19, 59, 27,34, 2, 42, 10, 50, 18, 58, 26, 33, 1, 41, 9, 49, 17, 57, 25,32, 0, 40, 8, 48, 16, 56, 24] # Type of crypting being done - ENCRYPT = 0x00 - DECRYPT = 0x01 + ENCRYPT = 0x00 + DECRYPT = 0x01 def __init__(self, key, mode=ECB, IV=None): if len(key) != 8: raise ValueError("Invalid DES key size. Key must be exactly 8 bytes long.") @@ -74,7 +74,7 @@ class Des(object): self.setIV(IV) self.L = [] self.R = [] - self.Kn = [ [0] * 48 ] * 16 # 16 48-bit keys (K1 - K16) + self.Kn = [ [0] * 48 ] * 16 # 16 48-bit keys (K1 - K16) self.final = [] self.setKey(key) def getKey(self): diff --git a/Calibre_Plugins/ignobleepub_plugin.zip b/Calibre_Plugins/ignobleepub_plugin.zip index 10e26e4e0943a410aa4157ffda8f145abfbd13a6..58086686e46768f1e64df5e3c68f95ff2e68d797 100644 GIT binary patch literal 40545 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 literal 32027 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 p{hvwDf2;qSf&ZgMborN^D@cR?C8PYu +# Released under the terms of the GNU General Public Licence, version 3 +# # # Requires Calibre version 0.7.55 or higher. # -# All credit given to I ♥ Cabbages for the original standalone scripts. -# I had the much easier job of converting them to Calibre a plugin. +# All credit given to i♥cabbages for the original standalone scripts. +# I had the much easier job of converting them to a calibre plugin. # # This plugin is meant to decrypt Barnes & Noble Epubs that are protected # with a version of 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 +# 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. # # Configuration: @@ -38,227 +37,75 @@ __docformat__ = 'restructuredtext en' # 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.1 - added zipfix.py and included zipfilerugged.py from 0.1.8 # 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. """ Decrypt Barnes & Noble ADEPT encrypted EPUB books. """ -PLUGIN_NAME = 'Ignoble Epub DeDRM' -PLUGIN_VERSION_TUPLE = (0, 2, 4) +PLUGIN_NAME = u"Ignoble Epub DeDRM" +PLUGIN_VERSION_TUPLE = (0, 2, 5) 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' -import sys, os, zlib, re -from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED -from zipfile import ZipInfo as _ZipInfo -#from lxml import etree -try: - import xml.etree.cElementTree as etree -except ImportError: - import xml.etree.ElementTree as etree -from contextlib import closing - -global AES - -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#'} +import sys, os, re +import zipfile +from zipfile import ZipFile 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 sys.platform.startswith('win'): - libcrypto = find_library('libeay32') - else: - libcrypto = find_library('crypto') - if libcrypto is None: - raise IGNOBLEError('%s Plugin v%s: libcrypto not found' % (PLUGIN_NAME, PLUGIN_VERSION)) - 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_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): - self._blocksize = len(userkey) - if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : - raise IGNOBLEError('%s Plugin v%s: AES improper key used' % (PLUGIN_NAME, PLUGIN_VERSION)) - return - key = self._key = AES_KEY() - rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key) - if rv < 0: - raise IGNOBLEError('%s Plugin v%s: Failed to initialize AES key' % (PLUGIN_NAME, PLUGIN_VERSION)) - - 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('%s Plugin v%s: AES decryption failed' % (PLUGIN_NAME, PLUGIN_VERSION)) - return out.raw - - print '%s Plugin v%s: Using libcrypto.' %(PLUGIN_NAME, PLUGIN_VERSION) - 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) - - print '%s Plugin v%s: Using PyCrypto.' %(PLUGIN_NAME, PLUGIN_VERSION) - 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 - -class ZipInfo(_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) - path = path.encode('utf-8') - if path is not None: - 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 - -def plugin_main(userkey, inpath, outpath): - key = userkey.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 '%s Plugin: Not Encrypted.' % PLUGIN_NAME - return 1 - for name in META_NAMES: - namelist.remove(name) - try: # If the generated keyfile doesn't match the bookkey, this is where it's likely to blow up. - 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)) - 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: - return 2 - return 0 from calibre.customize import FileTypePlugin +from calibre.constants import iswindows, isosx from calibre.gui2 import is_ok_to_use_qt +# 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) + + class IgnobleDeDRM(FileTypePlugin): name = PLUGIN_NAME - description = 'Removes DRM from secure Barnes & Noble epub files. Credit given to I ♥ Cabbages for the original stand-alone scripts.' + description = u"Removes DRM from secure Barnes & Noble epub files. Credit given to i♥cabbages for the original stand-alone scripts." supported_platforms = ['linux', 'osx', 'windows'] - author = 'DiapDealer' + author = u"DiapDealer, Apprentice Alf and i♥cabbages" version = PLUGIN_VERSION_TUPLE minimum_calibre_version = (0, 7, 55) # Compiled python libraries cannot be imported in earlier versions. file_types = set(['epub']) on_import = True - - def run(self, path_to_ebook): - from calibre_plugins.ignoble_epub import outputfix - - if sys.stdout.encoding == None: - sys.stdout = outputfix.getwriter('utf-8')(sys.stdout) - else: - sys.stdout = outputfix.getwriter(sys.stdout.encoding)(sys.stdout) - if sys.stderr.encoding == None: - sys.stderr = outputfix.getwriter('utf-8')(sys.stderr) - else: - sys.stderr = outputfix.getwriter(sys.stderr.encoding)(sys.stderr) - - global AES + priority = 101 - print '\n\nRunning {0} v{1} on "{2}"'.format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)) - AES = _load_crypto() - if AES == None: - # Failed to load libcrypto or PyCrypto... Adobe Epubs can't be decrypted.' - raise Exception('%s Plugin v%s: Failed to load crypto libs.' % (PLUGIN_NAME, PLUGIN_VERSION)) + def run(self, path_to_ebook): + + # make sure any unicode output gets converted safely with 'replace' + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) + + print u"{0} v{1}: Trying to decrypt {2}.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)) # First time use or first time after upgrade to new key-handling/storage method # or no keys configured. Give a visual prompt to configure. - import calibre_plugins.ignoble_epub.config as cfg + import calibre_plugins.ignobleepub.config as cfg if not cfg.prefs['configured']: titlemsg = '%s v%s' % (PLUGIN_NAME, PLUGIN_VERSION) errmsg = titlemsg + ' not (properly) configured!\n' + \ @@ -275,56 +122,67 @@ class IgnobleDeDRM(FileTypePlugin): d.exec_() raise Exception('%s Plugin v%s: Plugin not configured.' % (PLUGIN_NAME, PLUGIN_VERSION)) + # Create a TemporaryPersistent file to work with. # Check original epub archive for zip errors. - from calibre_plugins.ignoble_epub import zipfix - inf = self.temporary_file('.epub') + from calibre_plugins.ignobleepub import zipfix + inf = self.temporary_file(u".epub") try: - print '%s Plugin: Verifying zip archive integrity.' % PLUGIN_NAME + print u"{0} v{1}: Verifying zip archive integrity.".format(PLUGIN_NAME, PLUGIN_VERSION) fr = zipfix.fixZip(path_to_ebook, inf.name) fr.fix() except Exception, e: - print '%s Plugin: unforeseen zip archive issue.' % PLUGIN_NAME + print u"{0} v{1}: Error \'{2}\' when checking zip archive.".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0]) raise Exception(e) - # Create a TemporaryPersistent file to work with. - of = self.temporary_file('.epub') - - # Attempt to decrypt epub with each encryption key (generated or provided). - key_counter = 1 - for keyname, userkey in cfg.prefs['keys'].items(): - keyname_masked = keyname[:4] + ''.join('x' for x in keyname[4:]) - # Give the user key, ebook and TemporaryPersistent file to the Stripper function. - result = plugin_main(userkey, inf.name, of.name) + return - # Ebook is not a B&N Adept epub... do nothing and pass it on. + #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 + + + # Attempt to decrypt epub with each encryption key (generated or provided). + for keyname, userkey in cfg.prefs['keys'].items(): + keyname_masked = u"".join((u'X' if (x.isdigit()) else x) for x in keyname) + print u"{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname_masked) + of = self.temporary_file(u".epub") + + # 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 == 1: - print '%s Plugin: Not a B&N Epub - doing nothing.\n' % PLUGIN_NAME + if result[0] == 1: + print u"{0} v{1}: {2}".format(PLUGIN_NAME, PLUGIN_VERSION, result[1]) of.close() return path_to_ebook break # Decryption was successful return the modified PersistentTemporary # file to Calibre's import process. - if result == 0: - print '{0} Plugin: Encryption key {1} ("{2}") correct!'.format(PLUGIN_NAME, key_counter, keyname_masked) + if result[0] == 0: + print u"{0} v{1}: Encryption successfully removed.".format(PLUGIN_NAME, PLUGIN_VERSION) of.close() return of.name break - print '{0} Plugin: Encryption key {1} ("{2}") incorrect!'.format(PLUGIN_NAME, key_counter, keyname_masked) - key_counter += 1 + print u"{0} v{1}: {2}".format(PLUGIN_NAME, PLUGIN_VERSION, result[1]) + of.close() + # Something went wrong with decryption. # Import the original unmolested epub. - of.close - raise Exception('%s Plugin v%s: Ultimately failed to decrypt.\n' % (PLUGIN_NAME, PLUGIN_VERSION)) + print(u"{0} v{1}: Ultimately failed to decrypt".format(PLUGIN_NAME, PLUGIN_VERSION)) + return path_to_ebook def is_customizable(self): # return true to allow customization via the Plugin->Preferences. return True def config_widget(self): - from calibre_plugins.ignoble_epub.config import ConfigWidget + from calibre_plugins.ignobleepub.config import ConfigWidget # Extract the helpfile contents from in the plugin's zipfile. # The helpfile must be named + '_Help.htm' return ConfigWidget(self.load_resources(RESOURCE_NAME)[RESOURCE_NAME]) diff --git a/Calibre_Plugins/ignobleepub_plugin/config.py b/Calibre_Plugins/ignobleepub_plugin/config.py index 35724a0..9fee73d 100644 --- a/Calibre_Plugins/ignobleepub_plugin/config.py +++ b/Calibre_Plugins/ignobleepub_plugin/config.py @@ -1,7 +1,8 @@ #!/usr/bin/env python -# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +# -*- coding: utf-8 -*- from __future__ import with_statement + __license__ = 'GPL v3' # Standard Python modules. @@ -19,11 +20,11 @@ from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url, from calibre.utils.config import dynamic, config_dir, JSONConfig # modules from this plugin's zipfile. -from calibre_plugins.ignoble_epub.__init__ import PLUGIN_NAME, PLUGIN_VERSION -from calibre_plugins.ignoble_epub.__init__ import RESOURCE_NAME as help_file_name -from calibre_plugins.ignoble_epub.utilities import (_load_crypto, normalize_name, - generate_keyfile, uStrCmp, DETAILED_MESSAGE, parseCustString) -from calibre_plugins.ignoble_epub.dialogs import AddKeyDialog, RenameKeyDialog +from calibre_plugins.ignobleepub.__init__ import PLUGIN_NAME, PLUGIN_VERSION +from calibre_plugins.ignobleepub.__init__ import RESOURCE_NAME as help_file_name +from calibre_plugins.ignobleepub.utilities import (uStrCmp, DETAILED_MESSAGE, parseCustString) +from calibre_plugins.ignobleepub.dialogs import AddKeyDialog, RenameKeyDialog +from calibre_plugins.ignobleepub.ignoblekeygen import generate_key JSON_NAME = PLUGIN_NAME.strip().lower().replace(' ', '_') JSON_PATH = 'plugins/' + JSON_NAME + '.json' @@ -40,7 +41,7 @@ prefs.defaults['configured'] = False class ConfigWidget(QWidget): def __init__(self, help_file_data): QWidget.__init__(self) - + self.help_file_data = help_file_data self.plugin_keys = prefs['keys'] @@ -88,7 +89,7 @@ class ConfigWidget(QWidget): val = sc.pop(PLUGIN_NAME, None) if val is not None: config['plugin_customization'] = sc - + # First time run since upgrading to new key storage method, or 0 keys configured. # Prompt to import pre-existing key files. if not prefs['configured']: @@ -102,7 +103,7 @@ class ConfigWidget(QWidget): # Start Qt Gui dialog layout layout = QVBoxLayout(self) self.setLayout(layout) - + help_layout = QHBoxLayout() layout.addLayout(help_layout) # Add hyperlink to a help file at the right. We will replace the correct name when it is clicked. @@ -111,12 +112,12 @@ class ConfigWidget(QWidget): help_label.setAlignment(Qt.AlignRight) help_label.linkActivated.connect(self.help_link_activated) help_layout.addWidget(help_label) - + keys_group_box = QGroupBox(_('Configured Ignoble Keys:'), self) layout.addWidget(keys_group_box) keys_group_box_layout = QHBoxLayout() keys_group_box.setLayout(keys_group_box_layout) - + self.listy = QListWidget(self) self.listy.setToolTip(_('

Stored Ignoble keys that will be used for decryption')) self.listy.setSelectionMode(QAbstractItemView.SingleSelection) @@ -130,7 +131,7 @@ class ConfigWidget(QWidget): self._add_key_button.setIcon(QIcon(I('plus.png'))) self._add_key_button.clicked.connect(self.add_key) button_layout.addWidget(self._add_key_button) - + self._delete_key_button = QtGui.QToolButton(self) self._delete_key_button.setToolTip(_('Delete highlighted key')) self._delete_key_button.setIcon(QIcon(I('list_remove.png'))) @@ -142,7 +143,7 @@ class ConfigWidget(QWidget): self._rename_key_button.setIcon(QIcon(I('edit-select-all.png'))) self._rename_key_button.clicked.connect(self.rename_key) button_layout.addWidget(self._rename_key_button) - + self.export_key_button = QtGui.QToolButton(self) self.export_key_button.setToolTip(_('Export highlighted key')) self.export_key_button.setIcon(QIcon(I('save.png'))) @@ -150,7 +151,7 @@ class ConfigWidget(QWidget): button_layout.addWidget(self.export_key_button) spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) button_layout.addItem(spacerItem) - + layout.addSpacing(20) migrate_layout = QHBoxLayout() layout.addLayout(migrate_layout) @@ -159,7 +160,7 @@ class ConfigWidget(QWidget): self.migrate_btn.clicked.connect(self.migrate_wrapper) migrate_layout.setAlignment(Qt.AlignLeft) migrate_layout.addWidget(self.migrate_btn) - + self.resize(self.sizeHint()) def populate_list(self): @@ -173,7 +174,7 @@ class ConfigWidget(QWidget): if d.result() != d.Accepted: # New key generation cancelled. return - self.plugin_keys[d.key_name] = generate_keyfile(d.user_name, d.cc_number) + self.plugin_keys[d.key_name] = generate_key(d.user_name, d.cc_number) self.listy.clear() self.populate_list() @@ -184,7 +185,7 @@ class ConfigWidget(QWidget): r = error_dialog(None, PLUGIN_NAME, _(errmsg), show=True, show_copy_button=False) return - + d = RenameKeyDialog(self) d.exec_() @@ -211,10 +212,10 @@ class ConfigWidget(QWidget): show_copy_button=False, default_yes=False): return del self.plugin_keys[keyname] - + self.listy.clear() self.populate_list() - + def help_link_activated(self, url): def get_help_file_resource(): # Copy the HTML helpfile to the plugin directory each time the @@ -225,7 +226,7 @@ class ConfigWidget(QWidget): return file_path url = 'file:///' + get_help_file_resource() open_url(QUrl(url)) - + def save_settings(self): prefs['keys'] = self.plugin_keys if prefs['keys']: @@ -301,4 +302,4 @@ class ConfigWidget(QWidget): if filename: fname = open(filename, 'w') fname.write(strdata) - fname.close() \ No newline at end of file + fname.close() diff --git a/Calibre_Plugins/ignobleepub_plugin/dialogs.py b/Calibre_Plugins/ignobleepub_plugin/dialogs.py index 687a46a..8a1c345 100644 --- a/Calibre_Plugins/ignobleepub_plugin/dialogs.py +++ b/Calibre_Plugins/ignobleepub_plugin/dialogs.py @@ -8,8 +8,8 @@ from PyQt4.Qt import (Qt, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, QGroupBox, QDialog, QDialogButtonBox) from calibre.gui2 import error_dialog -from calibre_plugins.ignoble_epub.__init__ import PLUGIN_NAME, PLUGIN_VERSION -from calibre_plugins.ignoble_epub.utilities import uStrCmp +from calibre_plugins.ignobleepub.__init__ import PLUGIN_NAME, PLUGIN_VERSION +from calibre_plugins.ignobleepub.utilities import uStrCmp class AddKeyDialog(QDialog): def __init__(self, parent=None,): @@ -23,7 +23,7 @@ class AddKeyDialog(QDialog): layout.addWidget(data_group_box) data_group_box_layout = QVBoxLayout() data_group_box.setLayout(data_group_box_layout) - + key_group = QHBoxLayout() data_group_box_layout.addLayout(key_group) key_group.addWidget(QLabel('Unique Key Name:', self)) @@ -50,7 +50,7 @@ class AddKeyDialog(QDialog): name_disclaimer_label = QLabel(_('Will not be stored/saved in configuration data:'), self) name_disclaimer_label.setAlignment(Qt.AlignHCenter) data_group_box_layout.addWidget(name_disclaimer_label) - + ccn_group = QHBoxLayout() data_group_box_layout.addLayout(ccn_group) ccn_group.addWidget(QLabel('Credit Card#:', self)) @@ -103,10 +103,10 @@ class AddKeyDialog(QDialog): @property def user_name(self): return unicode(self.name_ledit.text().toUtf8(), 'utf8').strip().lower().replace(' ','') - @property + @property def cc_number(self): return unicode(self.cc_ledit.text().toUtf8(), 'utf8').strip().replace(' ', '').replace('-','') - @property + @property def key_name(self): return unicode(self.key_ledit.text().toUtf8(), 'utf8') @@ -122,7 +122,7 @@ class RenameKeyDialog(QDialog): layout.addWidget(data_group_box) data_group_box_layout = QVBoxLayout() data_group_box.setLayout(data_group_box_layout) - + data_group_box_layout.addWidget(QLabel('Key Name:', self)) self.key_ledit = QLineEdit(self.parent.listy.currentItem().text(), self) self.key_ledit.setToolTip(_('

Enter a new name for this existing Ignoble key.')) @@ -155,6 +155,6 @@ class RenameKeyDialog(QDialog): _(errmsg), show=True, show_copy_button=False) QDialog.accept(self) - @property + @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8') \ No newline at end of file + return unicode(self.key_ledit.text().toUtf8(), 'utf8') diff --git a/Calibre_Plugins/ignobleepub_plugin/ignobleepub.py b/Calibre_Plugins/ignobleepub_plugin/ignobleepub.py new file mode 100644 index 0000000..2e0bd06 --- /dev/null +++ b/Calibre_Plugins/ignobleepub_plugin/ignobleepub.py @@ -0,0 +1,420 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from __future__ import with_statement + +# ignobleepub.pyw, version 3.6 +# 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 +# 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. + +# 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 + +""" +Decrypt Barnes & Noble encrypted ePub books. +""" + +__license__ = 'GPL v3' +__version__ = "3.6" + +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 + +# 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') + +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)] + 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] + + +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_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): + 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') + + 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 + + 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/Other_Tools/Barnes_and_Noble_EPUB_Tools/ignoblekeygen.pyw b/Calibre_Plugins/ignobleepub_plugin/ignoblekeygen.py similarity index 56% rename from Other_Tools/Barnes_and_Noble_EPUB_Tools/ignoblekeygen.pyw rename to Calibre_Plugins/ignobleepub_plugin/ignoblekeygen.py index e2c50e2..f25359c 100644 --- a/Other_Tools/Barnes_and_Noble_EPUB_Tools/ignoblekeygen.pyw +++ b/Calibre_Plugins/ignobleepub_plugin/ignoblekeygen.py @@ -1,13 +1,25 @@ -#! /usr/bin/python +#!/usr/bin/env python +# -*- coding: utf-8 -*- from __future__ import with_statement -# ignoblekeygen.pyw, version 2.4 +# ignoblekeygen.pyw, version 2.5 +# Copyright © 2009-2010 by i♥cabbages -# To run this program install Python 2.6 from -# and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto -# (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. +# 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 @@ -16,36 +28,92 @@ from __future__ import with_statement # 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] -# use openssl's libcrypt if it exists in place of pycrypto -# code extracted from the Adobe Adept DRM removal code also by I HeartCabbages 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 sys.platform.startswith('win'): + if iswindows: libcrypto = find_library('libeay32') else: libcrypto = find_library('crypto') + if libcrypto is None: - print 'libcrypto not found' raise IGNOBLEError('libcrypto not found') libcrypto = CDLL(libcrypto) @@ -70,6 +138,7 @@ def _load_crypto_libcrypto(): 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) @@ -88,7 +157,6 @@ def _load_crypto_libcrypto(): return AES - def _load_crypto_pycrypto(): from Crypto.Cipher import AES as _AES @@ -120,25 +188,28 @@ def normalize_name(name): return ''.join(x for x in name.lower() if x != ' ') -def generate_keyfile(name, ccn, outpath): +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() - with open(outpath, 'wb') as f: - f.write(userkey.encode('base64')) - return userkey + return userkey.encode('base64') -def cli_main(argv=sys.argv): +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 " \ @@ -146,10 +217,11 @@ def cli_main(argv=sys.argv): (progname,) return 1 if len(argv) != 4: - print "usage: %s NAME CC# OUTFILE" % (progname,) + print u"usage: {0} ".format(progname) return 1 - name, ccn, outpath = argv[1:] - generate_keyfile(name, ccn, outpath) + name, ccn, keypath = argv[1:] + userkey = generate_key(name, ccn) + open(keypath,'wb').write(userkey) return 0 @@ -162,38 +234,38 @@ def gui_main(): class DecryptionDialog(Tkinter.Frame): def __init__(self, root): Tkinter.Frame.__init__(self, root, border=5) - self.status = Tkinter.Label(self, text='Enter parameters') + 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='Account Name').grid(row=0) + 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='CC#').grid(row=1) + 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='Output file').grid(row=2) + 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, 'bnepubkey.b64') - button = Tkinter.Button(body, text="...", command=self.get_keypath) + 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="Generate", width=10, command=self.generate) + 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="Quit", width=10, command=self.quit) + buttons, text=u"Quit", width=10, command=self.quit) button.pack(side=Tkconstants.RIGHT) - + def get_keypath(self): keypath = tkFileDialog.asksaveasfilename( - parent=None, title='Select B&N EPUB key file to produce', - defaultextension='.b64', + parent=None, title=u"Select B&N ePub key file to produce", + defaultextension=u".b64", filetypes=[('base64-encoded files', '.b64'), ('All Files', '.*')]) if keypath: @@ -201,27 +273,28 @@ def gui_main(): 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'] = 'Name not specified' + self.status['text'] = u"Name not specified" return if not ccn: - self.status['text'] = 'Credit card number not specified' + self.status['text'] = u"Credit card number not specified" return if not keypath: - self.status['text'] = 'Output keyfile path not specified' + self.status['text'] = u"Output keyfile path not specified" return - self.status['text'] = 'Generating...' + self.status['text'] = u"Generating..." try: - generate_keyfile(name, ccn, keypath) + userkey = generate_key(name, ccn) except Exception, e: - self.status['text'] = 'Error: ' + str(e) + self.status['text'] = u"Error: (0}".format(e.args[0]) return - self.status['text'] = 'Keyfile successfully generated' + open(keypath,'wb').write(userkey) + self.status['text'] = u"Keyfile successfully generated" root = Tkinter.Tk() if AES is None: @@ -231,7 +304,7 @@ def gui_main(): "This script requires OpenSSL or PyCrypto, which must be installed " "separately. Read the top-of-script comment for details.") return 1 - root.title('Ignoble EPUB Keyfile Generator') + 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) @@ -240,5 +313,7 @@ def gui_main(): 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/outputfix.py b/Calibre_Plugins/ignobleepub_plugin/outputfix.py deleted file mode 100644 index 906c6e9..0000000 --- a/Calibre_Plugins/ignobleepub_plugin/outputfix.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Adapted and simplified from the kitchen project -# -# Kitchen Project Copyright (c) 2012 Red Hat, Inc. -# -# kitchen is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# kitchen is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with kitchen; if not, see -# -# Authors: -# Toshio Kuratomi -# Seth Vidal -# -# Portions of code taken from yum/i18n.py and -# python-fedora: fedora/textutils.py - -import codecs - -# returns a char string unchanged -# returns a unicode string converted to a char string of the passed encoding -# return the empty string for anything else -def getwriter(encoding): - class _StreamWriter(codecs.StreamWriter): - def __init__(self, stream): - codecs.StreamWriter.__init__(self, stream, 'replace') - - def encode(self, msg, errors='replace'): - if isinstance(msg, basestring): - if isinstance(msg, str): - return (msg, len(msg)) - return (msg.encode(self.encoding, 'replace'), len(msg)) - return ('',0) - - _StreamWriter.encoding = encoding - return _StreamWriter diff --git a/Calibre_Plugins/ignobleepub_plugin/plugin-import-name-ignoble_epub.txt b/Calibre_Plugins/ignobleepub_plugin/plugin-import-name-ignobleepub.txt similarity index 100% rename from Calibre_Plugins/ignobleepub_plugin/plugin-import-name-ignoble_epub.txt rename to Calibre_Plugins/ignobleepub_plugin/plugin-import-name-ignobleepub.txt diff --git a/Calibre_Plugins/ignobleepub_plugin/utilities.py b/Calibre_Plugins/ignobleepub_plugin/utilities.py index 13d6a5d..c730607 100644 --- a/Calibre_Plugins/ignobleepub_plugin/utilities.py +++ b/Calibre_Plugins/ignobleepub_plugin/utilities.py @@ -1,18 +1,10 @@ #!/usr/bin/env python -# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +# -*- coding: utf-8 -*- from __future__ import with_statement + __license__ = 'GPL v3' -import hashlib - -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 - -from calibre.constants import iswindows -from calibre_plugins.ignoble_epub.__init__ import PLUGIN_NAME, PLUGIN_VERSION - DETAILED_MESSAGE = \ 'You have personal information stored in this plugin\'s customization '+ \ 'string from a previous version of this plugin.\n\n'+ \ @@ -25,99 +17,6 @@ DETAILED_MESSAGE = \ 'this new version of the plugin will not be responsible for storing that personal '+ \ 'info in plain sight any longer.' -class IGNOBLEError(Exception): - pass - -def normalize_name(name): # Strip spaces and convert to lowercase. - return ''.join(x for x in name.lower() if x != ' ') - -# These are the key ENCRYPTING aes crypto functions -def generate_keyfile(name, ccn): - # Load the necessary crypto libs. - AES = _load_crypto() - name = normalize_name(name) + '\x00' - ccn = 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 _load_crypto_libcrypto(): - 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 iswindows: - cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto) - for loader in cryptolist: - try: - _aes = loader() - break - except (ImportError, IGNOBLEError): - pass - return _aes - def uStrCmp (s1, s2, caseless=False): import unicodedata as ud str1 = s1 if isinstance(s1, unicode) else unicode(s1) @@ -133,8 +32,8 @@ def parseCustString(keystuff): for i in ar: try: name, ccn = i.split(',') + # Generate Barnes & Noble EPUB user key from name and credit card number. + userkeys.append(generate_key(name, ccn)) except: - return False - # Generate Barnes & Noble EPUB user key from name and credit card number. - userkeys.append(generate_keyfile(name, ccn)) - return userkeys \ No newline at end of file + pass + return userkeys diff --git a/Calibre_Plugins/ignobleepub_plugin/zipfix.py b/Calibre_Plugins/ignobleepub_plugin/zipfix.py index c401b36..eaee20d 100644 --- a/Calibre_Plugins/ignobleepub_plugin/zipfix.py +++ b/Calibre_Plugins/ignobleepub_plugin/zipfix.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- import sys import zlib diff --git a/Calibre_Plugins/ineptepub_plugin.zip b/Calibre_Plugins/ineptepub_plugin.zip index 216505bf556327360897ca7b5bf822f07bfd6824..b1a002b26366eb2437e40a96c0cfbc5d053e928d 100644 GIT binary patch delta 18018 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 delta 15081 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 m|9x4}|AtW<$Y=k59s9p # # Requires Calibre version 0.7.55 or higher. # -# All credit given to I <3 Cabbages for the original standalone scripts. -# I had the much easier job of converting them to a Calibre plugin. +# All credit given to i♥cabbages for the original standalone scripts. +# I had the much easier job of converting them to a calibre plugin. # # This plugin is meant to decrypt Adobe Digital Edition 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 +# 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. # # Configuration: # When first run, the plugin will attempt to find your Adobe Digital Editions installation -# (on Windows and Mac OS's). If successful, it will create an 'adeptkey.der' file and -# save it in Calibre's configuration directory. It will use that file on subsequent runs. -# If there are already '*.der' files in the directory, the plugin won't attempt to -# find the ADE installation. So if you have ADE installed on the same machine as Calibre... -# you are ready to go. +# (on Windows and Mac OS's). If successful, it will create one or more +# 'calibre-adeptkey.der' files and save them in calibre's configuration directory. +# It will use those files on subsequent runs. If there is already a 'calibre-adeptkey*.der' +# file in the directory, the plugin won't attempt to find the ADE installation. +# So if you have ADE installed on the same machine as calibre you are ready to go. # -# If you already have keyfiles generated with I <3 Cabbages' ineptkey.pyw script, +# If you already have keyfiles generated with i♥cabbages' ineptkey.pyw script, # you can put those keyfiles in Calibre's configuration directory. The easiest # way to find the correct directory is to go to Calibre's Preferences page... click # on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre -# configuration directory' button. Paste your keyfiles in there. Just make sure that +# configuration directory' button. Copy your keyfiles in there. Just make sure that # they have different names and are saved with the '.der' extension (like the ineptkey # script produces). This directory isn't touched when upgrading Calibre, so it's quite # safe to leave them there. @@ -55,447 +57,157 @@ from __future__ import with_statement # 0.1.7 - update to new calibre plugin interface # 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. -""" -Decrypt Adobe ADEPT-encrypted EPUB books. -""" -PLUGIN_NAME = 'Inept Epub DeDRM' -PLUGIN_VERSION_TUPLE = (0, 1, 9) -PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE]) - -__license__ = 'GPL v3' - -import sys -import os -import zlib -import zipfile -import re -from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED -from contextlib import closing -import xml.etree.ElementTree as etree - -global AES -global RSA - -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#'} +PLUGIN_NAME = u"Inept Epub DeDRM" +PLUGIN_VERSION_TUPLE = (0, 2, 0) +PLUGIN_VERSION = u'.'.join([str(x) for x in PLUGIN_VERSION_TUPLE]) +import sys, os, re class ADEPTError(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 sys.platform.startswith('win'): - libcrypto = find_library('libeay32') - else: - libcrypto = find_library('crypto') - if libcrypto is None: - raise ADEPTError('%s Plugin v%s: libcrypto not found' % (PLUGIN_NAME, PLUGIN_VERSION)) - libcrypto = CDLL(libcrypto) - - RSA_NO_PADDING = 3 - AES_MAXNR = 14 - - c_char_pp = POINTER(c_char_p) - c_int_p = POINTER(c_int) - - class RSA(Structure): - pass - RSA_p = POINTER(RSA) - - 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 - - d2i_RSAPrivateKey = F(RSA_p, 'd2i_RSAPrivateKey', - [RSA_p, c_char_pp, c_long]) - RSA_size = F(c_int, 'RSA_size', [RSA_p]) - RSA_private_decrypt = F(c_int, 'RSA_private_decrypt', - [c_int, c_char_p, c_char_p, RSA_p, c_int]) - RSA_free = F(None, 'RSA_free', [RSA_p]) - 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 RSA(object): - def __init__(self, der): - buf = create_string_buffer(der) - pp = c_char_pp(cast(buf, c_char_p)) - rsa = self._rsa = d2i_RSAPrivateKey(None, pp, len(der)) - if rsa is None: - raise ADEPTError('Error parsing ADEPT user key DER') - - def decrypt(self, from_): - rsa = self._rsa - to = create_string_buffer(RSA_size(rsa)) - dlen = RSA_private_decrypt(len(from_), from_, to, rsa, - RSA_NO_PADDING) - if dlen < 0: - raise ADEPTError('RSA decryption failed') - return to[:dlen] - - def __del__(self): - if self._rsa is not None: - RSA_free(self._rsa) - self._rsa = None - - class AES(object): - def __init__(self, userkey): - self._blocksize = len(userkey) - if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : - raise ADEPTError('AES improper key used') - return - key = self._key = AES_KEY() - rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key) - if rv < 0: - raise ADEPTError('Failed to initialize AES key') - - 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 ADEPTError('AES decryption failed') - return out.raw - print 'IneptEpub: Using libcrypto.' - return (AES, RSA) - -def _load_crypto_pycrypto(): - from Crypto.Cipher import AES as _AES - from Crypto.PublicKey import RSA as _RSA - - # ASN.1 parsing code from tlslite - 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) - - 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) - - class RSA(object): - def __init__(self, der): - key = ASN1Parser([ord(x) for x in der]) - key = [key.getChild(x).value for x in xrange(1, 4)] - key = [self.bytesToNumber(v) for v in key] - self._rsa = _RSA.construct(key) - - def bytesToNumber(self, bytes): - total = 0L - for byte in bytes: - total = (total << 8) + byte - return total - - def decrypt(self, data): - return self._rsa.decrypt(data) - print 'IneptEpub: Using pycrypto.' - return (AES, RSA) - -def _load_crypto(): - _aes = _rsa = 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, _rsa = loader() - break - except (ImportError, ADEPTError): - pass - return (_aes, _rsa) - -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) - path = path.encode('utf-8') - if path is not None: - 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 - -def plugin_main(userkey, inpath, outpath): - rsa = RSA(userkey) - 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 - 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)) - bookkey = rsa.decrypt(bookkey.decode('base64')) - # Padded as per RSAES-PKCS1-v1_5 - if bookkey[-17] != '\x00': - raise ADEPTError('problem decrypting session key') - 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: - return 2 - return 0 - from calibre.customize import FileTypePlugin from calibre.constants import iswindows, isosx +# 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) + + class IneptDeDRM(FileTypePlugin): name = PLUGIN_NAME - description = 'Removes DRM from secure Adobe epub files. \ - Credit given to I <3 Cabbages for the original stand-alone scripts.' + description = u"Removes DRM from secure Adobe epub files. Credit given to i♥cabbages for the original stand-alone scripts." supported_platforms = ['linux', 'osx', 'windows'] - author = 'DiapDealer' + author = u"DiapDealer, Apprentice Alf and i♥cabbages" version = PLUGIN_VERSION_TUPLE minimum_calibre_version = (0, 7, 55) # Compiled python libraries cannot be imported in earlier versions. file_types = set(['epub']) on_import = True priority = 100 - - def run(self, path_to_ebook): - from calibre_plugins.ineptepub import outputfix - - if sys.stdout.encoding == None: - sys.stdout = outputfix.getwriter('utf-8')(sys.stdout) - else: - sys.stdout = outputfix.getwriter(sys.stdout.encoding)(sys.stdout) - if sys.stderr.encoding == None: - sys.stderr = outputfix.getwriter('utf-8')(sys.stderr) - else: - sys.stderr = outputfix.getwriter(sys.stderr.encoding)(sys.stderr) - global AES - global RSA - - AES, RSA = _load_crypto() - - if AES == None or RSA == None: - # Failed to load libcrypto or PyCrypto... Adobe Epubs can\'t be decrypted.' - raise ADEPTError('IneptEpub: Failed to load crypto libs... Adobe Epubs can\'t be decrypted.') + def run(self, path_to_ebook): + + # make sure any unicode output gets converted safely with 'replace' + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) + + print u"{0} v{1}: Trying to decrypt {2}.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)) + + # Create a TemporaryPersistent file to work with. + # Check original epub archive for zip errors. + from calibre_plugins.ineptepub import zipfix + inf = self.temporary_file(u".epub") + try: + print u"{0} v{1}: Verifying zip archive integrity.".format(PLUGIN_NAME, PLUGIN_VERSION) + 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) + 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 + # Load any keyfiles (*.der) included Calibre's config directory. userkeys = [] - # Find Calibre's configuration directory. + # self.plugin_path is passed in unicode because we defined our name in unicode confpath = os.path.split(os.path.split(self.plugin_path)[0])[0] - print 'IneptEpub: Calibre configuration directory = %s' % confpath + print u"{0} v{1}: Calibre configuration directory = {2}".format(PLUGIN_NAME, PLUGIN_VERSION, confpath) files = os.listdir(confpath) - filefilter = re.compile("\.der$", re.IGNORECASE) + filefilter = re.compile(u"\.der$", re.IGNORECASE) files = filter(filefilter.search, files) foundDefault = False - if files: try: for filename in files: - if filename[:16] == 'calibre-adeptkey': + if filename[:16] == u"calibre-adeptkey": foundDefault = True fpath = os.path.join(confpath, filename) with open(fpath, 'rb') as f: - userkeys.append(f.read()) - print 'IneptEpub: Keyfile %s found in config folder.' % filename + userkeys.append([f.read(), filename]) + print u"{0} v{1}: Keyfile {2} found in config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, filename) except IOError: - print 'IneptEpub: Error reading keyfiles from config directory.' + print u"{0} v{1}: Error reading keyfiles from config directory.".format(PLUGIN_NAME, PLUGIN_VERSION) pass - + if not foundDefault: # Try to find key from ADE install and save the key in # Calibre's configuration directory for future use. if iswindows or isosx: + #ignore annoying future warning from key generation + import warnings + warnings.filterwarnings('ignore', category=FutureWarning) + # ADE key retrieval script included in respective OS folder. from calibre_plugins.ineptepub.ineptkey import retrieve_keys try: keys = retrieve_keys() for i,key in enumerate(keys): - userkeys.append(key) - keypath = os.path.join(confpath, 'calibre-adeptkey{0:d}.der'.format(i)) + keyname = u"calibre-adeptkey{0:d}.der".format(i) + userkeys.append([key,keyname]) + keypath = os.path.join(confpath, keyname) open(keypath, 'wb').write(key) - print 'IneptEpub: Created keyfile %s from ADE install.' % keypath + print u"{0} v{1}: Created keyfile {2} from ADE install.".format(PLUGIN_NAME, PLUGIN_VERSION, keyname) except: - print 'IneptEpub: Couldn\'t Retrieve key from ADE install.' + print u"{0} v{1}: Couldn\'t Retrieve key from ADE install.".format(PLUGIN_NAME, PLUGIN_VERSION) pass if not userkeys: # No user keys found... bail out. - raise ADEPTError('IneptEpub - No keys found. Check keyfile(s)/ADE install') + raise ADEPTError(u"{0} v{1}: No keys found. Check keyfile(s)/ADE install".format(PLUGIN_NAME, PLUGIN_VERSION)) return - + # Attempt to decrypt epub with each encryption key found. - for userkey in userkeys: - # Create a TemporaryPersistent file to work with. - # Check original epub archive for zip errors. - from calibre_plugins.ineptepub import zipfix - inf = self.temporary_file('.epub') - try: - print '%s Plugin: Verifying zip archive integrity.' % PLUGIN_NAME - fr = zipfix.fixZip(path_to_ebook, inf.name) - fr.fix() - except Exception, e: - print '%s Plugin: unforeseen zip archive issue.' % PLUGIN_NAME - raise Exception(e) - return - of = self.temporary_file('.epub') - - # Give the user key, ebook and TemporaryPersistent file to the plugin_main function. - result = plugin_main(userkey, inf.name, of.name) - + for userkeyinfo in userkeys: + print u"{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, userkeyinfo[1]) + 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 'IneptEpub: Not an Adobe Adept Epub... punting.' + 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 - + # Decryption was successful return the modified PersistentTemporary # file to Calibre's import process. if result == 0: - print 'IneptEpub: Encryption successfully removed.' - of.close + print u"{0} v{1}: Encryption successfully removed.".format(PLUGIN_NAME, PLUGIN_VERSION) + of.close() return of.name break - - print 'IneptEpub: Encryption key invalid... trying others.' - of.close() - + + 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. - of.close - raise ADEPTError('IneptEpub - Ultimately failed to decrypt') + raise ADEPTError(u"{0} v{1}: Ultimately failed to decrypt".format(PLUGIN_NAME, PLUGIN_VERSION)) return diff --git a/Other_Tools/Adobe_ePub_Tools/ineptepub.pyw b/Calibre_Plugins/ineptepub_plugin/ineptepub.py similarity index 53% rename from Other_Tools/Adobe_ePub_Tools/ineptepub.pyw rename to Calibre_Plugins/ineptepub_plugin/ineptepub.py index 829f1b2..4b5a296 100644 --- a/Other_Tools/Adobe_ePub_Tools/ineptepub.pyw +++ b/Calibre_Plugins/ineptepub_plugin/ineptepub.py @@ -3,11 +3,13 @@ from __future__ import with_statement -# ineptepub.pyw, version 5.7 -# Copyright © 2009-2010 i♥cabbages +# ineptepub.pyw, version 5.8 +# Copyright © 2009-2010 by i♥cabbages -# 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 +# + +# 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 @@ -31,24 +33,83 @@ from __future__ import with_statement # 5.5 - On Windows try PyCrypto first, OpenSSL next # 5.6 - Modify interface to allow use with import # 5.7 - Fix for potential problem with PyCrypto +# 5.8 - Revised to allow use in calibre plugins to eliminate need for duplicate code """ -Decrypt Adobe ADEPT-encrypted EPUB books. +Decrypt Adobe Digital Editions encrypted ePub books. """ __license__ = 'GPL v3' +__version__ = "5.8" 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 -import Tkinter -import Tkconstants -import tkFileDialog -import tkMessageBox + +# 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') + +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)] + 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] + class ADEPTError(Exception): pass @@ -58,7 +119,7 @@ def _load_crypto_libcrypto(): Structure, c_ulong, create_string_buffer, cast from ctypes.util import find_library - if sys.platform.startswith('win'): + if iswindows: libcrypto = find_library('libeay32') else: libcrypto = find_library('crypto') @@ -272,6 +333,7 @@ def _load_crypto(): except (ImportError, ADEPTError): pass return (AES, RSA) + AES, RSA = _load_crypto() META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml') @@ -314,158 +376,181 @@ class Decryptor(object): data = self.decompress(data) return data - -class DecryptionDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - self.status = Tkinter.Label(self, text='Select files for decryption') - self.status.pack(fill=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='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) - 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(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-encrypted EPUB file to decrypt', - defaultextension='.epub', filetypes=[('EPUB files', '.epub'), - ('All files', '.*')]) - 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='Select unencrypted EPUB file to produce', - defaultextension='.epub', filetypes=[('EPUB files', '.epub'), - ('All files', '.*')]) - 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'] = 'Specified key file does not exist' - return - if not inpath or not os.path.exists(inpath): - self.status['text'] = 'Specified input file does not exist' - return - if not outpath: - self.status['text'] = 'Output file not specified' - return - if inpath == outpath: - self.status['text'] = 'Must have different input and output files' - return - argv = [sys.argv[0], keypath, inpath, outpath] - self.status['text'] = 'Decrypting...' - try: - cli_main(argv) - except Exception, e: - self.status['text'] = 'Error: ' + str(e) - return - self.status['text'] = 'File successfully decrypted' - - -def decryptBook(keypath, inpath, outpath): - with open(keypath, 'rb') as f: - keyder = f.read() - rsa = RSA(keyder) +# check file to make check whether it's probably an Adobe Adept encrypted ePub +def adeptBook(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: - raise ADEPTError('%s: not an ADEPT EPUB' % (inpath,)) + 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) == 172: + return True + except: + # if we couldn't check, assume it is + return True + return False + +def decryptBook(userkey, inpath, outpath): + if AES is None: + raise ADEPTError(u"PyCrypto or OpenSSL must be installed.") + rsa = RSA(userkey) + 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) - 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)) - bookkey = rsa.decrypt(bookkey.decode('base64')) - # Padded as per RSAES-PKCS1-v1_5 - if bookkey[-17] != '\x00': - raise ADEPTError('problem decrypting session key') - 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)) + 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) != 172: + print u"{0:s} is not a secure Adobe Adept ePub.".format(os.path.basename(inpath)) + return 1 + bookkey = rsa.decrypt(bookkey.decode('base64')) + # Padded as per RSAES-PKCS1-v1_5 + if bookkey[-17] != '\x00': + print u"Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath)) + return 2 + 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=sys.argv): +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 "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,) + print u"usage: {0} ".format(progname) return 1 keypath, inpath, outpath = argv[1:] - return decryptBook(keypath, inpath, outpath) - + 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 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"adeptkey.der"): + self.keypath.insert(0, u"adeptkey.der") + 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 Adobe Adept \'.der\' key file", + defaultextension=u".der", + filetypes=[('Adobe Adept DER-encoded files', '.der'), + ('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 ADEPT-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) + return + 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( - "INEPT EPUB Decrypter", - "This script requires OpenSSL or PyCrypto, which must be" - " installed separately. Read the top-of-script comment for" - " details.") - return 1 - root.title('INEPT EPUB Decrypter') + root.title(u"Adobe Adept ePub Decrypter v.{0}".format(__version__)) root.resizable(True, False) root.minsize(300, 0) DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1) @@ -474,5 +559,7 @@ def gui_main(): 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/ineptepub_plugin/ineptkey.py b/Calibre_Plugins/ineptepub_plugin/ineptkey.py index 723b7c6..a9bc62d 100644 --- a/Calibre_Plugins/ineptepub_plugin/ineptkey.py +++ b/Calibre_Plugins/ineptepub_plugin/ineptkey.py @@ -6,8 +6,8 @@ from __future__ import with_statement # ineptkey.pyw, version 5.6 # Copyright © 2009-2010 i♥cabbages -# 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 +# # Windows users: Before running this program, you must first install Python 2.6 # from and PyCrypto from @@ -37,7 +37,7 @@ from __future__ import with_statement # 5.3 - On Windows try PyCrypto first, OpenSSL next # 5.4 - Modify interface to allow use of import # 5.5 - Fix for potential problem with PyCrypto -# 5.6 - Revise to allow use in Plugins to eliminate need for duplicate code +# 5.6 - Revised to allow use in Plugins to eliminate need for duplicate code """ Retrieve Adobe ADEPT user key. @@ -49,12 +49,65 @@ import sys import os import struct +# 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') +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)] + return [u"ineptkey.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 ADEPTError(Exception): pass @@ -80,13 +133,13 @@ if iswindows: _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_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', @@ -308,9 +361,9 @@ if iswindows: cuser = winreg.HKEY_CURRENT_USER try: regkey = winreg.OpenKey(cuser, DEVICE_KEY_PATH) + device = winreg.QueryValueEx(regkey, 'key')[0] except WindowsError: raise ADEPTError("Adobe Digital Editions not activated") - device = winreg.QueryValueEx(regkey, 'key')[0] keykey = CryptUnprotectData(device, entropy) userkey = None keys = [] @@ -343,7 +396,7 @@ if iswindows: if len(keys) == 0: raise ADEPTError('Could not locate privateLicenseKey') return keys - + elif isosx: import xml.etree.ElementTree as etree @@ -386,7 +439,7 @@ else: def retrieve_keys(keypath): raise ADEPTError("This script only supports Windows and Mac OS X.") return [] - + def retrieve_key(keypath): keys = retrieve_keys() with open(keypath, 'wb') as f: @@ -397,22 +450,22 @@ def extractKeyfile(keypath): try: success = retrieve_key(keypath) except ADEPTError, e: - print "Key generation Error: " + str(e) + print u"Key generation Error: {0}".format(e.args[0]) return 1 except Exception, e: - print "General Error: " + str(e) + print "General Error: {0}".format(e.args[0]) return 1 if not success: return 1 return 0 -def cli_main(argv=sys.argv): +def cli_main(argv=unicode_argv()): keypath = argv[1] return extractKeyfile(keypath) -def main(argv=sys.argv): +def gui_main(argv=unicode_argv()): import Tkinter import Tkconstants import tkMessageBox @@ -421,24 +474,24 @@ def main(argv=sys.argv): class ExceptionDialog(Tkinter.Frame): def __init__(self, root, text): Tkinter.Frame.__init__(self, root, border=5) - label = Tkinter.Label(self, text="Unexpected error:", + label = Tkinter.Label(self, text=u"Unexpected error:", anchor=Tkconstants.W, justify=Tkconstants.LEFT) label.pack(fill=Tkconstants.X, expand=0) self.text = Tkinter.Text(self) self.text.pack(fill=Tkconstants.BOTH, expand=1) - + self.text.insert(Tkconstants.END, text) root = Tkinter.Tk() root.withdraw() - progname = os.path.basename(argv[0]) - keypath = os.path.abspath("adeptkey.der") + keypath, progname = os.path.split(argv[0]) + keypath = os.path.join(keypath, u"adeptkey.der") success = False try: success = retrieve_key(keypath) except ADEPTError, e: - tkMessageBox.showerror("ADEPT Key", "Error: " + str(e)) + tkMessageBox.showerror(u"ADEPT Key", "Error: {0}".format(e.args[0])) except Exception: root.wm_state('normal') root.title('ADEPT Key') @@ -448,10 +501,12 @@ def main(argv=sys.argv): if not success: return 1 tkMessageBox.showinfo( - "ADEPT Key", "Key successfully retrieved to %s" % (keypath)) + u"ADEPT Key", u"Key successfully retrieved to {0}".format(keypath)) 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(main()) + sys.exit(gui_main()) diff --git a/Calibre_Plugins/ineptepub_plugin/outputfix.py b/Calibre_Plugins/ineptepub_plugin/outputfix.py deleted file mode 100644 index 906c6e9..0000000 --- a/Calibre_Plugins/ineptepub_plugin/outputfix.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Adapted and simplified from the kitchen project -# -# Kitchen Project Copyright (c) 2012 Red Hat, Inc. -# -# kitchen is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# kitchen is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with kitchen; if not, see -# -# Authors: -# Toshio Kuratomi -# Seth Vidal -# -# Portions of code taken from yum/i18n.py and -# python-fedora: fedora/textutils.py - -import codecs - -# returns a char string unchanged -# returns a unicode string converted to a char string of the passed encoding -# return the empty string for anything else -def getwriter(encoding): - class _StreamWriter(codecs.StreamWriter): - def __init__(self, stream): - codecs.StreamWriter.__init__(self, stream, 'replace') - - def encode(self, msg, errors='replace'): - if isinstance(msg, basestring): - if isinstance(msg, str): - return (msg, len(msg)) - return (msg.encode(self.encoding, 'replace'), len(msg)) - return ('',0) - - _StreamWriter.encoding = encoding - return _StreamWriter diff --git a/Calibre_Plugins/ineptepub_plugin/zipfix.py b/Calibre_Plugins/ineptepub_plugin/zipfix.py index c401b36..eaee20d 100644 --- a/Calibre_Plugins/ineptepub_plugin/zipfix.py +++ b/Calibre_Plugins/ineptepub_plugin/zipfix.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- import sys import zlib diff --git a/Calibre_Plugins/ineptpdf_plugin.zip b/Calibre_Plugins/ineptpdf_plugin.zip index e63dcecbed55787161f1b098d09ee687076008d3..5ad55e37fc6cc8ada0fd32a6a3462f973db18c04 100644 GIT binary patch literal 29664 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

9_rPk^!xpHt%6Kdx!fVF0lt||%+&hi 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 L1O)5+*Vg|6BW&Hi literal 26199 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 T&;bB2Z$~#M0N`Wz>)U?-D8^CC diff --git a/Calibre_Plugins/ineptpdf_plugin/__init__.py b/Calibre_Plugins/ineptpdf_plugin/__init__.py index b847e96..2901f1a 100644 --- a/Calibre_Plugins/ineptpdf_plugin/__init__.py +++ b/Calibre_Plugins/ineptpdf_plugin/__init__.py @@ -1,10 +1,11 @@ -#! /usr/bin/env python -# ineptpdf plugin __init__.py, version 0.1.5 +#!/usr/bin/env python +# -*- coding: utf-8 -*- from __future__ import with_statement +__license__ = 'GPL v3' -# 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 +# # PLEASE DO NOT PIRATE EBOOKS! @@ -15,7 +16,7 @@ from __future__ import with_statement # Requires Calibre version 0.7.55 or higher. # -# All credit given to I <3 Cabbages for the original standalone scripts. +# All credit given to i♥cabbages for the original standalone scripts. # I had the much easier job of converting them to a Calibre plugin. # # This plugin is meant to decrypt Adobe Digital Edition PDFs that are protected @@ -25,13 +26,13 @@ from __future__ import with_statement # # Configuration: # When first run, the plugin will attempt to find your Adobe Digital Editions installation -# (on Windows and Mac OS's). If successful, it will create a 'calibre-adeptkey.der' file and -# save it in Calibre's configuration directory. It will use that file on subsequent runs. -# If there are already '*.der' files in the directory, the plugin won't attempt to -# find the ADE installation. So if you have ADE installed on the same machine as Calibre... -# you are ready to go. +# (on Windows and Mac OS's). If successful, it will create one or more +# 'calibre-adeptkey.der' files and save them in calibre's configuration directory. +# It will use those files on subsequent runs. If there is already a 'calibre-adeptkey*.der' +# file in the directory, the plugin won't attempt to find the ADE installation. +# So if you have ADE installed on the same machine as calibre you are ready to go. # -# If you already have keyfiles generated with I <3 Cabbages' ineptkey.pyw script, +# If you already have keyfiles generated with i♥cabbages' ineptkey.pyw script, # you can put those keyfiles in Calibre's configuration directory. The easiest # way to find the correct directory is to go to Calibre's Preferences page... click # on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre @@ -58,2186 +59,137 @@ from __future__ import with_statement # 0.1.6 - Fix for potential problem with PyCrypto # 0.1.7 - Fix for potential problem with ADE keys and fix possible output/unicode problem # 0.1.8 - Fix for code copying error +# 0.1.9 - Major code change to use unaltered ineptepub.py """ Decrypts Adobe ADEPT-encrypted PDF files. """ -__license__ = 'GPL v3' +PLUGIN_NAME = u"Inept PDF DeDRM" +PLUGIN_VERSION_TUPLE = (0, 1, 9) +PLUGIN_VERSION = u'.'.join([str(x) for x in PLUGIN_VERSION_TUPLE]) import sys import os import re -import zlib -import struct -import hashlib -from itertools import chain, islice -import xml.etree.ElementTree as etree - -global ARC4, RSA, AES class ADEPTError(Exception): pass - -import hashlib - -def SHA256(message): - ctx = hashlib.sha256() - ctx.update(message) - return ctx.digest() - - -def _load_crypto_libcrypto(): - from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \ - Structure, c_ulong, create_string_buffer, cast - from ctypes.util import find_library - - if sys.platform.startswith('win'): - libcrypto = find_library('libeay32') - else: - libcrypto = find_library('crypto') - - if libcrypto is None: - raise ADEPTError('libcrypto not found') - libcrypto = CDLL(libcrypto) - - AES_MAXNR = 14 - - RSA_NO_PADDING = 3 - - c_char_pp = POINTER(c_char_p) - c_int_p = POINTER(c_int) - - class AES_KEY(Structure): - _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)] - AES_KEY_p = POINTER(AES_KEY) - - class RC4_KEY(Structure): - _fields_ = [('x', c_int), ('y', c_int), ('box', c_int * 256)] - RC4_KEY_p = POINTER(RC4_KEY) - - class RSA(Structure): - pass - RSA_p = POINTER(RSA) - - def F(restype, name, argtypes): - func = getattr(libcrypto, name) - func.restype = restype - func.argtypes = argtypes - return func - - AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,c_int]) - AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',[c_char_p, c_int, AES_KEY_p]) - - RC4_set_key = F(None,'RC4_set_key',[RC4_KEY_p, c_int, c_char_p]) - RC4_crypt = F(None,'RC4',[RC4_KEY_p, c_int, c_char_p, c_char_p]) - - d2i_RSAPrivateKey = F(RSA_p, 'd2i_RSAPrivateKey', - [RSA_p, c_char_pp, c_long]) - RSA_size = F(c_int, 'RSA_size', [RSA_p]) - RSA_private_decrypt = F(c_int, 'RSA_private_decrypt', - [c_int, c_char_p, c_char_p, RSA_p, c_int]) - RSA_free = F(None, 'RSA_free', [RSA_p]) - - class RSA(object): - def __init__(self, der): - buf = create_string_buffer(der) - pp = c_char_pp(cast(buf, c_char_p)) - rsa = self._rsa = d2i_RSAPrivateKey(None, pp, len(der)) - if rsa is None: - raise ADEPTError('Error parsing ADEPT user key DER') - - def decrypt(self, from_): - rsa = self._rsa - to = create_string_buffer(RSA_size(rsa)) - dlen = RSA_private_decrypt(len(from_), from_, to, rsa, - RSA_NO_PADDING) - if dlen < 0: - raise ADEPTError('RSA decryption failed') - return to[1:dlen] - - def __del__(self): - if self._rsa is not None: - RSA_free(self._rsa) - self._rsa = None - - class ARC4(object): - @classmethod - def new(cls, userkey): - self = ARC4() - self._blocksize = len(userkey) - key = self._key = RC4_KEY() - RC4_set_key(key, self._blocksize, userkey) - return self - def __init__(self): - self._blocksize = 0 - self._key = None - def decrypt(self, data): - out = create_string_buffer(len(data)) - RC4_crypt(self._key, len(data), data, out) - return out.raw - - class AES(object): - MODE_CBC = 0 - @classmethod - def new(cls, userkey, mode, iv): - self = AES() - self._blocksize = len(userkey) - # mode is ignored since CBCMODE is only thing supported/used so far - self._mode = mode - if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : - raise ADEPTError('AES improper key used') - return - keyctx = self._keyctx = AES_KEY() - self._iv = iv - rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx) - if rv < 0: - raise ADEPTError('Failed to initialize AES key') - return self - def __init__(self): - self._blocksize = 0 - self._keyctx = None - self._iv = 0 - self._mode = 0 - def decrypt(self, data): - out = create_string_buffer(len(data)) - rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self._iv, 0) - if rv == 0: - raise ADEPTError('AES decryption failed') - return out.raw - - return (ARC4, RSA, AES) - - -def _load_crypto_pycrypto(): - from Crypto.PublicKey import RSA as _RSA - from Crypto.Cipher import ARC4 as _ARC4 - from Crypto.Cipher import AES as _AES - - # ASN.1 parsing code from tlslite - 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) - - class ARC4(object): - @classmethod - def new(cls, userkey): - self = ARC4() - self._arc4 = _ARC4.new(userkey) - return self - def __init__(self): - self._arc4 = None - def decrypt(self, data): - return self._arc4.decrypt(data) - - class AES(object): - MODE_CBC = _AES.MODE_CBC - @classmethod - def new(cls, userkey, mode, iv): - self = AES() - self._aes = _AES.new(userkey, mode, iv) - return self - def __init__(self): - self._aes = None - def decrypt(self, data): - return self._aes.decrypt(data) - - class RSA(object): - def __init__(self, der): - key = ASN1Parser([ord(x) for x in der]) - key = [key.getChild(x).value for x in xrange(1, 4)] - key = [self.bytesToNumber(v) for v in key] - self._rsa = _RSA.construct(key) - - def bytesToNumber(self, bytes): - total = 0L - for byte in bytes: - total = (total << 8) + byte - return total - - def decrypt(self, data): - return self._rsa.decrypt(data) - - return (ARC4, RSA, AES) - -def _load_crypto(): - ARC4 = RSA = AES = None - cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto) - if sys.platform.startswith('win'): - cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto) - for loader in cryptolist: - try: - ARC4, RSA, AES = loader() - break - except (ImportError, ADEPTError): - pass - return (ARC4, RSA, AES) - -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO - - -# Do we generate cross reference streams on output? -# 0 = never -# 1 = only if present in input -# 2 = always - -GEN_XREF_STM = 1 - -# This is the value for the current document -gen_xref_stm = False # will be set in PDFSerializer - -# PDF parsing routines from pdfminer, with changes for EBX_HANDLER - -# Utilities - -def choplist(n, seq): - '''Groups every n elements of the list.''' - r = [] - for x in seq: - r.append(x) - if len(r) == n: - yield tuple(r) - r = [] - return - -def nunpack(s, default=0): - '''Unpacks up to 4 bytes big endian.''' - l = len(s) - if not l: - return default - elif l == 1: - return ord(s) - elif l == 2: - return struct.unpack('>H', s)[0] - elif l == 3: - return struct.unpack('>L', '\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 - 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) - raise PDFEncryptionError('Unknown filter: param=%r' % param) - - def initialize_adobe_ps(self, password, docid, param): - global KEYFILEPATH - self.decrypt_key = self.genkey_adobe_ps(param) - self.genkey = self.genkey_v4 - self.decipher = self.decrypt_aes - self.ready = True - return - - def genkey_adobe_ps(self, param): - # nice little offline principal keys dictionary - # global static principal key for German Onleihe / Bibliothek Digital - principalkeys = { 'bibliothek-digital.de': 'rRwGv2tbpKov1krvv7PO0ws9S436/lArPlfipz5Pqhw='.decode('base64')} - self.is_printable = self.is_modifiable = self.is_extractable = True - length = int_value(param.get('Length', 0)) / 8 - edcdata = str_value(param.get('EDCData')).decode('base64') - pdrllic = str_value(param.get('PDRLLic')).decode('base64') - pdrlpol = str_value(param.get('PDRLPol')).decode('base64') - edclist = [] - for pair in edcdata.split('\n'): - edclist.append(pair) - # principal key request - for key in principalkeys: - if key in pdrllic: - principalkey = principalkeys[key] - else: - raise ADEPTError('Cannot find principal key for this pdf') - shakey = SHA256(principalkey) - ivector = 16 * chr(0) - 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 ...') - 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]) - pdrlpol = pdrlpol[:cutter] - 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.') - 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): - self.is_printable = self.is_modifiable = self.is_extractable = True - with open(password, 'rb') as f: - keyder = f.read() - rsa = RSA(keyder) - 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 improper booktype / decryption book session key errors - if length > 0: - if len(bookkey) == length: - if ebx_V == 3: - V = 3 - else: - V = 2 - elif len(bookkey) == length + 1: - V = ord(bookkey[0]) - bookkey = bookkey[1:] - else: - print "ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type) - print "length is %d and len(bookkey) is %d" % (length, len(bookkey)) - print "bookkey[0] is %d" % ord(bookkey[0]) - raise ADEPTError('error decrypting book session key - mismatched length') - else: - # proper length unknown try with whatever you have - print "ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type) - print "length is %d and len(bookkey) is %d" % (length, len(bookkey)) - print "bookkey[0] is %d" % ord(bookkey[0]) - if ebx_V == 3: - V = 3 - else: - V = 2 - self.decrypt_key = bookkey - self.genkey = self.genkey_v3 if V == 3 else self.genkey_v2 - self.decipher = self.decrypt_rc4 - self.ready = True - return - - # genkey functions - def genkey_v2(self, objid, genno): - objid = struct.pack(' PDFObjStmRef.maxindex: - PDFObjStmRef.maxindex = index - - -## PDFParser -## -class PDFParser(PSStackParser): - - def __init__(self, doc, fp): - PSStackParser.__init__(self, fp) - self.doc = doc - self.doc.set_parser(self) - return - - def __repr__(self): - return '' - - KEYWORD_R = PSKeywordTable.intern('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 plugin_main(keypath, inpath, outpath): - with open(inpath, 'rb') as inf: - try: - serializer = PDFSerializer(inf, keypath) - except: - print "Error serializing pdf. Probably wrong key." - return 1 - # 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 - try: - serializer.dump(outf) - except: - print "error writing pdf." - return 1 - return 0 - - from calibre.customize import FileTypePlugin from calibre.constants import iswindows, isosx +# 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) + + +class ADEPTError(Exception): + pass + class IneptPDFDeDRM(FileTypePlugin): - name = 'Inept PDF DeDRM' - description = 'Removes DRM from secure Adobe pdf files. \ - Credit given to I <3 Cabbages for the original stand-alone scripts.' + name = PLUGIN_NAME + description = u"Removes DRM from secure Adobe pdf files. Credit given to i♥cabbages for the original stand-alone scripts." supported_platforms = ['linux', 'osx', 'windows'] - author = 'DiapDealer' - version = (0, 1, 8) + author = u"DiapDealer, Apprentice Alf and i♥cabbages" + version = PLUGIN_VERSION_TUPLE minimum_calibre_version = (0, 7, 55) # for the new plugin interface file_types = set(['pdf']) on_import = True + priority = 100 def run(self, path_to_ebook): - from calibre_plugins.ineptpdf import outputfix - if sys.stdout.encoding == None: - sys.stdout = outputfix.getwriter('utf-8')(sys.stdout) - else: - sys.stdout = outputfix.getwriter(sys.stdout.encoding)(sys.stdout) - if sys.stderr.encoding == None: - sys.stderr = outputfix.getwriter('utf-8')(sys.stderr) - else: - sys.stderr = outputfix.getwriter(sys.stderr.encoding)(sys.stderr) + # make sure any unicode output gets converted safely with 'replace' + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) - global ARC4, RSA, AES - - ARC4, RSA, AES = _load_crypto() - - if AES == None or RSA == None or ARC4 == None: - # Failed to load libcrypto or PyCrypto... Adobe PDFs can\'t be decrypted.' - raise ADEPTError('IneptPDF: Failed to load crypto libs... Adobe PDFs can\'t be decrypted.') - return + print u"{0} v{1}: Trying to decrypt {2}.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)) # Load any keyfiles (*.der) included Calibre's config directory. userkeys = [] - # Find Calibre's configuration directory. + # self.plugin_path is passed in unicode because we defined our name in unicode confpath = os.path.split(os.path.split(self.plugin_path)[0])[0] - print 'IneptPDF: Calibre configuration directory = %s' % confpath + print u"{0} v{1}: Calibre configuration directory = {2}".format(PLUGIN_NAME, PLUGIN_VERSION, confpath) files = os.listdir(confpath) - filefilter = re.compile("\.der$", re.IGNORECASE) + filefilter = re.compile(u"\.der$", re.IGNORECASE) files = filter(filefilter.search, files) foundDefault = False - if files: try: for filename in files: - if filename[:16] == 'calibre-adeptkey': + if filename[:16] == u"calibre-adeptkey": foundDefault = True fpath = os.path.join(confpath, filename) with open(fpath, 'rb') as f: - userkeys.append(f.read()) - print 'IneptPDF: Keyfile %s found in config folder.' % filename + userkeys.append([f.read(), filename]) + print u"{0} v{1}: Keyfile {2} found in config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, filename) except IOError: - print 'IneptPDF: Error reading keyfiles from config directory.' + print u"{0} v{1}: Error reading keyfiles from config directory.".format(PLUGIN_NAME, PLUGIN_VERSION) pass if not foundDefault: # Try to find key from ADE install and save the key in # Calibre's configuration directory for future use. if iswindows or isosx: + #ignore annoying future warning from key generation + import warnings + warnings.filterwarnings('ignore', category=FutureWarning) + # ADE key retrieval script included in respective OS folder. - from calibre_plugins.ineptpdf.ineptkey import retrieve_keys + from calibre_plugins.ineptepub.ineptkey import retrieve_keys try: keys = retrieve_keys() for i,key in enumerate(keys): - userkeys.append(key) - keypath = os.path.join(confpath, 'calibre-adeptkey{0:d}.der'.format(i)) + keyname = u"calibre-adeptkey{0:d}.der".format(i) + userkeys.append([key,keyname]) + keypath = os.path.join(confpath, keyname) open(keypath, 'wb').write(key) - print 'IneptPDF: Created keyfile %s from ADE install.' % keypath + print u"{0} v{1}: Created keyfile {2} from ADE install.".format(PLUGIN_NAME, PLUGIN_VERSION, keyname) except: - print 'IneptPDF: Couldn\'t Retrieve key from ADE install.' + print u"{0} v{1}: Couldn\'t Retrieve key from ADE install.".format(PLUGIN_NAME, PLUGIN_VERSION) pass if not userkeys: # No user keys found... bail out. - raise ADEPTError('IneptPDF - No keys found. Check keyfile(s)/ADE install') - return None + raise ADEPTError(u"{0} v{1}: No keys found. Check keyfile(s)/ADE install".format(PLUGIN_NAME, PLUGIN_VERSION)) + return # Attempt to decrypt pdf with each encryption key found. - for userkey in userkeys: + from calibre_plugins.ineptpdf import ineptpdf + for userkeyinfo in userkeys: + print u"{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, userkeyinfo[1]) # Create a TemporaryPersistent file to work with. of = self.temporary_file('.pdf') - kf = self.temporary_file('.der') - with open(kf.name, 'wb') as f: - f.write(userkey) - # Give the user keyfile, ebook and TemporaryPersistent file to the plugin_main function. - print "Ready to start decrypting." - result = plugin_main(kf.name, path_to_ebook, of.name) + # Give the user keyfile, ebook and TemporaryPersistent file to the decryptBook function. + result = ineptpdf.decryptBook(userkeyinfo[0], path_to_ebook, of.name) # Decryption was successful return the modified PersistentTemporary # file to Calibre's import process. if result == 0: - print 'IneptPDF: Encryption successfully removed.' - of.close + print u"{0} v{1}: Encryption successfully removed.".format(PLUGIN_NAME, PLUGIN_VERSION) + of.close() return of.name break - else: - print 'IneptPDF: Encryption key invalid... trying others.' - of.close() + + print u"{0} v{1}: Encryption key incorrect.".format(PLUGIN_NAME, PLUGIN_VERSION) + of.close() # Something went wrong with decryption. - # Import the original unmolested pdf. - of.close - raise ADEPTError('IneptPDF - Ultimately failed to decrypt') - return None + raise ADEPTError(u"{0} v{1}: Ultimately failed to decrypt".format(PLUGIN_NAME, PLUGIN_VERSION)) + return diff --git a/Calibre_Plugins/ineptpdf_plugin/ineptkey.py b/Calibre_Plugins/ineptpdf_plugin/ineptkey.py index 723b7c6..a9bc62d 100644 --- a/Calibre_Plugins/ineptpdf_plugin/ineptkey.py +++ b/Calibre_Plugins/ineptpdf_plugin/ineptkey.py @@ -6,8 +6,8 @@ from __future__ import with_statement # ineptkey.pyw, version 5.6 # Copyright © 2009-2010 i♥cabbages -# 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 +# # Windows users: Before running this program, you must first install Python 2.6 # from and PyCrypto from @@ -37,7 +37,7 @@ from __future__ import with_statement # 5.3 - On Windows try PyCrypto first, OpenSSL next # 5.4 - Modify interface to allow use of import # 5.5 - Fix for potential problem with PyCrypto -# 5.6 - Revise to allow use in Plugins to eliminate need for duplicate code +# 5.6 - Revised to allow use in Plugins to eliminate need for duplicate code """ Retrieve Adobe ADEPT user key. @@ -49,12 +49,65 @@ import sys import os import struct +# 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') +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)] + return [u"ineptkey.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 ADEPTError(Exception): pass @@ -80,13 +133,13 @@ if iswindows: _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_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', @@ -308,9 +361,9 @@ if iswindows: cuser = winreg.HKEY_CURRENT_USER try: regkey = winreg.OpenKey(cuser, DEVICE_KEY_PATH) + device = winreg.QueryValueEx(regkey, 'key')[0] except WindowsError: raise ADEPTError("Adobe Digital Editions not activated") - device = winreg.QueryValueEx(regkey, 'key')[0] keykey = CryptUnprotectData(device, entropy) userkey = None keys = [] @@ -343,7 +396,7 @@ if iswindows: if len(keys) == 0: raise ADEPTError('Could not locate privateLicenseKey') return keys - + elif isosx: import xml.etree.ElementTree as etree @@ -386,7 +439,7 @@ else: def retrieve_keys(keypath): raise ADEPTError("This script only supports Windows and Mac OS X.") return [] - + def retrieve_key(keypath): keys = retrieve_keys() with open(keypath, 'wb') as f: @@ -397,22 +450,22 @@ def extractKeyfile(keypath): try: success = retrieve_key(keypath) except ADEPTError, e: - print "Key generation Error: " + str(e) + print u"Key generation Error: {0}".format(e.args[0]) return 1 except Exception, e: - print "General Error: " + str(e) + print "General Error: {0}".format(e.args[0]) return 1 if not success: return 1 return 0 -def cli_main(argv=sys.argv): +def cli_main(argv=unicode_argv()): keypath = argv[1] return extractKeyfile(keypath) -def main(argv=sys.argv): +def gui_main(argv=unicode_argv()): import Tkinter import Tkconstants import tkMessageBox @@ -421,24 +474,24 @@ def main(argv=sys.argv): class ExceptionDialog(Tkinter.Frame): def __init__(self, root, text): Tkinter.Frame.__init__(self, root, border=5) - label = Tkinter.Label(self, text="Unexpected error:", + label = Tkinter.Label(self, text=u"Unexpected error:", anchor=Tkconstants.W, justify=Tkconstants.LEFT) label.pack(fill=Tkconstants.X, expand=0) self.text = Tkinter.Text(self) self.text.pack(fill=Tkconstants.BOTH, expand=1) - + self.text.insert(Tkconstants.END, text) root = Tkinter.Tk() root.withdraw() - progname = os.path.basename(argv[0]) - keypath = os.path.abspath("adeptkey.der") + keypath, progname = os.path.split(argv[0]) + keypath = os.path.join(keypath, u"adeptkey.der") success = False try: success = retrieve_key(keypath) except ADEPTError, e: - tkMessageBox.showerror("ADEPT Key", "Error: " + str(e)) + tkMessageBox.showerror(u"ADEPT Key", "Error: {0}".format(e.args[0])) except Exception: root.wm_state('normal') root.title('ADEPT Key') @@ -448,10 +501,12 @@ def main(argv=sys.argv): if not success: return 1 tkMessageBox.showinfo( - "ADEPT Key", "Key successfully retrieved to %s" % (keypath)) + u"ADEPT Key", u"Key successfully retrieved to {0}".format(keypath)) 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(main()) + sys.exit(gui_main()) diff --git a/Other_Tools/Adobe_PDF_Tools/ineptpdf.pyw b/Calibre_Plugins/ineptpdf_plugin/ineptpdf.py similarity index 88% rename from Other_Tools/Adobe_PDF_Tools/ineptpdf.pyw rename to Calibre_Plugins/ineptpdf_plugin/ineptpdf.py index 20721d1..9f4883e 100644 --- a/Other_Tools/Adobe_PDF_Tools/ineptpdf.pyw +++ b/Calibre_Plugins/ineptpdf_plugin/ineptpdf.py @@ -1,13 +1,25 @@ -#! /usr/bin/env python -# ineptpdf.pyw, version 7.11 +#! /usr/bin/python +# -*- coding: utf-8 -*- from __future__ import with_statement -# To run this program install Python 2.6 from http://www.python.org/download/ -# and OpenSSL (already installed on Mac OS X and Linux) OR -# PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto -# (make sure to install the version for Python 2.6). Save this script file as -# ineptpdf.pyw and double-click on it to run it. +# ineptpdf.pyw, version 7.11 +# 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 +# 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. # Revision history: # 1 - Initial release @@ -36,12 +48,14 @@ from __future__ import with_statement # 7.9 - Bug fix for some session key errors when len(bookkey) > length required # 7.10 - Various tweaks to fix minor problems. # 7.11 - More tweaks to fix minor problems. +# 7.12 - Revised to allow use in calibre plugins to eliminate need for duplicate code """ Decrypts Adobe ADEPT-encrypted PDF files. """ __license__ = 'GPL v3' +__version__ = "7.12" import sys import os @@ -51,10 +65,63 @@ import struct import hashlib from itertools import chain, islice import xml.etree.ElementTree as etree -import Tkinter -import Tkconstants -import tkFileDialog -import tkMessageBox + +# 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)] + 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] + class ADEPTError(Exception): pass @@ -1520,9 +1587,7 @@ class PDFDocument(object): def initialize_ebx(self, password, docid, param): self.is_printable = self.is_modifiable = self.is_extractable = True - with open(password, 'rb') as f: - keyder = f.read() - rsa = RSA(keyder) + rsa = RSA(password) length = int_value(param.get('Length', 0)) / 8 rights = str_value(param.get('ADEPT_LICENSE')).decode('base64') rights = zlib.decompress(rights, -15) @@ -1907,14 +1972,14 @@ class PDFObjStrmParser(PDFParser): ### My own code, for which there is none else to blame class PDFSerializer(object): - def __init__(self, inf, keypath): + def __init__(self, inf, userkey): global GEN_XREF_STM, gen_xref_stm gen_xref_stm = GEN_XREF_STM > 1 self.version = inf.read(8) inf.seek(0) self.doc = doc = PDFDocument() parser = PDFParser(doc, inf) - doc.initialize(keypath) + doc.initialize(userkey) self.objids = objids = set() for xref in reversed(doc.xrefs): trailer = xref.trailer @@ -2097,142 +2162,144 @@ class PDFSerializer(object): self.write('endobj\n') -class DecryptionDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - ltext='Select file for decryption\n' - 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='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) - 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 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 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): - 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 - self.status['text'] = 'Specified Adept key file does not exist' - return - if not inpath or not os.path.exists(inpath): - self.status['text'] = 'Specified input file does not exist' - return - if not outpath: - self.status['text'] = 'Output file not specified' - return - if inpath == outpath: - self.status['text'] = 'Must have different input and output files' - return - # patch for non-ascii characters - 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 decryptBook(keypath, inpath, outpath): +def decryptBook(userkey, inpath, outpath): + if RSA is None: + raise ADEPTError(u"PyCrypto or OpenSSL must be installed.") with open(inpath, 'rb') as inf: try: - serializer = PDFSerializer(inf, keypath) + serializer = PDFSerializer(inf, userkey) except: - print "Error serializing pdf. Probably wrong key." - return 1 + print u"Error serializing pdf {0}. Probably wrong key.".format(os.path.basename(inpath)) + return 2 # 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 + # help construct to make sure the method runs to the end try: serializer.dump(outf) - except: - print "error writing pdf." - return 1 + except Exception, e: + print u"error writing pdf: {0}".format(e.args[0]) + return 2 return 0 -def cli_main(argv=sys.argv): +def cli_main(argv=unicode_argv()): progname = os.path.basename(argv[0]) - if RSA 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 "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,) + print u"usage: {0} ".format(progname) return 1 keypath, inpath, outpath = argv[1:] - return decryptBook(keypath, inpath, outpath) + 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 + + 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"adeptkey.der"): + self.keypath.insert(0, u"adeptkey.der") + 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 Adobe Adept \'.der\' key file", + defaultextension=u".der", + filetypes=[('Adobe Adept DER-encoded files', '.der'), + ('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 ADEPT-encrypted PDF file to decrypt", + defaultextension=u".pdf", filetypes=[('PDF files', '.pdf')]) + 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 PDF file to produce", + defaultextension=u".pdf", filetypes=[('PDF files', '.pdf')]) + 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: + self.status['text'] = u"File successfully decrypted" + else: + self.status['text'] = u"The was an error decrypting the file." + + root = Tkinter.Tk() if RSA is None: root.withdraw() @@ -2241,7 +2308,7 @@ def gui_main(): "This script requires OpenSSL or PyCrypto, which must be installed " "separately. Read the top-of-script comment for details.") return 1 - root.title('INEPT PDF Decrypter') + root.title(u"Adobe Adept PDF Decrypter v.{0}".format(__version__)) root.resizable(True, False) root.minsize(370, 0) DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1) @@ -2251,5 +2318,7 @@ def gui_main(): 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/ineptpdf_plugin/outputfix.py b/Calibre_Plugins/ineptpdf_plugin/outputfix.py deleted file mode 100644 index 906c6e9..0000000 --- a/Calibre_Plugins/ineptpdf_plugin/outputfix.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Adapted and simplified from the kitchen project -# -# Kitchen Project Copyright (c) 2012 Red Hat, Inc. -# -# kitchen is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# kitchen is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with kitchen; if not, see -# -# Authors: -# Toshio Kuratomi -# Seth Vidal -# -# Portions of code taken from yum/i18n.py and -# python-fedora: fedora/textutils.py - -import codecs - -# returns a char string unchanged -# returns a unicode string converted to a char string of the passed encoding -# return the empty string for anything else -def getwriter(encoding): - class _StreamWriter(codecs.StreamWriter): - def __init__(self, stream): - codecs.StreamWriter.__init__(self, stream, 'replace') - - def encode(self, msg, errors='replace'): - if isinstance(msg, basestring): - if isinstance(msg, str): - return (msg, len(msg)) - return (msg.encode(self.encoding, 'replace'), len(msg)) - return ('',0) - - _StreamWriter.encoding = encoding - return _StreamWriter diff --git a/Calibre_Plugins/k4mobidedrm_plugin.zip b/Calibre_Plugins/k4mobidedrm_plugin.zip index 75c17f00aea5b345e60758efe0babd438ebf2333..e62b1b5f7d1650621c1b5038c6b19cd528ffafd1 100644 GIT binary patch delta 55383 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�PM+}fStNFc|4_9X)1Q-xA%=J`?e}Q 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 KW+%YoZ~qUB;*X91 delta 43237 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 P{&s>ZhffhsKmh&+N9-aF diff --git a/Calibre_Plugins/k4mobidedrm_plugin/k4mutils.py b/Calibre_Plugins/k4mobidedrm_plugin/k4mutils.py index 1fc08cb..bceb3a3 100644 --- a/Calibre_Plugins/k4mobidedrm_plugin/k4mutils.py +++ b/Calibre_Plugins/k4mobidedrm_plugin/k4mutils.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + # standlone set of Mac OSX specific routines needed for KindleBooks from __future__ import with_statement @@ -22,7 +25,7 @@ def _load_crypto_libcrypto(): libcrypto = find_library('crypto') if libcrypto is None: - raise DrmException('libcrypto not found') + raise DrmException(u"libcrypto not found") libcrypto = CDLL(libcrypto) # From OpenSSL's crypto aes header @@ -80,14 +83,14 @@ def _load_crypto_libcrypto(): def set_decrypt_key(self, userkey, iv): self._blocksize = len(userkey) if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : - raise DrmException('AES improper key used') + raise DrmException(u"AES improper key used") return keyctx = self._keyctx = AES_KEY() self._iv = iv self._userkey = userkey rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx) if rv < 0: - raise DrmException('Failed to initialize AES key') + raise DrmException(u"Failed to initialize AES key") def decrypt(self, data): out = create_string_buffer(len(data)) @@ -95,7 +98,7 @@ def _load_crypto_libcrypto(): keyctx = self._keyctx rv = AES_cbc_encrypt(data, out, len(data), keyctx, mutable_iv, 0) if rv == 0: - raise DrmException('AES decryption failed') + raise DrmException(u"AES decryption failed") return out.raw def keyivgen(self, passwd, salt, iter, keylen): @@ -139,20 +142,20 @@ def SHA256(message): return ctx.digest() # Various character maps used to decrypt books. Probably supposed to act as obfuscation -charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" -charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM" +charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M' +charMap2 = 'ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM' # For kinf approach of K4Mac 1.6.X or later -# On K4PC charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE" +# On K4PC charMap5 = 'AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE' # For Mac they seem to re-use charMap2 here charMap5 = charMap2 # new in K4M 1.9.X -testMap8 = "YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD" +testMap8 = 'YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD' def encode(data, map): - result = "" + result = '' for char in data: value = ord(char) Q = (value ^ 0x80) // len(map) @@ -167,14 +170,14 @@ def encodeHash(data,map): # Decode the string in data with the characters in map. Returns the decoded bytes def decode(data,map): - result = "" + result = '' for i in range (0,len(data)-1,2): high = map.find(data[i]) low = map.find(data[i+1]) if (high == -1) or (low == -1) : break value = (((high * len(map)) ^ 0x80) & 0xFF) + low - result += pack("B",value) + result += pack('B',value) return result # For K4M 1.6.X and later @@ -200,7 +203,7 @@ def primes(n): # uses a sub process to get the Hard Drive Serial Number using ioreg -# returns with the serial number of drive whose BSD Name is "disk0" +# returns with the serial number of drive whose BSD Name is 'disk0' def GetVolumeSerialNumber(): sernum = os.getenv('MYSERIALNUMBER') if sernum != None: @@ -216,11 +219,11 @@ def GetVolumeSerialNumber(): foundIt = False for j in xrange(cnt): resline = reslst[j] - pp = resline.find('"Serial Number" = "') + pp = resline.find('\"Serial Number\" = \"') if pp >= 0: sernum = resline[pp+19:-1] sernum = sernum.strip() - bb = resline.find('"BSD Name" = "') + bb = resline.find('\"BSD Name\" = \"') if bb >= 0: bsdname = resline[bb+14:-1] bsdname = bsdname.strip() @@ -277,7 +280,7 @@ def GetDiskPartitionUUID(diskpart): nest += 1 if resline.find('}') >= 0: nest -= 1 - pp = resline.find('"UUID" = "') + pp = resline.find('\"UUID\" = \"') if pp >= 0: uuidnum = resline[pp+10:-1] uuidnum = uuidnum.strip() @@ -285,7 +288,7 @@ def GetDiskPartitionUUID(diskpart): if partnest == uuidnest and uuidnest > 0: foundIt = True break - bb = resline.find('"BSD Name" = "') + bb = resline.find('\"BSD Name\" = \"') if bb >= 0: bsdname = resline[bb+14:-1] bsdname = bsdname.strip() @@ -323,7 +326,7 @@ def GetMACAddressMunged(): if pp >= 0: macnum = resline[pp+6:-1] macnum = macnum.strip() - # print "original mac", macnum + # print 'original mac', macnum # now munge it up the way Kindle app does # by xoring it with 0xa5 and swapping elements 3 and 4 maclst = macnum.split(':') @@ -340,7 +343,7 @@ def GetMACAddressMunged(): mlst[2] = maclst[2] ^ 0xa5 mlst[1] = maclst[1] ^ 0xa5 mlst[0] = maclst[0] ^ 0xa5 - macnum = "%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x" % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5]) + macnum = '%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x' % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5]) foundIt = True break if not foundIt: @@ -367,6 +370,19 @@ def isNewInstall(): return False +class Memoize: + """Memoize(fn) - an instance which acts like fn but memoizes its arguments + Will only work on functions with non-mutable arguments + """ + def __init__(self, fn): + self.fn = fn + self.memo = {} + def __call__(self, *args): + if not self.memo.has_key(args): + self.memo[args] = self.fn(*args) + return self.memo[args] + +@Memoize def GetIDString(): # K4Mac now has an extensive set of ids strings it uses # in encoding pids and in creating unique passwords @@ -530,7 +546,8 @@ def getKindleInfoFiles(): # determine type of kindle info provided and return a # database of keynames and values def getDBfromFile(kInfoFile): - names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber", "max_date", "SIGVERIF"] + + names = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber', 'max_date', 'SIGVERIF'] DB = {} cnt = 0 infoReader = open(kInfoFile, 'r') @@ -545,12 +562,12 @@ def getDBfromFile(kInfoFile): for item in items: if item != '': keyhash, rawdata = item.split(':') - keyname = "unknown" + keyname = 'unknown' for name in names: if encodeHash(name,charMap2) == keyhash: keyname = name break - if keyname == "unknown": + if keyname == 'unknown': keyname = keyhash encryptedValue = decode(rawdata,charMap2) cleartext = cud.decrypt(encryptedValue) @@ -563,8 +580,8 @@ def getDBfromFile(kInfoFile): if hdr == '/': # else newer style .kinf file used by K4Mac >= 1.6.0 - # the .kinf file uses "/" to separate it into records - # so remove the trailing "/" to make it easy to use split + # the .kinf file uses '/' to separate it into records + # so remove the trailing '/' to make it easy to use split data = data[:-1] items = data.split('/') cud = CryptUnprotectDataV2() @@ -578,11 +595,11 @@ def getDBfromFile(kInfoFile): # the first 32 chars of the first record of a group # is the MD5 hash of the key name encoded by charMap5 keyhash = item[0:32] - keyname = "unknown" + keyname = 'unknown' # the raw keyhash string is also used to create entropy for the actual # CryptProtectData Blob that represents that keys contents - # "entropy" not used for K4Mac only K4PC + # 'entropy' not used for K4Mac only K4PC # entropy = SHA1(keyhash) # the remainder of the first record when decoded with charMap5 @@ -599,12 +616,12 @@ def getDBfromFile(kInfoFile): item = items.pop(0) edlst.append(item) - keyname = "unknown" + keyname = 'unknown' for name in names: if encodeHash(name,charMap5) == keyhash: keyname = name break - if keyname == "unknown": + if keyname == 'unknown': keyname = keyhash # the charMap5 encoded contents data has had a length @@ -615,10 +632,10 @@ def getDBfromFile(kInfoFile): # The offset into the charMap5 encoded contents seems to be: # len(contents) - largest prime number less than or equal to int(len(content)/3) - # (in other words split "about" 2/3rds of the way through) + # (in other words split 'about' 2/3rds of the way through) # move first offsets chars to end to align for decode by charMap5 - encdata = "".join(edlst) + encdata = ''.join(edlst) contlen = len(encdata) # now properly split and recombine @@ -667,7 +684,7 @@ def getDBfromFile(kInfoFile): # the first 32 chars of the first record of a group # is the MD5 hash of the key name encoded by charMap5 keyhash = item[0:32] - keyname = "unknown" + keyname = 'unknown' # unlike K4PC the keyhash is not used in generating entropy # entropy = SHA1(keyhash) + added_entropy @@ -687,12 +704,12 @@ def getDBfromFile(kInfoFile): item = items.pop(0) edlst.append(item) - keyname = "unknown" + keyname = 'unknown' for name in names: if encodeHash(name,testMap8) == keyhash: keyname = name break - if keyname == "unknown": + if keyname == 'unknown': keyname = keyhash # the testMap8 encoded contents data has had a length @@ -703,10 +720,10 @@ def getDBfromFile(kInfoFile): # The offset into the testMap8 encoded contents seems to be: # len(contents) - largest prime number less than or equal to int(len(content)/3) - # (in other words split "about" 2/3rds of the way through) + # (in other words split 'about' 2/3rds of the way through) # move first offsets chars to end to align for decode by testMap8 - encdata = "".join(edlst) + encdata = ''.join(edlst) contlen = len(encdata) # now properly split and recombine diff --git a/Calibre_Plugins/k4mobidedrm_plugin/k4pcutils.py b/Calibre_Plugins/k4mobidedrm_plugin/k4pcutils.py index 9f9ca07..476844c 100644 --- a/Calibre_Plugins/k4mobidedrm_plugin/k4pcutils.py +++ b/Calibre_Plugins/k4mobidedrm_plugin/k4pcutils.py @@ -1,4 +1,6 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- + # K4PC Windows specific routines from __future__ import with_statement diff --git a/Calibre_Plugins/k4mobidedrm_plugin/mobidedrm.py b/Calibre_Plugins/k4mobidedrm_plugin/mobidedrm.py index cd993e1..113f57a 100644 --- a/Calibre_Plugins/k4mobidedrm_plugin/mobidedrm.py +++ b/Calibre_Plugins/k4mobidedrm_plugin/mobidedrm.py @@ -1,5 +1,11 @@ -#!/usr/bin/python +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# mobidedrm.py, version 0.38 +# Copyright © 2008 The Dark Reverser # +# Modified 2008–2012 by some_updates, DiapDealer and Apprentice Alf + # This is a python script. You need a Python interpreter to run it. # For example, ActiveState Python, which exists for windows. # @@ -59,26 +65,78 @@ # 0.35 - add interface to get mobi_version # 0.36 - fixed problem with TEXtREAd and getBookTitle interface # 0.37 - Fixed double announcement for stand-alone operation +# 0.38 - Unicode used wherever possible, cope with absent alfcrypto -__version__ = '0.37' +__version__ = u"0.38" import sys +import os +import struct +import binascii +try: + from alfcrypto import Pukall_Cipher +except: + print u"AlfCrypto not found. Using python PC1 implementation." -class Unbuffered: +# 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) -sys.stdout=Unbuffered(sys.stdout) -import os -import struct -import binascii -from alfcrypto import Pukall_Cipher +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"mobidedrm.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 DrmException(Exception): pass @@ -90,40 +148,45 @@ class DrmException(Exception): # Implementation of Pukall Cipher 1 def PC1(key, src, decryption=True): - return Pukall_Cipher().PC1(key,src,decryption) -# sum1 = 0; -# sum2 = 0; -# keyXorVal = 0; -# if len(key)!=16: -# print "Bad key length!" -# return None -# wkey = [] -# for i in xrange(8): -# wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1])) -# dst = "" -# for i in xrange(len(src)): -# temp1 = 0; -# byteXorVal = 0; -# for j in xrange(8): -# temp1 ^= wkey[j] -# sum2 = (sum2+j)*20021 + sum1 -# sum1 = (temp1*346)&0xFFFF -# sum2 = (sum2+sum1)&0xFFFF -# temp1 = (temp1*20021+1)&0xFFFF -# byteXorVal ^= temp1 ^ sum2 -# curByte = ord(src[i]) -# if not decryption: -# keyXorVal = curByte * 257; -# curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF -# if decryption: -# keyXorVal = curByte * 257; -# for j in xrange(8): -# wkey[j] ^= keyXorVal; -# dst+=chr(curByte) -# return dst + # if we can get it from alfcrypto, use that + try: + return Pukall_Cipher().PC1(key,src,decryption) + except NameError: + pass + + # use slow python version, since Pukall_Cipher didn't load + sum1 = 0; + sum2 = 0; + keyXorVal = 0; + if len(key)!=16: + DrmException (u"PC1: Bad key length") + wkey = [] + for i in xrange(8): + wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1])) + dst = "" + for i in xrange(len(src)): + temp1 = 0; + byteXorVal = 0; + for j in xrange(8): + temp1 ^= wkey[j] + sum2 = (sum2+j)*20021 + sum1 + sum1 = (temp1*346)&0xFFFF + sum2 = (sum2+sum1)&0xFFFF + temp1 = (temp1*20021+1)&0xFFFF + byteXorVal ^= temp1 ^ sum2 + curByte = ord(src[i]) + if not decryption: + keyXorVal = curByte * 257; + curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF + if decryption: + keyXorVal = curByte * 257; + for j in xrange(8): + wkey[j] ^= keyXorVal; + dst+=chr(curByte) + return dst def checksumPid(s): - letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" + letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789' crc = (~binascii.crc32(s,-1))&0xFFFFFFFF crc = crc ^ (crc >> 16) res = s @@ -171,17 +234,24 @@ class MobiBook: off = self.sections[section][0] return self.data_file[off:endoff] - def __init__(self, infile, announce = True): - if announce: - print ('MobiDeDrm v%(__version__)s. ' - 'Copyright 2008-2012 The Dark Reverser et al.' % globals()) + def cleanup(self): + # to match function in Topaz book + pass + + def __init__(self, infile): + print u"MobiDeDrm v{0:s}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__) + + try: + from alfcrypto import Pukall_Cipher + except: + print u"AlfCrypto not found. Using python PC1 implementation." # initial sanity check on file self.data_file = file(infile, 'rb').read() self.mobi_data = '' self.header = self.data_file[0:78] if self.header[0x3C:0x3C+8] != 'BOOKMOBI' and self.header[0x3C:0x3C+8] != 'TEXtREAd': - raise DrmException("invalid file format") + raise DrmException(u"Invalid file format") self.magic = self.header[0x3C:0x3C+8] self.crypto_type = -1 @@ -199,7 +269,7 @@ class MobiBook: self.compression, = struct.unpack('>H', self.sect[0x0:0x0+2]) if self.magic == 'TEXtREAd': - print "Book has format: ", self.magic + print u"PalmDoc format book detected." self.extra_data_flags = 0 self.mobi_length = 0 self.mobi_codepage = 1252 @@ -209,11 +279,11 @@ class MobiBook: self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18]) self.mobi_codepage, = struct.unpack('>L',self.sect[0x1c:0x20]) self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C]) - print "MOBI header version = %d, length = %d" %(self.mobi_version, self.mobi_length) + print u"MOBI header version {0:d}, header length {1:d}".format(self.mobi_version, self.mobi_length) self.extra_data_flags = 0 if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5): self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4]) - print "Extra Data Flags = %d" % self.extra_data_flags + print u"Extra Data Flags: {0:d}".format(self.extra_data_flags) if (self.compression != 17480): # multibyte utf8 data is included in the encryption for PalmDoc compression # so clear that byte so that we leave it to be decrypted. @@ -223,10 +293,10 @@ class MobiBook: self.meta_array = {} try: exth_flag, = struct.unpack('>L', self.sect[0x80:0x84]) - exth = 'NONE' + exth = '' if exth_flag & 0x40: exth = self.sect[16 + self.mobi_length:] - if (len(exth) >= 4) and (exth[:4] == 'EXTH'): + if (len(exth) >= 12) and (exth[:4] == 'EXTH'): nitems, = struct.unpack('>I', exth[8:12]) pos = 12 for i in xrange(nitems): @@ -236,10 +306,10 @@ class MobiBook: # reset the text to speech flag and clipping limit, if present if type == 401 and size == 9: # set clipping limit to 100% - self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8) + self.patchSection(0, '\144', 16 + self.mobi_length + pos + 8) elif type == 404 and size == 9: # make sure text to speech is enabled - self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8) + self.patchSection(0, '\0', 16 + self.mobi_length + pos + 8) # print type, size, content, content.encode('hex') pos += size except: @@ -265,8 +335,8 @@ class MobiBook: codec = codec_map[self.mobi_codepage] if title == '': title = self.header[:32] - title = title.split("\0")[0] - return unicode(title, codec).encode('utf-8') + title = title.split('\0')[0] + return unicode(title, codec) def getPIDMetaInfo(self): rec209 = '' @@ -297,7 +367,7 @@ class MobiBook: def parseDRM(self, data, count, pidlist): found_key = None - keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96" + keyvec1 = '\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96' for pid in pidlist: bigpid = pid.ljust(16,'\0') temp_key = PC1(keyvec1, bigpid, False) @@ -315,7 +385,7 @@ class MobiBook: break if not found_key: # Then try the default encoding that doesn't require a PID - pid = "00000000" + pid = '00000000' temp_key = keyvec1 temp_key_sum = sum(map(ord,temp_key)) & 0xff for i in xrange(count): @@ -328,82 +398,90 @@ class MobiBook: break return [found_key,pid] - def getMobiFile(self, outpath): + def getFile(self, outpath): file(outpath,'wb').write(self.mobi_data) - def getMobiVersion(self): - return self.mobi_version + def getBookType(self): + if self.print_replica: + return u"Print Replica" + if self.mobi_version >= 8: + return u"Kindle Format 8" + return u"Mobipocket" - def getPrintReplica(self): - return self.print_replica + def getBookExtension(self): + if self.print_replica: + return u".azw4" + if self.mobi_version >= 8: + return u".azw3" + return u".mobi" def processBook(self, pidlist): crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2]) - print 'Crypto Type is: ', crypto_type + print u"Crypto Type is: {0:d}".format(crypto_type) self.crypto_type = crypto_type if crypto_type == 0: - print "This book is not encrypted." + print u"This book is not encrypted." # we must still check for Print Replica self.print_replica = (self.loadSection(1)[0:4] == '%MOP') self.mobi_data = self.data_file return if crypto_type != 2 and crypto_type != 1: - raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type) + raise DrmException(u"Cannot decode unknown Mobipocket encryption type {0:d}".format(crypto_type)) if 406 in self.meta_array: data406 = self.meta_array[406] val406, = struct.unpack('>Q',data406) if val406 != 0: - raise DrmException("Cannot decode library or rented ebooks.") + raise DrmException(u"Cannot decode library or rented ebooks.") goodpids = [] for pid in pidlist: if len(pid)==10: if checksumPid(pid[0:-2]) != pid: - print "Warning: PID " + pid + " has incorrect checksum, should have been "+checksumPid(pid[0:-2]) + print u"Warning: PID {0} has incorrect checksum, should have been {1}".format(pid,checksumPid(pid[0:-2])) goodpids.append(pid[0:-2]) elif len(pid)==8: goodpids.append(pid) if self.crypto_type == 1: - t1_keyvec = "QDCVEPMU675RUBSZ" + t1_keyvec = 'QDCVEPMU675RUBSZ' if self.magic == 'TEXtREAd': bookkey_data = self.sect[0x0E:0x0E+16] elif self.mobi_version < 0: bookkey_data = self.sect[0x90:0x90+16] else: bookkey_data = self.sect[self.mobi_length+16:self.mobi_length+32] - pid = "00000000" + pid = '00000000' found_key = PC1(t1_keyvec, bookkey_data) else : # calculate the keys drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16]) if drm_count == 0: - raise DrmException("Not yet initialised with PID. Must be opened with Mobipocket Reader first.") + raise DrmException(u"Encryption not initialised. Must be opened with Mobipocket Reader first.") found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids) if not found_key: - raise DrmException("No key found in " + str(len(goodpids)) + " keys tried. Read the FAQs at Alf's blog. Only if none apply, report this failure for help.") + raise DrmException(u"No key found in {0:d} keys tried. Read the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(len(goodpids))) # kill the drm keys - self.patchSection(0, "\0" * drm_size, drm_ptr) + self.patchSection(0, '\0' * drm_size, drm_ptr) # kill the drm pointers - self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8) + self.patchSection(0, '\xff' * 4 + '\0' * 12, 0xA8) - if pid=="00000000": - print "File has default encryption, no specific PID." + if pid=='00000000': + print u"File has default encryption, no specific key needed." else: - print "File is encoded with PID "+checksumPid(pid)+"." + print u"File is encoded with PID {0}.".format(checksumPid(pid)) # clear the crypto type self.patchSection(0, "\0" * 2, 0xC) # decrypt sections - print "Decrypting. Please wait . . .", + print u"Decrypting. Please wait . . .", mobidataList = [] mobidataList.append(self.data_file[:self.sections[1][0]]) for i in xrange(1, self.records+1): data = self.loadSection(i) extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags) if i%100 == 0: - print ".", + print u".", # print "record %d, extra_size %d" %(i,extra_size) decoded_data = PC1(found_key, data[0:len(data) - extra_size]) if i==1: @@ -414,31 +492,24 @@ class MobiBook: if self.num_sections > self.records+1: mobidataList.append(self.data_file[self.sections[self.records+1][0]:]) self.mobi_data = "".join(mobidataList) - print "done" + print u"done" return -def getUnencryptedBook(infile,pid,announce=True): +def getUnencryptedBook(infile,pidlist): if not os.path.isfile(infile): - raise DrmException('Input File Not Found') - book = MobiBook(infile,announce) - book.processBook([pid]) - return book.mobi_data - -def getUnencryptedBookWithList(infile,pidlist,announce=True): - if not os.path.isfile(infile): - raise DrmException('Input File Not Found') - book = MobiBook(infile, announce) + raise DrmException(u"Input File Not Found.") + book = MobiBook(infile) book.processBook(pidlist) return book.mobi_data -def main(argv=sys.argv): - print ('MobiDeDrm v%(__version__)s. ' - 'Copyright 2008-2012 The Dark Reverser et al.' % globals()) +def cli_main(argv=unicode_argv()): + progname = os.path.basename(argv[0]) if len(argv)<3 or len(argv)>4: - print "Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks" - print "Usage:" - print " %s []" % sys.argv[0] + print u"MobiDeDrm v{0}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__) + print u"Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks" + print u"Usage:" + print u" {0} []".format(os.path.basename(sys.argv[0])) return 1 else: infile = argv[1] @@ -446,15 +517,17 @@ def main(argv=sys.argv): if len(argv) is 4: pidlist = argv[3].split(',') else: - pidlist = {} + pidlist = [] try: - stripped_file = getUnencryptedBookWithList(infile, pidlist, False) + stripped_file = getUnencryptedBook(infile, pidlist) file(outfile, 'wb').write(stripped_file) except DrmException, e: - print "Error: %s" % e + print u"MobiDeDRM v{0} Error: {0:s}".format(__version__,e.args[0]) return 1 return 0 -if __name__ == "__main__": - sys.exit(main()) +if __name__ == '__main__': + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) + sys.exit(cli_main()) diff --git a/DeDRM_Macintosh_Application/DeDRM ReadMe.rtf b/DeDRM_Macintosh_Application/DeDRM ReadMe.rtf index b95faf7..63da825 100644 --- a/DeDRM_Macintosh_Application/DeDRM ReadMe.rtf +++ b/DeDRM_Macintosh_Application/DeDRM ReadMe.rtf @@ -41,7 +41,7 @@ Mac OS X 10.5 and above: You do \i not \i0 need to install Python.\ \ -Drag the DeDRM application from from tools_v5.4.1\\DeDRM_Applications\\Macintosh (the location of this ReadMe) to your Applications folder, or anywhere else you find convenient.\ +Drag the DeDRM application from from tools_v5.5\\DeDRM_Applications\\Macintosh (the location of this ReadMe) to your Applications folder, or anywhere else you find convenient.\ \ \ diff --git a/DeDRM_Macintosh_Application/DeDRM.app.txt b/DeDRM_Macintosh_Application/DeDRM.app.txt index 17d8dff..cb177cb 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app.txt +++ b/DeDRM_Macintosh_Application/DeDRM.app.txt @@ -13,6 +13,7 @@ global BNKeyGenTool global BNePubTool global AdobeKeyGenTool global AdobeePubTool +global ePubTestTool global AdobePDFTool global ZipFixTool global ProgressApp @@ -42,7 +43,7 @@ on writetolog(logstring) try set fileRef to open for access logFilePath with write permission write logstring & " -" to fileRef starting at eof +" to fileRef starting at eof as class utf8 close access fileRef end try end writetolog @@ -68,11 +69,7 @@ on readtemp() try set fileRef to open for access tempfilepath if (get eof fileRef) > 0 then - set tempContentsLines to (read fileRef from 1 using delimiter (character id 10)) - set oldTIDs to AppleScript's text item delimiters - set AppleScript's text item delimiters to (character id 13) - set tempContents to tempContentsLines as string - set AppleScript's text item delimiters to oldTIDs + set tempContents to read fileRef from 1 as class utf8 end if close access fileRef end try @@ -155,6 +152,7 @@ on GetTools() set BNePubTool to POSIX path of (path to resource "ignobleepub.py") set AdobeKeyGenTool to POSIX path of (path to resource "ineptkey.py") set AdobeePubTool to POSIX path of (path to resource "ineptepub.py") + set ePubTestTool to POSIX path of (path to resource "epubtest.py") set AdobePDFTool to POSIX path of (path to resource "ineptpdf.py") set ZipFixTool to POSIX path of (path to resource "zipfix.py") set ProgressApp to POSIX path of (path to resource "DeDRM Progress.app") @@ -208,6 +206,12 @@ on GetTools() return false end if end if + if not fileexists(ePubTestTool) then + set dialogresult to (display dialog "The ePub encryption test script (epubtesttool.py) is missing from this package. Get a fresh copy." buttons {"Quit", "Continue Anyway"} default button 1 with title "DeDRM" with icon stop) + if button returned of dialogresult is "Quit" then + return false + end if + end if if not folderexists(ProgressApp) then set dialogresult to (display dialog "The Progress dialog application (DeDRM Progress.app) is missing from this package. Get a fresh copy." buttons {"Quit", "Continue Anyway"} default button 1 with title "DeDRM" with icon stop) if button returned of dialogresult is "Quit" then @@ -278,7 +282,8 @@ on unlockmobifile(encryptedFile) writetolog("shellresult: " & shellresult & " " & ErrorText) try repeat - if (not DecodingError) or (totalebooks > 1) or (offset of "No key found" in ErrorText) is 0 then + if (totalebooks > 1) or (offset of "No key found" in shellresult) is 0 then + --display dialog (totalebooks as text) & shellresult exit repeat end if -- ask for another PID as we're only doing one ebook @@ -311,8 +316,7 @@ on unlockmobifile(encryptedFile) end repeat if DecodingError then set ErrorCount to ErrorCount + 1 - set ErrorList to ErrorList & fileName & fileExtension & " couldn't be decoded: -" & (ErrorText as text) & " + set ErrorList to ErrorList & fileName & fileExtension & " couldn't be decrypted. " else if (offset of "not encrypted" in shellresult) > 0 then set WarningCount to WarningCount + 1 @@ -512,34 +516,66 @@ on unlockepubfile(encryptedFile) set shellresult to "no keys" set ErrorText to "" - -- first we'll try the Barnes & Noble keys - repeat with BNKey in bnKeys - - set keyfilepath to third item of BNKey - if length of keyfilepath > 0 then - set shellcommand to python & (quoted form of BNePubTool) & " " & (quoted form of keyfilepath) & " " & (quoted form of fixedFilePath) & " " & (quoted form of unlockedFilePath) - set shellcommand to shellcommand & " > " & quotedtemppath() - --display dialog "shellcommand: " & shellcommand buttons {"OK"} default button 1 giving up after 10 - writetolog("shellcommand: " & shellcommand) - cleartemp() - set DecodingError to false - set ErrorText to "" - try - do shell script shellcommand - on error ErrorText - set DecodingError to true - end try - set shellresult to readtemp() - writetolog("shellresult: " & shellresult & " " & ErrorText) - --display dialog shellresult - if not DecodingError then - set decoded to "YES" - exit repeat - end if + -- get encryption type + set TryBandNePub to true + set TryAdobeePub to true + set shellcommand to python & (quoted form of ePubTestTool) & " " & (quoted form of fixedFilePath) + set shellcommand to shellcommand & " > " & quotedtemppath() + --display dialog "shellcommand: " & shellcommand buttons {"OK"} default button 1 giving up after 10 + writetolog("shellcommand: " & shellcommand) + cleartemp() + set TestError to false + set ErrorText to "" + try + do shell script shellcommand + on error ErrorText + set TestError to true + end try + set shellresult to readtemp() + writetolog("shellresult: " & shellresult & " " & ErrorText) + --display dialog shellresult + if not TestError then + if (offset of "B&N" in shellresult) > 0 then + set TryAdobeePub to false + else if (offset of "Adobe" in shellresult) > 0 then + set TryBandNePub to false + else if (offset of "Unencrypted" in shellresult) > 0 then + set TryAdobeePub to false + set TryBandNePub to false end if - end repeat + end if - if decoded is "NO" then + + -- first we'll try the Barnes & Noble keys + if TryBandNePub then + repeat with BNKey in bnKeys + + set keyfilepath to third item of BNKey + if length of keyfilepath > 0 then + set shellcommand to python & (quoted form of BNePubTool) & " " & (quoted form of keyfilepath) & " " & (quoted form of fixedFilePath) & " " & (quoted form of unlockedFilePath) + set shellcommand to shellcommand & " > " & quotedtemppath() + --display dialog "shellcommand: " & shellcommand buttons {"OK"} default button 1 giving up after 10 + writetolog("shellcommand: " & shellcommand) + cleartemp() + set DecodingError to false + set ErrorText to "" + try + do shell script shellcommand + on error ErrorText + set DecodingError to true + end try + set shellresult to readtemp() + writetolog("shellresult: " & shellresult & " " & ErrorText) + --display dialog shellresult + if not DecodingError then + set decoded to "YES" + exit repeat + end if + end if + end repeat + end if + + if decoded is "NO" and TryAdobeePub then -- now try Adobe ePub repeat with AdeptKey in AdeptKeyList set shellcommand to python & (quoted form of AdobeePubTool) & " " & (quoted form of AdeptKey) & " " & (quoted form of fixedFilePath) & " " & (quoted form of unlockedFilePath) @@ -567,13 +603,13 @@ on unlockepubfile(encryptedFile) if decoded is "YES" then set CompletedCount to CompletedCount + 1 set CompletedList to CompletedList & fileName & fileExtension & paraend + else if not TryAdobeePub and not TryBandNePub then + set WarningCount to WarningCount + 1 + set WarningList to (WarningList & fileName & " doesn't seem to be encrypted. +") else if shellresult is "no keys" then set ErrorCount to ErrorCount + 1 set ErrorList to (ErrorList & fileName & fileExtension & " couldn't be decoded: no keys. -") - else if (offset of "not an ADEPT EPUB" in shellresult) is not 0 then - set WarningCount to WarningCount + 1 - set WarningList to (WarningList & fileName & " doesn't seem to be encrypted. ") else set ErrorCount to ErrorCount + 1 @@ -942,7 +978,7 @@ Enter any additional Kindle Serial Numbers one at a time:" if button returned of dialogresult is "Add" then set Serial to text returned of dialogresult set Seriallength to length of Serial - if Seriallength is 16 and (first character of Serial) is "B" then + if Seriallength is 16 and ((first character of Serial) is "B" or (first character of Serial) is "9") then set KindleSerialList to KindleSerialList & Serial set Serial to "" else @@ -1528,7 +1564,7 @@ For full information about the licence, please see http://unlicense.org/ The application icon is adapted from the Authors Against DRM logo at http://readersbillofrights.info/AAD and is under the Creative Commons Attribution-ShareAlike licence. The included Python scripts are all free to use, but have a variety of licences. See the individual files for details. -" with title "DeDRM 5.2 by Apprentice Alf" buttons {"Close", "Select Ebook ", "Configure "} default button 1 with icon note +" with title "DeDRM by Apprentice Alf" buttons {"Close", "Select Ebook ", "Configure "} default button 1 with icon note ReadPrefs() clearlog() GetAdeptKey(false) diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist index d73d063..2d8ce5a 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist @@ -24,17 +24,17 @@ CFBundleExecutable droplet CFBundleGetInfoString - DeDRM 5.4.1. AppleScript written 2010–2012 by Apprentice Alf and others. + DeDRM 5.5. AppleScript written 2010–2012 by Apprentice Alf and others. CFBundleIconFile DeDRM CFBundleInfoDictionaryVersion 6.0 CFBundleName - DeDRM 5.4.1 + DeDRM 5.5 CFBundlePackageType APPL CFBundleShortVersionString - 5.4.1 + 5.5 CFBundleSignature dplt LSRequiresCarbon @@ -50,7 +50,7 @@ positionOfDivider 0 savedFrame - 287 405 800 473 0 0 1440 878 + 1846 -16 800 473 1440 -180 1920 1080 selectedTabView event log 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 14dc8b6347edb151e1d4c7e6d353e52167c47faf..1504336a69833e85e9960d9f9354b4ce03f3615f 100644 GIT binary patch literal 268574 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 p>bBUNWz723Vo)gA%YVR!e7D#%ZePEDPDa;P> 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

xH1r$25SzcVPr0Tnl5! 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 D_D;Rg literal 261580 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 E56%$+V*mgE diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/alfcrypto.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/alfcrypto.py index e25a0c8..b1b0606 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/alfcrypto.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/alfcrypto.py @@ -1,11 +1,18 @@ -#! /usr/bin/env python +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# crypto library mainly by some_updates + +# pbkdf2.py pbkdf2 code taken from pbkdf2.py +# pbkdf2.py Copyright © 2004 Matt Johnston +# pbkdf2.py Copyright © 2009 Daniel Holth +# pbkdf2.py This code may be freely used and modified for any purpose. import sys, os import hmac from struct import pack import hashlib - # interface to needed routines libalfcrypto def _load_libalfcrypto(): import ctypes @@ -26,8 +33,8 @@ def _load_libalfcrypto(): name_of_lib = 'libalfcrypto32.so' else: name_of_lib = 'libalfcrypto64.so' - - libalfcrypto = sys.path[0] + os.sep + name_of_lib + + libalfcrypto = os.path.join(sys.path[0],name_of_lib) if not os.path.isfile(libalfcrypto): raise Exception('libalfcrypto not found') @@ -55,7 +62,7 @@ def _load_libalfcrypto(): # # int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key); # - # + # # void AES_cbc_encrypt(const unsigned char *in, unsigned char *out, # const unsigned long length, const AES_KEY *key, # unsigned char *ivec, const int enc); @@ -147,7 +154,7 @@ def _load_libalfcrypto(): topazCryptoDecrypt(ctx, data, out, len(data)) return out.raw - print "Using Library AlfCrypto DLL/DYLIB/SO" + print u"Using Library AlfCrypto DLL/DYLIB/SO" return (AES_CBC, Pukall_Cipher, Topaz_Cipher) @@ -164,8 +171,7 @@ def _load_python_alfcrypto(): sum2 = 0; keyXorVal = 0; if len(key)!=16: - print "Bad key length!" - return None + raise Exception('Pukall_Cipher: Bad key length.') wkey = [] for i in xrange(8): wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1])) @@ -234,6 +240,7 @@ def _load_python_alfcrypto(): cleartext = self.aes.decrypt(iv + data) return cleartext + print u"Using Library AlfCrypto Python" return (AES_CBC, Pukall_Cipher, Topaz_Cipher) diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/config.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/config.py index 9825878..9521540 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/config.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/config.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + from PyQt4.Qt import QWidget, QVBoxLayout, QLabel, QLineEdit from calibre.utils.config import JSONConfig diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/convert2xml.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/convert2xml.py index c412d7b..0f64a1b 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/convert2xml.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/convert2xml.py @@ -230,6 +230,7 @@ class PageParser(object): 'empty' : (1, 'snippets', 1, 0), 'page' : (1, 'snippets', 1, 0), + 'page.class' : (1, 'scalar_text', 0, 0), 'page.pageid' : (1, 'scalar_text', 0, 0), 'page.pagelabel' : (1, 'scalar_text', 0, 0), 'page.type' : (1, 'scalar_text', 0, 0), @@ -238,11 +239,13 @@ class PageParser(object): 'page.startID' : (1, 'scalar_number', 0, 0), 'group' : (1, 'snippets', 1, 0), + 'group.class' : (1, 'scalar_text', 0, 0), 'group.type' : (1, 'scalar_text', 0, 0), 'group._tag' : (1, 'scalar_text', 0, 0), 'group.orientation': (1, 'scalar_text', 0, 0), 'region' : (1, 'snippets', 1, 0), + 'region.class' : (1, 'scalar_text', 0, 0), 'region.type' : (1, 'scalar_text', 0, 0), 'region.x' : (1, 'scalar_number', 0, 0), 'region.y' : (1, 'scalar_number', 0, 0), diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/epubtest.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/epubtest.py new file mode 100644 index 0000000..a44308e --- /dev/null +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/epubtest.py @@ -0,0 +1,169 @@ +#!/usr/bin/python +# +# This is a python script. You need a Python interpreter to run it. +# For example, ActiveState Python, which exists for windows. +# +# Changelog drmcheck +# 1.00 - Initial version, with code from various other scripts +# 1.01 - Moved authorship announcement to usage section. +# +# Changelog drmcheck +# 1.00 - Cut to drmtest.py, testing ePub files only by Apprentice Alf +# +# Written in 2011 by Paul Durrant +# Released with unlicense. See http://unlicense.org/ +# +############################################################################# +# +# This is free and unencumbered software released into the public domain. +# +# Anyone is free to copy, modify, publish, use, compile, sell, or +# distribute this software, either in source code form or as a compiled +# binary, for any purpose, commercial or non-commercial, and by any +# means. +# +# In jurisdictions that recognize copyright laws, the author or authors +# of this software dedicate any and all copyright interest in the +# software to the public domain. We make this dedication for the benefit +# of the public at large and to the detriment of our heirs and +# successors. We intend this dedication to be an overt act of +# relinquishment in perpetuity of all present and future rights to this +# software under copyright law. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +############################################################################# +# +# It's still polite to give attribution if you do reuse this code. +# + +from __future__ import with_statement + +__version__ = '1.00' + +import sys, struct, os +import zlib +import zipfile +import xml.etree.ElementTree as etree + +NSMAP = {'adept': 'http://ns.adobe.com/adept', + 'enc': 'http://www.w3.org/2001/04/xmlenc#'} + +# 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) + +def unicode_argv(): + 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] + +_FILENAME_LEN_OFFSET = 26 +_EXTRA_LEN_OFFSET = 28 +_FILENAME_OFFSET = 30 +_MAX_SIZE = 64 * 1024 + + +def uncompress(cmpdata): + dc = zlib.decompressobj(-15) + data = '' + while len(cmpdata) > 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(file, zi): + # get file name length and exta data length to find start of file data + local_header_offset = zi.header_offset + + file.seek(local_header_offset + _FILENAME_LEN_OFFSET) + leninfo = file.read(2) + local_name_length, = struct.unpack(' 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"mobidedrm.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] + Des = None -if sys.platform.startswith('win'): +if iswindows: # first try with pycrypto if inCalibre: from calibre_plugins.erdrpdb2pml import pycrypto_des @@ -168,17 +221,30 @@ class Sectionizer(object): off = self.sections[section][0] return self.contents[off:end_off] -def sanitizeFileName(s): - r = '' - for c in s: - if c in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-": - r += c - return r +# cleanup unicode filenames +# borrowed from calibre from calibre/src/calibre/__init__.py +# added in removal of control (<32) chars +# and removal of . at start and end +# and with some (heavily edited) code from Paul Durrant's kindlenamer.py +def sanitizeFileName(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"\'") + # 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 + name = re.sub(ur"\s", u" ", name).strip() + # remove leading dots + while len(name)>0 and name[0] == u".": + name = name[1:] + # remove trailing dots (Windows doesn't like them) + if name.endswith(u'.'): + name = name[:-1] + return name def fixKey(key): def fixByte(b): return b ^ ((b ^ (b<<1) ^ (b<<2) ^ (b<<3) ^ (b<<4) ^ (b<<5) ^ (b<<6) ^ (b<<7) ^ 0x80) & 0x80) - return "".join([chr(fixByte(ord(a))) for a in key]) + return "".join([chr(fixByte(ord(a))) for a in key]) def deXOR(text, sp, table): r='' @@ -191,7 +257,7 @@ def deXOR(text, sp, table): return r class EreaderProcessor(object): - def __init__(self, sect, username, creditcard): + def __init__(self, sect, user_key): self.section_reader = sect.loadSection data = self.section_reader(0) version, = struct.unpack('>H', data[0:2]) @@ -212,18 +278,10 @@ class EreaderProcessor(object): for i in xrange(len(data)): j = (j + shuf) % len(data) r[j] = data[i] - assert len("".join(r)) == len(data) + assert len("".join(r)) == len(data) return "".join(r) r = unshuff(input[0:-8], cookie_shuf) - def fixUsername(s): - r = '' - for c in s.lower(): - if (c >= 'a' and c <= 'z' or c >= '0' and c <= '9'): - r += c - return r - - user_key = struct.pack('>LL', binascii.crc32(fixUsername(username)) & 0xffffffff, binascii.crc32(creditcard[-8:])& 0xffffffff) drm_sub_version = struct.unpack('>H', r[0:2])[0] self.num_text_pages = struct.unpack('>H', r[2:4])[0] - 1 self.num_image_pages = struct.unpack('>H', r[26:26+2])[0] @@ -302,7 +360,7 @@ class EreaderProcessor(object): sect = self.section_reader(self.first_image_page + i) name = sect[4:4+32].strip('\0') data = sect[62:] - return sanitizeFileName(name), data + return sanitizeFileName(unicode(name,'windows-1252')), data # def getChapterNamePMLOffsetData(self): @@ -399,60 +457,53 @@ class EreaderProcessor(object): return r def cleanPML(pml): - # Convert special characters to proper PML code. High ASCII start at (\x80, \a128) and go up to (\xff, \a255) + # Convert special characters to proper PML code. High ASCII start at (\x80, \a128) and go up to (\xff, \a255) pml2 = pml for k in xrange(128,256): badChar = chr(k) pml2 = pml2.replace(badChar, '\\a%03d' % k) return pml2 -def convertEreaderToPml(infile, name, cc, outdir): - if not os.path.exists(outdir): - os.makedirs(outdir) +def decryptBook(infile, outpath, make_pmlz, user_key): bookname = os.path.splitext(os.path.basename(infile))[0] - print " Decoding File" - sect = Sectionizer(infile, 'PNRdPPrs') - er = EreaderProcessor(sect, name, cc) - - if er.getNumImages() > 0: - print " Extracting images" - imagedir = bookname + '_img/' - imagedirpath = os.path.join(outdir,imagedir) - if not os.path.exists(imagedirpath): - os.makedirs(imagedirpath) - for i in xrange(er.getNumImages()): - name, contents = er.getImage(i) - file(os.path.join(imagedirpath, name), 'wb').write(contents) - - print " Extracting pml" - pml_string = er.getText() - pmlfilename = bookname + ".pml" - file(os.path.join(outdir, pmlfilename),'wb').write(cleanPML(pml_string)) - - # bkinfo = er.getBookInfo() - # if bkinfo != '': - # print " Extracting book meta information" - # file(os.path.join(outdir, 'bookinfo.txt'),'wb').write(bkinfo) - - - -def decryptBook(infile, outdir, name, cc, make_pmlz): - if make_pmlz : - # ignore specified outdir, use tempdir instead + if make_pmlz: + # outpath is actually pmlz name + pmlzname = outpath outdir = tempfile.mkdtemp() + imagedirpath = os.path.join(outdir,u"images") + else: + pmlzname = None + outdir = outpath + imagedirpath = os.path.join(outdir,bookname + u"_img") + try: - print "Processing..." - convertEreaderToPml(infile, name, cc, outdir) - if make_pmlz : + if not os.path.exists(outdir): + os.makedirs(outdir) + print u"Decoding File" + sect = Sectionizer(infile, 'PNRdPPrs') + er = EreaderProcessor(sect, user_key) + + if er.getNumImages() > 0: + print u"Extracting images" + if not os.path.exists(imagedirpath): + os.makedirs(imagedirpath) + for i in xrange(er.getNumImages()): + name, contents = er.getImage(i) + file(os.path.join(imagedirpath, name), 'wb').write(contents) + + print u"Extracting pml" + pml_string = er.getText() + pmlfilename = bookname + ".pml" + file(os.path.join(outdir, pmlfilename),'wb').write(cleanPML(pml_string)) + if pmlzname is not None: import zipfile import shutil - print " Creating PMLZ file" - zipname = infile[:-4] + '.pmlz' - myZipFile = zipfile.ZipFile(zipname,'w',zipfile.ZIP_STORED, False) + print u"Creating PMLZ file {0}".format(os.path.basename(pmlzname)) + myZipFile = zipfile.ZipFile(pmlzname,'w',zipfile.ZIP_STORED, False) list = os.listdir(outdir) - for file in list: - localname = file - filePath = os.path.join(outdir,file) + for filename in list: + localname = filename + filePath = os.path.join(outdir,filename) if os.path.isfile(filePath): myZipFile.write(filePath, localname) elif os.path.isdir(filePath): @@ -466,36 +517,46 @@ def decryptBook(infile, outdir, name, cc, make_pmlz): myZipFile.close() # remove temporary directory shutil.rmtree(outdir, True) - print 'output is %s' % zipname + print u"Output is {0}".format(pmlzname) else : - print 'output in %s' % outdir + print u"Output is in {0}".format(outdir) print "done" except ValueError, e: - print "Error: %s" % e + print u"Error: {0}".format(e.args[0]) return 1 return 0 def usage(): - print "Converts DRMed eReader books to PML Source" - print "Usage:" - print " erdr2pml [options] infile.pdb [outdir] \"your name\" credit_card_number " - print " " - print "Options: " - print " -h prints this message" - print " --make-pmlz create PMLZ instead of using output directory" - print " " - print "Note:" - print " if ommitted, outdir defaults based on 'infile.pdb'" - print " It's enough to enter the last 8 digits of the credit card number" + print u"Converts DRMed eReader books to PML Source" + print u"Usage:" + print u" erdr2pml [options] infile.pdb [outpath] \"your name\" credit_card_number" + print u" " + print u"Options: " + print u" -h prints this message" + print u" -p create PMLZ instead of source folder" + print u" --make-pmlz create PMLZ instead of source folder" + print u" " + print u"Note:" + print u" if outpath is ommitted, creates source in 'infile_Source' folder" + print u" if outpath is ommitted and pmlz option, creates PMLZ 'infile.pmlz'" + print u" if source folder created, images are in infile_img folder" + print u" if pmlz file created, images are in images folder" + print u" It's enough to enter the last 8 digits of the credit card number" return +def getuser_key(name,cc): + newname = "".join(c for c in name.lower() if c >= 'a' and c <= 'z' or c >= '0' and c <= '9') + cc = cc.replace(" ","") + return struct.pack('>LL', binascii.crc32(newname) & 0xffffffff,binascii.crc32(cc[-8:])& 0xffffffff) + +def cli_main(argv=unicode_argv()): + print u"eRdr2Pml v{0}. Copyright © 2009–2012 The Dark Reverser et al.".format(__version__) -def main(argv=None): try: - opts, args = getopt.getopt(sys.argv[1:], "h", ["make-pmlz"]) + opts, args = getopt.getopt(argv[1:], "hp", ["make-pmlz"]) except getopt.GetoptError, err: - print str(err) + print err.args[0] usage() return 1 make_pmlz = False @@ -503,24 +564,31 @@ def main(argv=None): if o == "-h": usage() return 0 + elif o == "-p": + make_pmlz = True elif o == "--make-pmlz": make_pmlz = True - print "eRdr2Pml v%s. Copyright (c) 2009 The Dark Reverser" % __version__ - if len(args)!=3 and len(args)!=4: usage() return 1 if len(args)==3: - infile, name, cc = args[0], args[1], args[2] - outdir = infile[:-4] + '_Source' + infile, name, cc = args + if make_pmlz: + outpath = os.path.splitext(infile)[0] + u".pmlz" + else: + outpath = os.path.splitext(infile)[0] + u"_Source" elif len(args)==4: - infile, outdir, name, cc = args[0], args[1], args[2], args[3] + infile, outpath, name, cc = args - return decryptBook(infile, outdir, name, cc, make_pmlz) + print getuser_key(name,cc).encode('hex') + + return decryptBook(infile, outpath, make_pmlz, getuser_key(name,cc)) if __name__ == "__main__": - sys.stdout=Unbuffered(sys.stdout) - sys.exit(main()) + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) + sys.exit(cli_main()) + diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignobleepub.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignobleepub.py index 03aa91f..2e0bd06 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignobleepub.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignobleepub.py @@ -1,13 +1,25 @@ -#! /usr/bin/python +#!/usr/bin/env python +# -*- coding: utf-8 -*- from __future__ import with_statement -# ignobleepub.pyw, version 3.5 +# ignobleepub.pyw, version 3.6 +# Copyright © 2009-2010 by i♥cabbages -# To run this program install Python 2.6 from -# and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto -# (make sure to install the version for Python 2.6). Save this script file as -# ignobleepub.pyw and double-click on it to run it. +# 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 +# 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. # Revision history: # 1 - Initial release @@ -18,21 +30,83 @@ from __future__ import with_statement # 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 +""" +Decrypt Barnes & Noble encrypted ePub books. +""" __license__ = 'GPL v3' +__version__ = "3.6" 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 -import Tkinter -import Tkconstants -import tkFileDialog -import tkMessageBox + +# 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') + +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)] + 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] + class IGNOBLEError(Exception): pass @@ -42,10 +116,11 @@ def _load_crypto_libcrypto(): Structure, c_ulong, create_string_buffer, cast from ctypes.util import find_library - if sys.platform.startswith('win'): + if iswindows: libcrypto = find_library('libeay32') else: libcrypto = find_library('crypto') + if libcrypto is None: raise IGNOBLEError('libcrypto not found') libcrypto = CDLL(libcrypto) @@ -66,9 +141,6 @@ def _load_crypto_libcrypto(): func.argtypes = argtypes return func - AES_cbc_encrypt = F(None, 'AES_cbc_encrypt', - [c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p, - c_int]) AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key', [c_char_p, c_int, AES_KEY_p]) AES_cbc_encrypt = F(None, 'AES_cbc_encrypt', @@ -123,13 +195,6 @@ def _load_crypto(): AES = _load_crypto() - - -""" -Decrypt Barnes & Noble ADEPT encrypted EPUB books. -""" - - 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#'} @@ -144,7 +209,6 @@ class ZipInfo(zipfile.ZipInfo): class Decryptor(object): def __init__(self, bookkey, encryption): enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag) - # self._aes = AES.new(bookkey, AES.MODE_CBC, '\x00'*16) self._aes = AES(bookkey) encryption = etree.fromstring(encryption) self._encrypted = encrypted = set() @@ -152,8 +216,8 @@ class Decryptor(object): enc('CipherReference')) for elem in encryption.findall(expr): path = elem.get('URI', None) - path = path.encode('utf-8') if path is not None: + path = path.encode('utf-8') encrypted.add(path) def decompress(self, bytes): @@ -171,167 +235,186 @@ class Decryptor(object): data = self.decompress(data) return data - -class DecryptionDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - self.status = Tkinter.Label(self, text='Select files for decryption') - self.status.pack(fill=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='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('bnepubkey.b64'): - self.keypath.insert(0, 'bnepubkey.b64') - button = Tkinter.Button(body, text="...", command=self.get_keypath) - button.grid(row=0, column=2) - Tkinter.Label(body, text='Input file').grid(row=1) - self.inpath = Tkinter.Entry(body, width=30) - self.inpath.grid(row=1, column=1, sticky=sticky) - button = Tkinter.Button(body, text="...", command=self.get_inpath) - button.grid(row=1, column=2) - Tkinter.Label(body, text='Output file').grid(row=2) - self.outpath = Tkinter.Entry(body, width=30) - self.outpath.grid(row=2, column=1, sticky=sticky) - button = Tkinter.Button(body, text="...", command=self.get_outpath) - button.grid(row=2, column=2) - buttons = Tkinter.Frame(self) - buttons.pack() - botton = Tkinter.Button( - buttons, text="Decrypt", width=10, command=self.decrypt) - botton.pack(side=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 B&N EPUB key file', - defaultextension='.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='Select B&N-encrypted EPUB file to decrypt', - defaultextension='.epub', filetypes=[('EPUB files', '.epub'), - ('All files', '.*')]) - 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='Select unencrypted EPUB file to produce', - defaultextension='.epub', filetypes=[('EPUB files', '.epub'), - ('All files', '.*')]) - 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'] = 'Specified key file does not exist' - return - if not inpath or not os.path.exists(inpath): - self.status['text'] = 'Specified input file does not exist' - return - if not outpath: - self.status['text'] = 'Output file not specified' - return - if inpath == outpath: - self.status['text'] = 'Must have different input and output files' - return - argv = [sys.argv[0], keypath, inpath, outpath] - self.status['text'] = 'Decrypting...' - try: - cli_main(argv) - except Exception, e: - self.status['text'] = 'Error: ' + str(e) - return - self.status['text'] = 'File successfully decrypted' - - -def decryptBook(keypath, inpath, outpath): - with open(keypath, 'rb') as f: - keyb64 = f.read() - key = keyb64.decode('base64')[:16] - # aes = AES.new(key, AES.MODE_CBC, '\x00'*16) - aes = AES(key) - +# 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: - raise IGNOBLEError('%s: not an B&N ADEPT EPUB' % (inpath,)) + 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) - 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)) - 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)) - return 0 + 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=sys.argv): +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 "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,) + print u"usage: {0} ".format(progname) return 1 keypath, inpath, outpath = argv[1:] - return decryptBook(keypath, inpath, outpath) - + 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() - if AES is None: - root.withdraw() - tkMessageBox.showerror( - "Ignoble EPUB Decrypter", - "This script requires OpenSSL or PyCrypto, which must be installed " - "separately. Read the top-of-script comment for details.") - return 1 - root.title('Ignoble EPUB Decrypter') + 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/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignoblekeygen.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignoblekeygen.py index e2c50e2..f25359c 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignoblekeygen.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignoblekeygen.py @@ -1,13 +1,25 @@ -#! /usr/bin/python +#!/usr/bin/env python +# -*- coding: utf-8 -*- from __future__ import with_statement -# ignoblekeygen.pyw, version 2.4 +# ignoblekeygen.pyw, version 2.5 +# Copyright © 2009-2010 by i♥cabbages -# To run this program install Python 2.6 from -# and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto -# (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. +# 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 @@ -16,36 +28,92 @@ from __future__ import with_statement # 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] -# use openssl's libcrypt if it exists in place of pycrypto -# code extracted from the Adobe Adept DRM removal code also by I HeartCabbages 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 sys.platform.startswith('win'): + if iswindows: libcrypto = find_library('libeay32') else: libcrypto = find_library('crypto') + if libcrypto is None: - print 'libcrypto not found' raise IGNOBLEError('libcrypto not found') libcrypto = CDLL(libcrypto) @@ -70,6 +138,7 @@ def _load_crypto_libcrypto(): 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) @@ -88,7 +157,6 @@ def _load_crypto_libcrypto(): return AES - def _load_crypto_pycrypto(): from Crypto.Cipher import AES as _AES @@ -120,25 +188,28 @@ def normalize_name(name): return ''.join(x for x in name.lower() if x != ' ') -def generate_keyfile(name, ccn, outpath): +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() - with open(outpath, 'wb') as f: - f.write(userkey.encode('base64')) - return userkey + return userkey.encode('base64') -def cli_main(argv=sys.argv): +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 " \ @@ -146,10 +217,11 @@ def cli_main(argv=sys.argv): (progname,) return 1 if len(argv) != 4: - print "usage: %s NAME CC# OUTFILE" % (progname,) + print u"usage: {0} ".format(progname) return 1 - name, ccn, outpath = argv[1:] - generate_keyfile(name, ccn, outpath) + name, ccn, keypath = argv[1:] + userkey = generate_key(name, ccn) + open(keypath,'wb').write(userkey) return 0 @@ -162,38 +234,38 @@ def gui_main(): class DecryptionDialog(Tkinter.Frame): def __init__(self, root): Tkinter.Frame.__init__(self, root, border=5) - self.status = Tkinter.Label(self, text='Enter parameters') + 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='Account Name').grid(row=0) + 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='CC#').grid(row=1) + 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='Output file').grid(row=2) + 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, 'bnepubkey.b64') - button = Tkinter.Button(body, text="...", command=self.get_keypath) + 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="Generate", width=10, command=self.generate) + 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="Quit", width=10, command=self.quit) + buttons, text=u"Quit", width=10, command=self.quit) button.pack(side=Tkconstants.RIGHT) - + def get_keypath(self): keypath = tkFileDialog.asksaveasfilename( - parent=None, title='Select B&N EPUB key file to produce', - defaultextension='.b64', + parent=None, title=u"Select B&N ePub key file to produce", + defaultextension=u".b64", filetypes=[('base64-encoded files', '.b64'), ('All Files', '.*')]) if keypath: @@ -201,27 +273,28 @@ def gui_main(): 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'] = 'Name not specified' + self.status['text'] = u"Name not specified" return if not ccn: - self.status['text'] = 'Credit card number not specified' + self.status['text'] = u"Credit card number not specified" return if not keypath: - self.status['text'] = 'Output keyfile path not specified' + self.status['text'] = u"Output keyfile path not specified" return - self.status['text'] = 'Generating...' + self.status['text'] = u"Generating..." try: - generate_keyfile(name, ccn, keypath) + userkey = generate_key(name, ccn) except Exception, e: - self.status['text'] = 'Error: ' + str(e) + self.status['text'] = u"Error: (0}".format(e.args[0]) return - self.status['text'] = 'Keyfile successfully generated' + open(keypath,'wb').write(userkey) + self.status['text'] = u"Keyfile successfully generated" root = Tkinter.Tk() if AES is None: @@ -231,7 +304,7 @@ def gui_main(): "This script requires OpenSSL or PyCrypto, which must be installed " "separately. Read the top-of-script comment for details.") return 1 - root.title('Ignoble EPUB Keyfile Generator') + 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) @@ -240,5 +313,7 @@ def gui_main(): 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/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptepub.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptepub.py index 2bb32b1..4b5a296 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptepub.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptepub.py @@ -3,11 +3,13 @@ from __future__ import with_statement -# ineptepub.pyw, version 5.6 -# Copyright © 2009-2010 i♥cabbages +# ineptepub.pyw, version 5.8 +# Copyright © 2009-2010 by i♥cabbages -# 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 +# + +# 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 @@ -31,24 +33,83 @@ from __future__ import with_statement # 5.5 - On Windows try PyCrypto first, OpenSSL next # 5.6 - Modify interface to allow use with import # 5.7 - Fix for potential problem with PyCrypto +# 5.8 - Revised to allow use in calibre plugins to eliminate need for duplicate code """ -Decrypt Adobe ADEPT-encrypted EPUB books. +Decrypt Adobe Digital Editions encrypted ePub books. """ __license__ = 'GPL v3' +__version__ = "5.8" 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 -import Tkinter -import Tkconstants -import tkFileDialog -import tkMessageBox + +# 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') + +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)] + 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] + class ADEPTError(Exception): pass @@ -58,7 +119,7 @@ def _load_crypto_libcrypto(): Structure, c_ulong, create_string_buffer, cast from ctypes.util import find_library - if sys.platform.startswith('win'): + if iswindows: libcrypto = find_library('libeay32') else: libcrypto = find_library('crypto') @@ -272,6 +333,7 @@ def _load_crypto(): except (ImportError, ADEPTError): pass return (AES, RSA) + AES, RSA = _load_crypto() META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml') @@ -314,158 +376,181 @@ class Decryptor(object): data = self.decompress(data) return data - -class DecryptionDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - self.status = Tkinter.Label(self, text='Select files for decryption') - self.status.pack(fill=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='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) - 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(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-encrypted EPUB file to decrypt', - defaultextension='.epub', filetypes=[('EPUB files', '.epub'), - ('All files', '.*')]) - 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='Select unencrypted EPUB file to produce', - defaultextension='.epub', filetypes=[('EPUB files', '.epub'), - ('All files', '.*')]) - 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'] = 'Specified key file does not exist' - return - if not inpath or not os.path.exists(inpath): - self.status['text'] = 'Specified input file does not exist' - return - if not outpath: - self.status['text'] = 'Output file not specified' - return - if inpath == outpath: - self.status['text'] = 'Must have different input and output files' - return - argv = [sys.argv[0], keypath, inpath, outpath] - self.status['text'] = 'Decrypting...' - try: - cli_main(argv) - except Exception, e: - self.status['text'] = 'Error: ' + str(e) - return - self.status['text'] = 'File successfully decrypted' - - -def decryptBook(keypath, inpath, outpath): - with open(keypath, 'rb') as f: - keyder = f.read() - rsa = RSA(keyder) +# check file to make check whether it's probably an Adobe Adept encrypted ePub +def adeptBook(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: - raise ADEPTError('%s: not an ADEPT EPUB' % (inpath,)) + 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) == 172: + return True + except: + # if we couldn't check, assume it is + return True + return False + +def decryptBook(userkey, inpath, outpath): + if AES is None: + raise ADEPTError(u"PyCrypto or OpenSSL must be installed.") + rsa = RSA(userkey) + 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) - 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)) - bookkey = rsa.decrypt(bookkey.decode('base64')) - # Padded as per RSAES-PKCS1-v1_5 - if bookkey[-17] != '\x00': - raise ADEPTError('problem decrypting session key') - 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)) + 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) != 172: + print u"{0:s} is not a secure Adobe Adept ePub.".format(os.path.basename(inpath)) + return 1 + bookkey = rsa.decrypt(bookkey.decode('base64')) + # Padded as per RSAES-PKCS1-v1_5 + if bookkey[-17] != '\x00': + print u"Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath)) + return 2 + 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=sys.argv): +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 "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,) + print u"usage: {0} ".format(progname) return 1 keypath, inpath, outpath = argv[1:] - return decryptBook(keypath, inpath, outpath) - + 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 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"adeptkey.der"): + self.keypath.insert(0, u"adeptkey.der") + 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 Adobe Adept \'.der\' key file", + defaultextension=u".der", + filetypes=[('Adobe Adept DER-encoded files', '.der'), + ('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 ADEPT-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) + return + 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( - "INEPT EPUB Decrypter", - "This script requires OpenSSL or PyCrypto, which must be" - " installed separately. Read the top-of-script comment for" - " details.") - return 1 - root.title('INEPT EPUB Decrypter') + root.title(u"Adobe Adept ePub Decrypter v.{0}".format(__version__)) root.resizable(True, False) root.minsize(300, 0) DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1) @@ -474,5 +559,7 @@ def gui_main(): 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/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptkey.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptkey.py index 723b7c6..a9bc62d 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptkey.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptkey.py @@ -6,8 +6,8 @@ from __future__ import with_statement # ineptkey.pyw, version 5.6 # Copyright © 2009-2010 i♥cabbages -# 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 +# # Windows users: Before running this program, you must first install Python 2.6 # from and PyCrypto from @@ -37,7 +37,7 @@ from __future__ import with_statement # 5.3 - On Windows try PyCrypto first, OpenSSL next # 5.4 - Modify interface to allow use of import # 5.5 - Fix for potential problem with PyCrypto -# 5.6 - Revise to allow use in Plugins to eliminate need for duplicate code +# 5.6 - Revised to allow use in Plugins to eliminate need for duplicate code """ Retrieve Adobe ADEPT user key. @@ -49,12 +49,65 @@ import sys import os import struct +# 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') +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)] + return [u"ineptkey.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 ADEPTError(Exception): pass @@ -80,13 +133,13 @@ if iswindows: _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_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', @@ -308,9 +361,9 @@ if iswindows: cuser = winreg.HKEY_CURRENT_USER try: regkey = winreg.OpenKey(cuser, DEVICE_KEY_PATH) + device = winreg.QueryValueEx(regkey, 'key')[0] except WindowsError: raise ADEPTError("Adobe Digital Editions not activated") - device = winreg.QueryValueEx(regkey, 'key')[0] keykey = CryptUnprotectData(device, entropy) userkey = None keys = [] @@ -343,7 +396,7 @@ if iswindows: if len(keys) == 0: raise ADEPTError('Could not locate privateLicenseKey') return keys - + elif isosx: import xml.etree.ElementTree as etree @@ -386,7 +439,7 @@ else: def retrieve_keys(keypath): raise ADEPTError("This script only supports Windows and Mac OS X.") return [] - + def retrieve_key(keypath): keys = retrieve_keys() with open(keypath, 'wb') as f: @@ -397,22 +450,22 @@ def extractKeyfile(keypath): try: success = retrieve_key(keypath) except ADEPTError, e: - print "Key generation Error: " + str(e) + print u"Key generation Error: {0}".format(e.args[0]) return 1 except Exception, e: - print "General Error: " + str(e) + print "General Error: {0}".format(e.args[0]) return 1 if not success: return 1 return 0 -def cli_main(argv=sys.argv): +def cli_main(argv=unicode_argv()): keypath = argv[1] return extractKeyfile(keypath) -def main(argv=sys.argv): +def gui_main(argv=unicode_argv()): import Tkinter import Tkconstants import tkMessageBox @@ -421,24 +474,24 @@ def main(argv=sys.argv): class ExceptionDialog(Tkinter.Frame): def __init__(self, root, text): Tkinter.Frame.__init__(self, root, border=5) - label = Tkinter.Label(self, text="Unexpected error:", + label = Tkinter.Label(self, text=u"Unexpected error:", anchor=Tkconstants.W, justify=Tkconstants.LEFT) label.pack(fill=Tkconstants.X, expand=0) self.text = Tkinter.Text(self) self.text.pack(fill=Tkconstants.BOTH, expand=1) - + self.text.insert(Tkconstants.END, text) root = Tkinter.Tk() root.withdraw() - progname = os.path.basename(argv[0]) - keypath = os.path.abspath("adeptkey.der") + keypath, progname = os.path.split(argv[0]) + keypath = os.path.join(keypath, u"adeptkey.der") success = False try: success = retrieve_key(keypath) except ADEPTError, e: - tkMessageBox.showerror("ADEPT Key", "Error: " + str(e)) + tkMessageBox.showerror(u"ADEPT Key", "Error: {0}".format(e.args[0])) except Exception: root.wm_state('normal') root.title('ADEPT Key') @@ -448,10 +501,12 @@ def main(argv=sys.argv): if not success: return 1 tkMessageBox.showinfo( - "ADEPT Key", "Key successfully retrieved to %s" % (keypath)) + u"ADEPT Key", u"Key successfully retrieved to {0}".format(keypath)) 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(main()) + sys.exit(gui_main()) diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptpdf.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptpdf.py index 20721d1..9f4883e 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptpdf.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptpdf.py @@ -1,13 +1,25 @@ -#! /usr/bin/env python -# ineptpdf.pyw, version 7.11 +#! /usr/bin/python +# -*- coding: utf-8 -*- from __future__ import with_statement -# To run this program install Python 2.6 from http://www.python.org/download/ -# and OpenSSL (already installed on Mac OS X and Linux) OR -# PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto -# (make sure to install the version for Python 2.6). Save this script file as -# ineptpdf.pyw and double-click on it to run it. +# ineptpdf.pyw, version 7.11 +# 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 +# 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. # Revision history: # 1 - Initial release @@ -36,12 +48,14 @@ from __future__ import with_statement # 7.9 - Bug fix for some session key errors when len(bookkey) > length required # 7.10 - Various tweaks to fix minor problems. # 7.11 - More tweaks to fix minor problems. +# 7.12 - Revised to allow use in calibre plugins to eliminate need for duplicate code """ Decrypts Adobe ADEPT-encrypted PDF files. """ __license__ = 'GPL v3' +__version__ = "7.12" import sys import os @@ -51,10 +65,63 @@ import struct import hashlib from itertools import chain, islice import xml.etree.ElementTree as etree -import Tkinter -import Tkconstants -import tkFileDialog -import tkMessageBox + +# 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)] + 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] + class ADEPTError(Exception): pass @@ -1520,9 +1587,7 @@ class PDFDocument(object): def initialize_ebx(self, password, docid, param): self.is_printable = self.is_modifiable = self.is_extractable = True - with open(password, 'rb') as f: - keyder = f.read() - rsa = RSA(keyder) + rsa = RSA(password) length = int_value(param.get('Length', 0)) / 8 rights = str_value(param.get('ADEPT_LICENSE')).decode('base64') rights = zlib.decompress(rights, -15) @@ -1907,14 +1972,14 @@ class PDFObjStrmParser(PDFParser): ### My own code, for which there is none else to blame class PDFSerializer(object): - def __init__(self, inf, keypath): + def __init__(self, inf, userkey): global GEN_XREF_STM, gen_xref_stm gen_xref_stm = GEN_XREF_STM > 1 self.version = inf.read(8) inf.seek(0) self.doc = doc = PDFDocument() parser = PDFParser(doc, inf) - doc.initialize(keypath) + doc.initialize(userkey) self.objids = objids = set() for xref in reversed(doc.xrefs): trailer = xref.trailer @@ -2097,142 +2162,144 @@ class PDFSerializer(object): self.write('endobj\n') -class DecryptionDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - ltext='Select file for decryption\n' - 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='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) - 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 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 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): - 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 - self.status['text'] = 'Specified Adept key file does not exist' - return - if not inpath or not os.path.exists(inpath): - self.status['text'] = 'Specified input file does not exist' - return - if not outpath: - self.status['text'] = 'Output file not specified' - return - if inpath == outpath: - self.status['text'] = 'Must have different input and output files' - return - # patch for non-ascii characters - 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 decryptBook(keypath, inpath, outpath): +def decryptBook(userkey, inpath, outpath): + if RSA is None: + raise ADEPTError(u"PyCrypto or OpenSSL must be installed.") with open(inpath, 'rb') as inf: try: - serializer = PDFSerializer(inf, keypath) + serializer = PDFSerializer(inf, userkey) except: - print "Error serializing pdf. Probably wrong key." - return 1 + print u"Error serializing pdf {0}. Probably wrong key.".format(os.path.basename(inpath)) + return 2 # 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 + # help construct to make sure the method runs to the end try: serializer.dump(outf) - except: - print "error writing pdf." - return 1 + except Exception, e: + print u"error writing pdf: {0}".format(e.args[0]) + return 2 return 0 -def cli_main(argv=sys.argv): +def cli_main(argv=unicode_argv()): progname = os.path.basename(argv[0]) - if RSA 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 "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,) + print u"usage: {0} ".format(progname) return 1 keypath, inpath, outpath = argv[1:] - return decryptBook(keypath, inpath, outpath) + 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 + + 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"adeptkey.der"): + self.keypath.insert(0, u"adeptkey.der") + 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 Adobe Adept \'.der\' key file", + defaultextension=u".der", + filetypes=[('Adobe Adept DER-encoded files', '.der'), + ('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 ADEPT-encrypted PDF file to decrypt", + defaultextension=u".pdf", filetypes=[('PDF files', '.pdf')]) + 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 PDF file to produce", + defaultextension=u".pdf", filetypes=[('PDF files', '.pdf')]) + 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: + self.status['text'] = u"File successfully decrypted" + else: + self.status['text'] = u"The was an error decrypting the file." + + root = Tkinter.Tk() if RSA is None: root.withdraw() @@ -2241,7 +2308,7 @@ def gui_main(): "This script requires OpenSSL or PyCrypto, which must be installed " "separately. Read the top-of-script comment for details.") return 1 - root.title('INEPT PDF Decrypter') + root.title(u"Adobe Adept PDF Decrypter v.{0}".format(__version__)) root.resizable(True, False) root.minsize(370, 0) DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1) @@ -2251,5 +2318,7 @@ def gui_main(): 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/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py index 717b0d0..8adb107 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py @@ -1,7 +1,11 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- from __future__ import with_statement +# ignobleepub.pyw, version 3.6 +# Copyright © 2009-2012 by DiapDealer et al. + # engine to remove drm from Kindle for Mac and Kindle for PC books # for personal use for archiving and converting your ebooks @@ -12,30 +16,51 @@ from __future__ import with_statement # be able to read OUR books on whatever device we want and to keep # readable for a long, long time -# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle, +# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle, # unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates # and many many others +# Special thanks to The Dark Reverser for MobiDeDrm and CMBDTC for cmbdtc_dump +# from which this script borrows most unashamedly. -__version__ = '4.4' +# Changelog +# 1.0 - Name change to k4mobidedrm. Adds Mac support, Adds plugin code +# 1.1 - Adds support for additional kindle.info files +# 1.2 - Better error handling for older Mobipocket +# 1.3 - Don't try to decrypt Topaz books +# 1.7 - Add support for Topaz books and Kindle serial numbers. Split code. +# 1.9 - Tidy up after Topaz, minor exception changes +# 2.1 - Topaz fix and filename sanitizing +# 2.2 - Topaz Fix and minor Mac code fix +# 2.3 - More Topaz fixes +# 2.4 - K4PC/Mac key generation fix +# 2.6 - Better handling of non-K4PC/Mac ebooks +# 2.7 - Better trailing bytes handling in mobidedrm +# 2.8 - Moved parsing of kindle.info files to mac & pc util files. +# 3.1 - Updated for new calibre interface. Now __init__ in plugin. +# 3.5 - Now support Kindle for PC/Mac 1.6 +# 3.6 - Even better trailing bytes handling in mobidedrm +# 3.7 - Add support for Amazon Print Replica ebooks. +# 3.8 - Improved Topaz support +# 4.1 - Improved Topaz support and faster decryption with alfcrypto +# 4.2 - Added support for Amazon's KF8 format ebooks +# 4.4 - Linux calls to Wine added, and improved configuration dialog +# 4.5 - Linux works again without Wine. Some Mac key file search changes +# 4.6 - First attempt to handle unicode properly +# 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. -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) +__version__ = '4.8' -import sys -import os, csv, getopt -import string + +import sys, os, re +import csv +import getopt import re import traceback import time - -buildXML = False +import htmlentitydefs class DrmException(Exception): pass @@ -54,161 +79,203 @@ else: import topazextract import kgenpids +# 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) -# cleanup bytestring filenames +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"mobidedrm.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] + +# cleanup unicode filenames # borrowed from calibre from calibre/src/calibre/__init__.py -# added in removal of non-printing chars -# and removal of . at start -# convert underscores to spaces (we're OK with spaces in file names) +# added in removal of control (<32) chars +# and removal of . at start and end +# and with some (heavily edited) code from Paul Durrant's kindlenamer.py def cleanup_name(name): - _filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+/]') - substitute='_' - one = ''.join(char for char in name if char in string.printable) - one = _filename_sanitize.sub(substitute, one) - one = re.sub(r'\s', ' ', one).strip() - one = re.sub(r'^\.+$', '_', one) - one = one.replace('..', substitute) - # Windows doesn't like path components that end with a period - if one.endswith('.'): - one = one[:-1]+substitute - # Mac and Unix don't like file names that begin with a full stop - if len(one) > 0 and one[0] == '.': - one = substitute+one[1:] - one = one.replace('_',' ') - return one - -def decryptBook(infile, outdir, k4, kInfoFiles, serials, pids): - global buildXML + # 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"\'") + # 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 + name = re.sub(ur"\s", u" ", name).strip() + # remove leading dots + while len(name)>0 and name[0] == u".": + name = name[1:] + # remove trailing dots (Windows doesn't like them) + if name.endswith(u'.'): + name = name[:-1] + return name +# must be passed unicode +def unescape(text): + def fixup(m): + text = m.group(0) + if text[:2] == u"&#": + # character reference + try: + if text[:3] == u"&#x": + return unichr(int(text[3:-1], 16)) + else: + return unichr(int(text[2:-1])) + except ValueError: + pass + else: + # named entity + try: + text = unichr(htmlentitydefs.name2codepoint[text[1:-1]]) + except KeyError: + pass + return text # leave as is + return re.sub(u"&#?\w+;", fixup, text) +def GetDecryptedBook(infile, kInfoFiles, serials, pids, starttime = time.time()): # handle the obvious cases at the beginning if not os.path.isfile(infile): - print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: Input file does not exist" - return 1 - - starttime = time.time() - print "Starting decryptBook routine." - + raise DRMException (u"Input file does not exist.") mobi = True magic3 = file(infile,'rb').read(3) if magic3 == 'TPZ': mobi = False - bookname = os.path.splitext(os.path.basename(infile))[0] - if mobi: mb = mobidedrm.MobiBook(infile) else: mb = topazextract.TopazBook(infile) - title = mb.getBookTitle() - print "Processing Book: ", title - filenametitle = cleanup_name(title) - outfilename = cleanup_name(bookname) + bookname = unescape(mb.getBookTitle()) + print u"Decrypting {1} ebook: {0}".format(bookname, mb.getBookType()) - # generate 'sensible' filename, that will sort with the original name, - # but is close to the name from the file. - outlength = len(outfilename) - comparelength = min(8,min(outlength,len(filenametitle))) - copylength = min(max(outfilename.find(' '),8),len(outfilename)) - if outlength==0: - outfilename = filenametitle - elif comparelength > 0: - if outfilename[:comparelength] == filenametitle[:comparelength]: - outfilename = filenametitle - else: - outfilename = outfilename[:copylength] + " " + filenametitle + # extend PID list with book-specific PIDs + md1, md2 = mb.getPIDMetaInfo() + pids.extend(kgenpids.getPidList(md1, md2, serials, kInfoFiles)) + print u"Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(pids)) + + try: + mb.processBook(pids) + except: + mb.cleanup + raise + + print u"Decryption succeeded after {0:.1f} seconds".format(time.time()-starttime) + return mb + + +# infile, outdir and kInfoFiles should be unicode strings +def decryptBook(infile, outdir, kInfoFiles, serials, pids): + starttime = time.time() + print "Starting decryptBook routine." + try: + 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) + return 1 + + # if we're saving to the same folder as the original, use file name_ + # if to a different folder, use book name + if os.path.normcase(os.path.normpath(outdir)) == os.path.normcase(os.path.normpath(os.path.dirname(infile))): + outfilename = os.path.splitext(os.path.basename(infile))[0] + else: + outfilename = cleanup_name(book.getBookTitle()) # avoid excessively long file names if len(outfilename)>150: outfilename = outfilename[:150] - # build pid list - md1, md2 = mb.getPIDMetaInfo() - pids.extend(kgenpids.getPidList(md1, md2, k4, serials, kInfoFiles)) + outfilename = outfilename+u"_nodrm" + outfile = os.path.join(outdir, outfilename + book.getBookExtension()) - print "Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(pids)) + book.getFile(outfile) + print u"Saved decrypted book {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename) - - try: - mb.processBook(pids) - - except mobidedrm.DrmException, e: - print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: " + str(e) + "\nDRM Removal Failed.\n" - print "Failed to decrypted book after {0:.1f} seconds".format(time.time()-starttime) - return 1 - except topazextract.TpzDRMError, e: - print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: " + str(e) + "\nDRM Removal Failed.\n" - print "Failed to decrypted book after {0:.1f} seconds".format(time.time()-starttime) - return 1 - except Exception, e: - print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: " + str(e) + "\nDRM Removal Failed.\n" - print "Failed to decrypted book after {0:.1f} seconds".format(time.time()-starttime) - return 1 - - print "Successfully decrypted book after {0:.1f} seconds".format(time.time()-starttime) - - if mobi: - if mb.getPrintReplica(): - outfile = os.path.join(outdir, outfilename + '_nodrm' + '.azw4') - elif mb.getMobiVersion() >= 8: - outfile = os.path.join(outdir, outfilename + '_nodrm' + '.azw3') - else: - outfile = os.path.join(outdir, outfilename + '_nodrm' + '.mobi') - mb.getMobiFile(outfile) - print "Saved decrypted book {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename + '_nodrm') - return 0 - - # topaz: - print " Creating NoDRM HTMLZ Archive" - zipname = os.path.join(outdir, outfilename + '_nodrm' + '.htmlz') - mb.getHTMLZip(zipname) - - print " Creating SVG ZIP Archive" - zipname = os.path.join(outdir, outfilename + '_SVG' + '.zip') - mb.getSVGZip(zipname) - - if buildXML: - print " Creating XML ZIP Archive" - zipname = os.path.join(outdir, outfilename + '_XML' + '.zip') - mb.getXMLZip(zipname) + if book.getBookType()==u"Topaz": + zipname = os.path.join(outdir, outfilename + u"_SVG.zip") + book.getSVGZip(zipname) + print u"Saved SVG ZIP Archive for {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename) # remove internal temporary directory of Topaz pieces - mb.cleanup() - print "Saved decrypted Topaz book parts after {0:.1f} seconds".format(time.time()-starttime) - return 0 + book.cleanup() def usage(progname): - print "Removes DRM protection from K4PC/M, Kindle, Mobi and Topaz ebooks" - print "Usage:" - print " %s [-k ] [-p ] [-s ] " % progname + print u"Removes DRM protection from Mobipocket, Amazon KF8, Amazon Print Replica and Amazon Topaz ebooks" + print u"Usage:" + print u" {0} [-k ] [-p ] [-s ] ".format(progname) # # Main # -def main(argv=sys.argv): +def cli_main(argv=unicode_argv()): progname = os.path.basename(argv[0]) - - k4 = False - kInfoFiles = [] - serials = [] - pids = [] - - print ('K4MobiDeDrm v%(__version__)s ' - 'provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc .' % globals()) + print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__) try: opts, args = getopt.getopt(sys.argv[1:], "k:p:s:") except getopt.GetoptError, err: - print str(err) + print u"Error in options or arguments: {0}".format(err.args[0]) usage(progname) sys.exit(2) if len(args)<2: usage(progname) sys.exit(2) + infile = args[0] + outdir = args[1] + kInfoFiles = [] + serials = [] + pids = [] + for o, a in opts: if o == "-k": if a == None : @@ -223,16 +290,13 @@ def main(argv=sys.argv): raise DrmException("Invalid parameter for -s") serials = a.split(',') - # try with built in Kindle Info files - k4 = True - if sys.platform.startswith('linux'): - k4 = False - kInfoFiles = None - infile = args[0] - outdir = args[1] - return decryptBook(infile, outdir, k4, kInfoFiles, serials, pids) + # try with built in Kindle Info files if not on Linux + k4 = not sys.platform.startswith('linux') + + return decryptBook(infile, outdir, kInfoFiles, serials, pids) if __name__ == '__main__': - sys.stdout=Unbuffered(sys.stdout) - sys.exit(main()) + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) + sys.exit(cli_main()) diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mutils.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mutils.py index 1fc08cb..bceb3a3 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mutils.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mutils.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + # standlone set of Mac OSX specific routines needed for KindleBooks from __future__ import with_statement @@ -22,7 +25,7 @@ def _load_crypto_libcrypto(): libcrypto = find_library('crypto') if libcrypto is None: - raise DrmException('libcrypto not found') + raise DrmException(u"libcrypto not found") libcrypto = CDLL(libcrypto) # From OpenSSL's crypto aes header @@ -80,14 +83,14 @@ def _load_crypto_libcrypto(): def set_decrypt_key(self, userkey, iv): self._blocksize = len(userkey) if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : - raise DrmException('AES improper key used') + raise DrmException(u"AES improper key used") return keyctx = self._keyctx = AES_KEY() self._iv = iv self._userkey = userkey rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx) if rv < 0: - raise DrmException('Failed to initialize AES key') + raise DrmException(u"Failed to initialize AES key") def decrypt(self, data): out = create_string_buffer(len(data)) @@ -95,7 +98,7 @@ def _load_crypto_libcrypto(): keyctx = self._keyctx rv = AES_cbc_encrypt(data, out, len(data), keyctx, mutable_iv, 0) if rv == 0: - raise DrmException('AES decryption failed') + raise DrmException(u"AES decryption failed") return out.raw def keyivgen(self, passwd, salt, iter, keylen): @@ -139,20 +142,20 @@ def SHA256(message): return ctx.digest() # Various character maps used to decrypt books. Probably supposed to act as obfuscation -charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" -charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM" +charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M' +charMap2 = 'ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM' # For kinf approach of K4Mac 1.6.X or later -# On K4PC charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE" +# On K4PC charMap5 = 'AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE' # For Mac they seem to re-use charMap2 here charMap5 = charMap2 # new in K4M 1.9.X -testMap8 = "YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD" +testMap8 = 'YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD' def encode(data, map): - result = "" + result = '' for char in data: value = ord(char) Q = (value ^ 0x80) // len(map) @@ -167,14 +170,14 @@ def encodeHash(data,map): # Decode the string in data with the characters in map. Returns the decoded bytes def decode(data,map): - result = "" + result = '' for i in range (0,len(data)-1,2): high = map.find(data[i]) low = map.find(data[i+1]) if (high == -1) or (low == -1) : break value = (((high * len(map)) ^ 0x80) & 0xFF) + low - result += pack("B",value) + result += pack('B',value) return result # For K4M 1.6.X and later @@ -200,7 +203,7 @@ def primes(n): # uses a sub process to get the Hard Drive Serial Number using ioreg -# returns with the serial number of drive whose BSD Name is "disk0" +# returns with the serial number of drive whose BSD Name is 'disk0' def GetVolumeSerialNumber(): sernum = os.getenv('MYSERIALNUMBER') if sernum != None: @@ -216,11 +219,11 @@ def GetVolumeSerialNumber(): foundIt = False for j in xrange(cnt): resline = reslst[j] - pp = resline.find('"Serial Number" = "') + pp = resline.find('\"Serial Number\" = \"') if pp >= 0: sernum = resline[pp+19:-1] sernum = sernum.strip() - bb = resline.find('"BSD Name" = "') + bb = resline.find('\"BSD Name\" = \"') if bb >= 0: bsdname = resline[bb+14:-1] bsdname = bsdname.strip() @@ -277,7 +280,7 @@ def GetDiskPartitionUUID(diskpart): nest += 1 if resline.find('}') >= 0: nest -= 1 - pp = resline.find('"UUID" = "') + pp = resline.find('\"UUID\" = \"') if pp >= 0: uuidnum = resline[pp+10:-1] uuidnum = uuidnum.strip() @@ -285,7 +288,7 @@ def GetDiskPartitionUUID(diskpart): if partnest == uuidnest and uuidnest > 0: foundIt = True break - bb = resline.find('"BSD Name" = "') + bb = resline.find('\"BSD Name\" = \"') if bb >= 0: bsdname = resline[bb+14:-1] bsdname = bsdname.strip() @@ -323,7 +326,7 @@ def GetMACAddressMunged(): if pp >= 0: macnum = resline[pp+6:-1] macnum = macnum.strip() - # print "original mac", macnum + # print 'original mac', macnum # now munge it up the way Kindle app does # by xoring it with 0xa5 and swapping elements 3 and 4 maclst = macnum.split(':') @@ -340,7 +343,7 @@ def GetMACAddressMunged(): mlst[2] = maclst[2] ^ 0xa5 mlst[1] = maclst[1] ^ 0xa5 mlst[0] = maclst[0] ^ 0xa5 - macnum = "%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x" % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5]) + macnum = '%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x' % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5]) foundIt = True break if not foundIt: @@ -367,6 +370,19 @@ def isNewInstall(): return False +class Memoize: + """Memoize(fn) - an instance which acts like fn but memoizes its arguments + Will only work on functions with non-mutable arguments + """ + def __init__(self, fn): + self.fn = fn + self.memo = {} + def __call__(self, *args): + if not self.memo.has_key(args): + self.memo[args] = self.fn(*args) + return self.memo[args] + +@Memoize def GetIDString(): # K4Mac now has an extensive set of ids strings it uses # in encoding pids and in creating unique passwords @@ -530,7 +546,8 @@ def getKindleInfoFiles(): # determine type of kindle info provided and return a # database of keynames and values def getDBfromFile(kInfoFile): - names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber", "max_date", "SIGVERIF"] + + names = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber', 'max_date', 'SIGVERIF'] DB = {} cnt = 0 infoReader = open(kInfoFile, 'r') @@ -545,12 +562,12 @@ def getDBfromFile(kInfoFile): for item in items: if item != '': keyhash, rawdata = item.split(':') - keyname = "unknown" + keyname = 'unknown' for name in names: if encodeHash(name,charMap2) == keyhash: keyname = name break - if keyname == "unknown": + if keyname == 'unknown': keyname = keyhash encryptedValue = decode(rawdata,charMap2) cleartext = cud.decrypt(encryptedValue) @@ -563,8 +580,8 @@ def getDBfromFile(kInfoFile): if hdr == '/': # else newer style .kinf file used by K4Mac >= 1.6.0 - # the .kinf file uses "/" to separate it into records - # so remove the trailing "/" to make it easy to use split + # the .kinf file uses '/' to separate it into records + # so remove the trailing '/' to make it easy to use split data = data[:-1] items = data.split('/') cud = CryptUnprotectDataV2() @@ -578,11 +595,11 @@ def getDBfromFile(kInfoFile): # the first 32 chars of the first record of a group # is the MD5 hash of the key name encoded by charMap5 keyhash = item[0:32] - keyname = "unknown" + keyname = 'unknown' # the raw keyhash string is also used to create entropy for the actual # CryptProtectData Blob that represents that keys contents - # "entropy" not used for K4Mac only K4PC + # 'entropy' not used for K4Mac only K4PC # entropy = SHA1(keyhash) # the remainder of the first record when decoded with charMap5 @@ -599,12 +616,12 @@ def getDBfromFile(kInfoFile): item = items.pop(0) edlst.append(item) - keyname = "unknown" + keyname = 'unknown' for name in names: if encodeHash(name,charMap5) == keyhash: keyname = name break - if keyname == "unknown": + if keyname == 'unknown': keyname = keyhash # the charMap5 encoded contents data has had a length @@ -615,10 +632,10 @@ def getDBfromFile(kInfoFile): # The offset into the charMap5 encoded contents seems to be: # len(contents) - largest prime number less than or equal to int(len(content)/3) - # (in other words split "about" 2/3rds of the way through) + # (in other words split 'about' 2/3rds of the way through) # move first offsets chars to end to align for decode by charMap5 - encdata = "".join(edlst) + encdata = ''.join(edlst) contlen = len(encdata) # now properly split and recombine @@ -667,7 +684,7 @@ def getDBfromFile(kInfoFile): # the first 32 chars of the first record of a group # is the MD5 hash of the key name encoded by charMap5 keyhash = item[0:32] - keyname = "unknown" + keyname = 'unknown' # unlike K4PC the keyhash is not used in generating entropy # entropy = SHA1(keyhash) + added_entropy @@ -687,12 +704,12 @@ def getDBfromFile(kInfoFile): item = items.pop(0) edlst.append(item) - keyname = "unknown" + keyname = 'unknown' for name in names: if encodeHash(name,testMap8) == keyhash: keyname = name break - if keyname == "unknown": + if keyname == 'unknown': keyname = keyhash # the testMap8 encoded contents data has had a length @@ -703,10 +720,10 @@ def getDBfromFile(kInfoFile): # The offset into the testMap8 encoded contents seems to be: # len(contents) - largest prime number less than or equal to int(len(content)/3) - # (in other words split "about" 2/3rds of the way through) + # (in other words split 'about' 2/3rds of the way through) # move first offsets chars to end to align for decode by testMap8 - encdata = "".join(edlst) + encdata = ''.join(edlst) contlen = len(encdata) # now properly split and recombine diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4pcutils.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4pcutils.py index 9f9ca07..476844c 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4pcutils.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4pcutils.py @@ -1,4 +1,6 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- + # K4PC Windows specific routines from __future__ import with_statement diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kgenpids.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kgenpids.py index b0fbaa4..c5de9b9 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kgenpids.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kgenpids.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- from __future__ import with_statement import sys @@ -17,26 +18,24 @@ global charMap4 if 'calibre' in sys.modules: inCalibre = True -else: - inCalibre = False - -if inCalibre: - if sys.platform.startswith('win'): + from calibre.constants import iswindows, isosx + if iswindows: from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString - - if sys.platform.startswith('darwin'): + if isosx: from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString else: - if sys.platform.startswith('win'): + inCalibre = False + iswindows = sys.platform.startswith('win') + isosx = sys.platform.startswith('darwin') + if iswindows: from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString - - if sys.platform.startswith('darwin'): + if isosx: from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString -charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" -charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" -charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" +charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M' +charMap3 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' +charMap4 = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789' # crypto digestroutines import hashlib @@ -54,7 +53,7 @@ def SHA1(message): # Encode the bytes in data with the characters in map def encode(data, map): - result = "" + result = '' for char in data: value = ord(char) Q = (value ^ 0x80) // len(map) @@ -69,14 +68,14 @@ def encodeHash(data,map): # Decode the string in data with the characters in map. Returns the decoded bytes def decode(data,map): - result = "" + result = '' for i in range (0,len(data)-1,2): high = map.find(data[i]) low = map.find(data[i+1]) if (high == -1) or (low == -1) : break value = (((high * len(map)) ^ 0x80) & 0xFF) + low - result += pack("B",value) + result += pack('B',value) return result # @@ -98,7 +97,7 @@ def getSixBitsFromBitField(bitField,offset): # 8 bits to six bits encoding from hash to generate PID string def encodePID(hash): global charMap3 - PID = "" + PID = '' for position in range (0,8): PID += charMap3[getSixBitsFromBitField(hash,position)] return PID @@ -129,7 +128,7 @@ def generatePidSeed(table,dsn) : def generateDevicePID(table,dsn,nbRoll): global charMap4 seed = generatePidSeed(table,dsn) - pidAscii = "" + pidAscii = '' pid = [(seed >>24) &0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF,(seed>>24) & 0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF] index = 0 for counter in range (0,nbRoll): @@ -176,28 +175,31 @@ def pidFromSerial(s, l): # Parse the EXTH header records and use the Kindle serial number to calculate the book pid. -def getKindlePid(pidlst, rec209, token, serialnum): +def getKindlePids(rec209, token, serialnum): + pids=[] + # Compute book PID pidHash = SHA1(serialnum+rec209+token) bookPID = encodePID(pidHash) bookPID = checksumPid(bookPID) - pidlst.append(bookPID) + pids.append(bookPID) # compute fixed pid for old pre 2.5 firmware update pid as well - bookPID = pidFromSerial(serialnum, 7) + "*" - bookPID = checksumPid(bookPID) - pidlst.append(bookPID) + kindlePID = pidFromSerial(serialnum, 7) + "*" + kindlePID = checksumPid(kindlePID) + pids.append(kindlePID) - return pidlst + return pids # parse the Kindleinfo file to calculate the book pid. -keynames = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"] +keynames = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber'] -def getK4Pids(pidlst, rec209, token, kInfoFile): +def getK4Pids(rec209, token, kInfoFile): global charMap1 kindleDatabase = None + pids = [] try: kindleDatabase = getDBfromFile(kInfoFile) except Exception, message: @@ -206,17 +208,17 @@ def getK4Pids(pidlst, rec209, token, kInfoFile): pass if kindleDatabase == None : - return pidlst + return pids try: # Get the Mazama Random number - MazamaRandomNumber = kindleDatabase["MazamaRandomNumber"] + MazamaRandomNumber = kindleDatabase['MazamaRandomNumber'] # Get the kindle account token - kindleAccountToken = kindleDatabase["kindle.account.tokens"] + kindleAccountToken = kindleDatabase['kindle.account.tokens'] except KeyError: - print "Keys not found in " + kInfoFile - return pidlst + print u"Keys not found in {0}".format(os.path.basename(kInfoFile)) + return pids # Get the ID string used encodedIDString = encodeHash(GetIDString(),charMap1) @@ -231,7 +233,7 @@ def getK4Pids(pidlst, rec209, token, kInfoFile): table = generatePidEncryptionTable() devicePID = generateDevicePID(table,DSN,4) devicePID = checksumPid(devicePID) - pidlst.append(devicePID) + pids.append(devicePID) # Compute book PIDs @@ -239,36 +241,38 @@ def getK4Pids(pidlst, rec209, token, kInfoFile): pidHash = SHA1(DSN+kindleAccountToken+rec209+token) bookPID = encodePID(pidHash) bookPID = checksumPid(bookPID) - pidlst.append(bookPID) + pids.append(bookPID) # variant 1 pidHash = SHA1(kindleAccountToken+rec209+token) bookPID = encodePID(pidHash) bookPID = checksumPid(bookPID) - pidlst.append(bookPID) + pids.append(bookPID) # variant 2 pidHash = SHA1(DSN+rec209+token) bookPID = encodePID(pidHash) bookPID = checksumPid(bookPID) - pidlst.append(bookPID) + pids.append(bookPID) - return pidlst + return pids -def getPidList(md1, md2, k4 = True, serials=[], kInfoFiles=[]): +def getPidList(md1, md2, serials=[], kInfoFiles=[]): pidlst = [] if kInfoFiles is None: kInfoFiles = [] - if k4: + if serials is None: + serials = [] + if iswindows or isosx: kInfoFiles.extend(getKindleInfoFiles()) for infoFile in kInfoFiles: try: - pidlst = getK4Pids(pidlst, md1, md2, infoFile) - except Exception, message: - print("Error getting PIDs from " + infoFile + ": " + message) + pidlst.extend(getK4Pids(md1, md2, infoFile)) + except Exception, e: + print u"Error getting PIDs from {0}: {1}".format(os.path.basename(infoFile),e.args[0]) for serialnum in serials: try: - pidlst = getKindlePid(pidlst, md1, md2, serialnum) + pidlst.extend(getKindlePids(md1, md2, serialnum)) except Exception, message: - print("Error getting PIDs from " + serialnum + ": " + message) + print u"Error getting PIDs from serial number {0}: {1}".format(serialnum ,e.args[0]) return pidlst diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kindlepid.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kindlepid.py index 90a59ad..38c5e4e 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kindlepid.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kindlepid.py @@ -1,29 +1,80 @@ #!/usr/bin/python -# Mobipocket PID calculator v0.2 for Amazon Kindle. +# -*- coding: utf-8 -*- + +# Mobipocket PID calculator v0.4 for Amazon Kindle. # Copyright (c) 2007, 2009 Igor Skochinsky # History: # 0.1 Initial release # 0.2 Added support for generating PID for iPhone (thanks to mbp) # 0.3 changed to autoflush stdout, fixed return code usage -class Unbuffered: +# 0.3 updated for unicode + +import sys +import binascii + +# 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) -import sys -sys.stdout=Unbuffered(sys.stdout) +iswindows = sys.platform.startswith('win') +isosx = sys.platform.startswith('darwin') -import binascii +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"mobidedrm.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] if sys.hexversion >= 0x3000000: - print "This script is incompatible with Python 3.x. Please install Python 2.6.x from python.org" + print 'This script is incompatible with Python 3.x. Please install Python 2.7.x.' sys.exit(2) -letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" +letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789' def crc32(s): return (~binascii.crc32(s,-1))&0xFFFFFFFF @@ -53,39 +104,39 @@ def pidFromSerial(s, l): for i in xrange(l): arr1[i] ^= crc_bytes[i&3] - pid = "" + pid = '' for i in xrange(l): b = arr1[i] & 0xff pid+=letters[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))] return pid -def main(argv=sys.argv): - print "Mobipocket PID calculator for Amazon Kindle. Copyright (c) 2007, 2009 Igor Skochinsky" +def cli_main(argv=unicode_argv()): + print u"Mobipocket PID calculator for Amazon Kindle. Copyright © 2007, 2009 Igor Skochinsky" if len(sys.argv)==2: serial = sys.argv[1] else: - print "Usage: kindlepid.py /" + print u"Usage: kindlepid.py /" return 1 if len(serial)==16: if serial.startswith("B"): - print "Kindle serial number detected" + print u"Kindle serial number detected" else: - print "Warning: unrecognized serial number. Please recheck input." + print u"Warning: unrecognized serial number. Please recheck input." return 1 - pid = pidFromSerial(serial,7)+"*" - print "Mobipocket PID for Kindle serial# "+serial+" is "+checksumPid(pid) + pid = pidFromSerial(serial.encode("utf-8"),7)+'*' + print u"Mobipocket PID for Kindle serial#{0} is {1} ".format(serial,checksumPid(pid)) return 0 elif len(serial)==40: - print "iPhone serial number (UDID) detected" - pid = pidFromSerial(serial,8) - print "Mobipocket PID for iPhone serial# "+serial+" is "+checksumPid(pid) + print u"iPhone serial number (UDID) detected" + pid = pidFromSerial(serial.encode("utf-8"),8) + print u"Mobipocket PID for iPhone serial#{0} is {1} ".format(serial,checksumPid(pid)) return 0 - else: - print "Warning: unrecognized serial number. Please recheck input." - return 1 - return 0 + print u"Warning: unrecognized serial number. Please recheck input." + return 1 if __name__ == "__main__": - sys.exit(main()) + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) + sys.exit(cli_main()) diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py index cd993e1..113f57a 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py @@ -1,5 +1,11 @@ -#!/usr/bin/python +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# mobidedrm.py, version 0.38 +# Copyright © 2008 The Dark Reverser # +# Modified 2008–2012 by some_updates, DiapDealer and Apprentice Alf + # This is a python script. You need a Python interpreter to run it. # For example, ActiveState Python, which exists for windows. # @@ -59,26 +65,78 @@ # 0.35 - add interface to get mobi_version # 0.36 - fixed problem with TEXtREAd and getBookTitle interface # 0.37 - Fixed double announcement for stand-alone operation +# 0.38 - Unicode used wherever possible, cope with absent alfcrypto -__version__ = '0.37' +__version__ = u"0.38" import sys +import os +import struct +import binascii +try: + from alfcrypto import Pukall_Cipher +except: + print u"AlfCrypto not found. Using python PC1 implementation." -class Unbuffered: +# 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) -sys.stdout=Unbuffered(sys.stdout) -import os -import struct -import binascii -from alfcrypto import Pukall_Cipher +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"mobidedrm.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 DrmException(Exception): pass @@ -90,40 +148,45 @@ class DrmException(Exception): # Implementation of Pukall Cipher 1 def PC1(key, src, decryption=True): - return Pukall_Cipher().PC1(key,src,decryption) -# sum1 = 0; -# sum2 = 0; -# keyXorVal = 0; -# if len(key)!=16: -# print "Bad key length!" -# return None -# wkey = [] -# for i in xrange(8): -# wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1])) -# dst = "" -# for i in xrange(len(src)): -# temp1 = 0; -# byteXorVal = 0; -# for j in xrange(8): -# temp1 ^= wkey[j] -# sum2 = (sum2+j)*20021 + sum1 -# sum1 = (temp1*346)&0xFFFF -# sum2 = (sum2+sum1)&0xFFFF -# temp1 = (temp1*20021+1)&0xFFFF -# byteXorVal ^= temp1 ^ sum2 -# curByte = ord(src[i]) -# if not decryption: -# keyXorVal = curByte * 257; -# curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF -# if decryption: -# keyXorVal = curByte * 257; -# for j in xrange(8): -# wkey[j] ^= keyXorVal; -# dst+=chr(curByte) -# return dst + # if we can get it from alfcrypto, use that + try: + return Pukall_Cipher().PC1(key,src,decryption) + except NameError: + pass + + # use slow python version, since Pukall_Cipher didn't load + sum1 = 0; + sum2 = 0; + keyXorVal = 0; + if len(key)!=16: + DrmException (u"PC1: Bad key length") + wkey = [] + for i in xrange(8): + wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1])) + dst = "" + for i in xrange(len(src)): + temp1 = 0; + byteXorVal = 0; + for j in xrange(8): + temp1 ^= wkey[j] + sum2 = (sum2+j)*20021 + sum1 + sum1 = (temp1*346)&0xFFFF + sum2 = (sum2+sum1)&0xFFFF + temp1 = (temp1*20021+1)&0xFFFF + byteXorVal ^= temp1 ^ sum2 + curByte = ord(src[i]) + if not decryption: + keyXorVal = curByte * 257; + curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF + if decryption: + keyXorVal = curByte * 257; + for j in xrange(8): + wkey[j] ^= keyXorVal; + dst+=chr(curByte) + return dst def checksumPid(s): - letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" + letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789' crc = (~binascii.crc32(s,-1))&0xFFFFFFFF crc = crc ^ (crc >> 16) res = s @@ -171,17 +234,24 @@ class MobiBook: off = self.sections[section][0] return self.data_file[off:endoff] - def __init__(self, infile, announce = True): - if announce: - print ('MobiDeDrm v%(__version__)s. ' - 'Copyright 2008-2012 The Dark Reverser et al.' % globals()) + def cleanup(self): + # to match function in Topaz book + pass + + def __init__(self, infile): + print u"MobiDeDrm v{0:s}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__) + + try: + from alfcrypto import Pukall_Cipher + except: + print u"AlfCrypto not found. Using python PC1 implementation." # initial sanity check on file self.data_file = file(infile, 'rb').read() self.mobi_data = '' self.header = self.data_file[0:78] if self.header[0x3C:0x3C+8] != 'BOOKMOBI' and self.header[0x3C:0x3C+8] != 'TEXtREAd': - raise DrmException("invalid file format") + raise DrmException(u"Invalid file format") self.magic = self.header[0x3C:0x3C+8] self.crypto_type = -1 @@ -199,7 +269,7 @@ class MobiBook: self.compression, = struct.unpack('>H', self.sect[0x0:0x0+2]) if self.magic == 'TEXtREAd': - print "Book has format: ", self.magic + print u"PalmDoc format book detected." self.extra_data_flags = 0 self.mobi_length = 0 self.mobi_codepage = 1252 @@ -209,11 +279,11 @@ class MobiBook: self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18]) self.mobi_codepage, = struct.unpack('>L',self.sect[0x1c:0x20]) self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C]) - print "MOBI header version = %d, length = %d" %(self.mobi_version, self.mobi_length) + print u"MOBI header version {0:d}, header length {1:d}".format(self.mobi_version, self.mobi_length) self.extra_data_flags = 0 if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5): self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4]) - print "Extra Data Flags = %d" % self.extra_data_flags + print u"Extra Data Flags: {0:d}".format(self.extra_data_flags) if (self.compression != 17480): # multibyte utf8 data is included in the encryption for PalmDoc compression # so clear that byte so that we leave it to be decrypted. @@ -223,10 +293,10 @@ class MobiBook: self.meta_array = {} try: exth_flag, = struct.unpack('>L', self.sect[0x80:0x84]) - exth = 'NONE' + exth = '' if exth_flag & 0x40: exth = self.sect[16 + self.mobi_length:] - if (len(exth) >= 4) and (exth[:4] == 'EXTH'): + if (len(exth) >= 12) and (exth[:4] == 'EXTH'): nitems, = struct.unpack('>I', exth[8:12]) pos = 12 for i in xrange(nitems): @@ -236,10 +306,10 @@ class MobiBook: # reset the text to speech flag and clipping limit, if present if type == 401 and size == 9: # set clipping limit to 100% - self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8) + self.patchSection(0, '\144', 16 + self.mobi_length + pos + 8) elif type == 404 and size == 9: # make sure text to speech is enabled - self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8) + self.patchSection(0, '\0', 16 + self.mobi_length + pos + 8) # print type, size, content, content.encode('hex') pos += size except: @@ -265,8 +335,8 @@ class MobiBook: codec = codec_map[self.mobi_codepage] if title == '': title = self.header[:32] - title = title.split("\0")[0] - return unicode(title, codec).encode('utf-8') + title = title.split('\0')[0] + return unicode(title, codec) def getPIDMetaInfo(self): rec209 = '' @@ -297,7 +367,7 @@ class MobiBook: def parseDRM(self, data, count, pidlist): found_key = None - keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96" + keyvec1 = '\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96' for pid in pidlist: bigpid = pid.ljust(16,'\0') temp_key = PC1(keyvec1, bigpid, False) @@ -315,7 +385,7 @@ class MobiBook: break if not found_key: # Then try the default encoding that doesn't require a PID - pid = "00000000" + pid = '00000000' temp_key = keyvec1 temp_key_sum = sum(map(ord,temp_key)) & 0xff for i in xrange(count): @@ -328,82 +398,90 @@ class MobiBook: break return [found_key,pid] - def getMobiFile(self, outpath): + def getFile(self, outpath): file(outpath,'wb').write(self.mobi_data) - def getMobiVersion(self): - return self.mobi_version + def getBookType(self): + if self.print_replica: + return u"Print Replica" + if self.mobi_version >= 8: + return u"Kindle Format 8" + return u"Mobipocket" - def getPrintReplica(self): - return self.print_replica + def getBookExtension(self): + if self.print_replica: + return u".azw4" + if self.mobi_version >= 8: + return u".azw3" + return u".mobi" def processBook(self, pidlist): crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2]) - print 'Crypto Type is: ', crypto_type + print u"Crypto Type is: {0:d}".format(crypto_type) self.crypto_type = crypto_type if crypto_type == 0: - print "This book is not encrypted." + print u"This book is not encrypted." # we must still check for Print Replica self.print_replica = (self.loadSection(1)[0:4] == '%MOP') self.mobi_data = self.data_file return if crypto_type != 2 and crypto_type != 1: - raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type) + raise DrmException(u"Cannot decode unknown Mobipocket encryption type {0:d}".format(crypto_type)) if 406 in self.meta_array: data406 = self.meta_array[406] val406, = struct.unpack('>Q',data406) if val406 != 0: - raise DrmException("Cannot decode library or rented ebooks.") + raise DrmException(u"Cannot decode library or rented ebooks.") goodpids = [] for pid in pidlist: if len(pid)==10: if checksumPid(pid[0:-2]) != pid: - print "Warning: PID " + pid + " has incorrect checksum, should have been "+checksumPid(pid[0:-2]) + print u"Warning: PID {0} has incorrect checksum, should have been {1}".format(pid,checksumPid(pid[0:-2])) goodpids.append(pid[0:-2]) elif len(pid)==8: goodpids.append(pid) if self.crypto_type == 1: - t1_keyvec = "QDCVEPMU675RUBSZ" + t1_keyvec = 'QDCVEPMU675RUBSZ' if self.magic == 'TEXtREAd': bookkey_data = self.sect[0x0E:0x0E+16] elif self.mobi_version < 0: bookkey_data = self.sect[0x90:0x90+16] else: bookkey_data = self.sect[self.mobi_length+16:self.mobi_length+32] - pid = "00000000" + pid = '00000000' found_key = PC1(t1_keyvec, bookkey_data) else : # calculate the keys drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16]) if drm_count == 0: - raise DrmException("Not yet initialised with PID. Must be opened with Mobipocket Reader first.") + raise DrmException(u"Encryption not initialised. Must be opened with Mobipocket Reader first.") found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids) if not found_key: - raise DrmException("No key found in " + str(len(goodpids)) + " keys tried. Read the FAQs at Alf's blog. Only if none apply, report this failure for help.") + raise DrmException(u"No key found in {0:d} keys tried. Read the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(len(goodpids))) # kill the drm keys - self.patchSection(0, "\0" * drm_size, drm_ptr) + self.patchSection(0, '\0' * drm_size, drm_ptr) # kill the drm pointers - self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8) + self.patchSection(0, '\xff' * 4 + '\0' * 12, 0xA8) - if pid=="00000000": - print "File has default encryption, no specific PID." + if pid=='00000000': + print u"File has default encryption, no specific key needed." else: - print "File is encoded with PID "+checksumPid(pid)+"." + print u"File is encoded with PID {0}.".format(checksumPid(pid)) # clear the crypto type self.patchSection(0, "\0" * 2, 0xC) # decrypt sections - print "Decrypting. Please wait . . .", + print u"Decrypting. Please wait . . .", mobidataList = [] mobidataList.append(self.data_file[:self.sections[1][0]]) for i in xrange(1, self.records+1): data = self.loadSection(i) extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags) if i%100 == 0: - print ".", + print u".", # print "record %d, extra_size %d" %(i,extra_size) decoded_data = PC1(found_key, data[0:len(data) - extra_size]) if i==1: @@ -414,31 +492,24 @@ class MobiBook: if self.num_sections > self.records+1: mobidataList.append(self.data_file[self.sections[self.records+1][0]:]) self.mobi_data = "".join(mobidataList) - print "done" + print u"done" return -def getUnencryptedBook(infile,pid,announce=True): +def getUnencryptedBook(infile,pidlist): if not os.path.isfile(infile): - raise DrmException('Input File Not Found') - book = MobiBook(infile,announce) - book.processBook([pid]) - return book.mobi_data - -def getUnencryptedBookWithList(infile,pidlist,announce=True): - if not os.path.isfile(infile): - raise DrmException('Input File Not Found') - book = MobiBook(infile, announce) + raise DrmException(u"Input File Not Found.") + book = MobiBook(infile) book.processBook(pidlist) return book.mobi_data -def main(argv=sys.argv): - print ('MobiDeDrm v%(__version__)s. ' - 'Copyright 2008-2012 The Dark Reverser et al.' % globals()) +def cli_main(argv=unicode_argv()): + progname = os.path.basename(argv[0]) if len(argv)<3 or len(argv)>4: - print "Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks" - print "Usage:" - print " %s []" % sys.argv[0] + print u"MobiDeDrm v{0}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__) + print u"Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks" + print u"Usage:" + print u" {0} []".format(os.path.basename(sys.argv[0])) return 1 else: infile = argv[1] @@ -446,15 +517,17 @@ def main(argv=sys.argv): if len(argv) is 4: pidlist = argv[3].split(',') else: - pidlist = {} + pidlist = [] try: - stripped_file = getUnencryptedBookWithList(infile, pidlist, False) + stripped_file = getUnencryptedBook(infile, pidlist) file(outfile, 'wb').write(stripped_file) except DrmException, e: - print "Error: %s" % e + print u"MobiDeDRM v{0} Error: {0:s}".format(__version__,e.args[0]) return 1 return 0 -if __name__ == "__main__": - sys.exit(main()) +if __name__ == '__main__': + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) + sys.exit(cli_main()) diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/topazextract.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/topazextract.py index bf2ad47..a343922 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/topazextract.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/topazextract.py @@ -1,43 +1,90 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- -class Unbuffered: +# topazextract.py, version ? +# Mostly written by some_updates based on code from many others + +__version__ = '4.8' + +import sys +import os, csv, getopt +import zlib, zipfile, tempfile, shutil +import traceback +from struct import pack +from struct import unpack +from alfcrypto import Topaz_Cipher + +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) -import sys +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"mobidedrm.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] if 'calibre' in sys.modules: inCalibre = True -else: - inCalibre = False - -buildXML = False - -import os, csv, getopt -import zlib, zipfile, tempfile, shutil -from struct import pack -from struct import unpack -from alfcrypto import Topaz_Cipher - -class TpzDRMError(Exception): - pass - - -# local support routines -if inCalibre: from calibre_plugins.k4mobidedrm import kgenpids else: + inCalibre = False import kgenpids + +class DrmException(Exception): + pass + + # recursive zip creation support routine def zipUpDir(myzip, tdir, localname): currentdir = tdir - if localname != "": + if localname != u"": currentdir = os.path.join(currentdir,localname) list = os.listdir(currentdir) for file in list: @@ -73,7 +120,7 @@ def bookReadEncodedNumber(fo): # Get a length prefixed string from file def bookReadString(fo): stringLength = bookReadEncodedNumber(fo) - return unpack(str(stringLength)+"s",fo.read(stringLength))[0] + return unpack(str(stringLength)+'s',fo.read(stringLength))[0] # # crypto routines @@ -112,13 +159,13 @@ def decryptRecord(data,PID): # Try to decrypt a dkey record (contains the bookPID) def decryptDkeyRecord(data,PID): record = decryptRecord(data,PID) - fields = unpack("3sB8sB8s3s",record) - if fields[0] != "PID" or fields[5] != "pid" : - raise TpzDRMError("Didn't find PID magic numbers in record") + fields = unpack('3sB8sB8s3s',record) + if fields[0] != 'PID' or fields[5] != 'pid' : + raise DrmException(u"Didn't find PID magic numbers in record") elif fields[1] != 8 or fields[3] != 8 : - raise TpzDRMError("Record didn't contain correct length fields") + raise DrmException(u"Record didn't contain correct length fields") elif fields[2] != PID : - raise TpzDRMError("Record didn't contain PID") + raise DrmException(u"Record didn't contain PID") return fields[4] # Decrypt all dkey records (contain the book PID) @@ -131,11 +178,11 @@ def decryptDkeyRecords(data,PID): try: key = decryptDkeyRecord(data[1:length+1],PID) records.append(key) - except TpzDRMError: + except DrmException: pass data = data[1+length:] if len(records) == 0: - raise TpzDRMError("BookKey Not Found") + raise DrmException(u"BookKey Not Found") return records @@ -148,9 +195,9 @@ class TopazBook: self.bookHeaderRecords = {} self.bookMetadata = {} self.bookKey = None - magic = unpack("4s",self.fo.read(4))[0] + magic = unpack('4s',self.fo.read(4))[0] if magic != 'TPZ0': - raise TpzDRMError("Parse Error : Invalid Header, not a Topaz file") + raise DrmException(u"Parse Error : Invalid Header, not a Topaz file") self.parseTopazHeaders() self.parseMetadata() @@ -167,7 +214,7 @@ class TopazBook: # Read and parse one header record at the current book file position and return the associated data # [[offset,decompressedLength,compressedLength],...] if ord(self.fo.read(1)) != 0x63: - raise TpzDRMError("Parse Error : Invalid Header") + raise DrmException(u"Parse Error : Invalid Header") tag = bookReadString(self.fo) record = bookReadHeaderRecordData() return [tag,record] @@ -177,15 +224,15 @@ class TopazBook: # print result[0], result[1] self.bookHeaderRecords[result[0]] = result[1] if ord(self.fo.read(1)) != 0x64 : - raise TpzDRMError("Parse Error : Invalid Header") + raise DrmException(u"Parse Error : Invalid Header") self.bookPayloadOffset = self.fo.tell() def parseMetadata(self): # Parse the metadata record from the book payload and return a list of [key,values] - self.fo.seek(self.bookPayloadOffset + self.bookHeaderRecords["metadata"][0][0]) + self.fo.seek(self.bookPayloadOffset + self.bookHeaderRecords['metadata'][0][0]) tag = bookReadString(self.fo) - if tag != "metadata" : - raise TpzDRMError("Parse Error : Record Names Don't Match") + if tag != 'metadata' : + raise DrmException(u"Parse Error : Record Names Don't Match") flags = ord(self.fo.read(1)) nbRecords = ord(self.fo.read(1)) # print nbRecords @@ -210,7 +257,7 @@ class TopazBook: title = '' if 'Title' in self.bookMetadata: title = self.bookMetadata['Title'] - return title + return title.decode('utf-8') def setBookKey(self, key): self.bookKey = key @@ -223,13 +270,13 @@ class TopazBook: try: recordOffset = self.bookHeaderRecords[name][index][0] except: - raise TpzDRMError("Parse Error : Invalid Record, record not found") + raise DrmException("Parse Error : Invalid Record, record not found") self.fo.seek(self.bookPayloadOffset + recordOffset) tag = bookReadString(self.fo) if tag != name : - raise TpzDRMError("Parse Error : Invalid Record, record name doesn't match") + raise DrmException("Parse Error : Invalid Record, record name doesn't match") recordIndex = bookReadEncodedNumber(self.fo) if recordIndex < 0 : @@ -237,7 +284,7 @@ class TopazBook: recordIndex = -recordIndex -1 if recordIndex != index : - raise TpzDRMError("Parse Error : Invalid Record, index doesn't match") + raise DrmException("Parse Error : Invalid Record, index doesn't match") if (self.bookHeaderRecords[name][index][2] > 0): compressed = True @@ -250,7 +297,7 @@ class TopazBook: ctx = topazCryptoInit(self.bookKey) record = topazCryptoDecrypt(record,ctx) else : - raise TpzDRMError("Error: Attempt to decrypt without bookKey") + raise DrmException("Error: Attempt to decrypt without bookKey") if compressed: record = zlib.decompress(record) @@ -262,12 +309,12 @@ class TopazBook: fixedimage=True try: keydata = self.getBookPayloadRecord('dkey', 0) - except TpzDRMError, e: - print "no dkey record found, book may not be encrypted" - print "attempting to extrct files without a book key" + except DrmException, e: + print u"no dkey record found, book may not be encrypted" + print u"attempting to extrct files without a book key" self.createBookDirectory() self.extractFiles() - print "Successfully Extracted Topaz contents" + print u"Successfully Extracted Topaz contents" if inCalibre: from calibre_plugins.k4mobidedrm import genbook else: @@ -275,7 +322,7 @@ class TopazBook: rv = genbook.generateBook(self.outdir, raw, fixedimage) if rv == 0: - print "\nBook Successfully generated" + print u"Book Successfully generated." return rv # try each pid to decode the file @@ -283,25 +330,25 @@ class TopazBook: for pid in pidlst: # use 8 digit pids here pid = pid[0:8] - print "\nTrying: ", pid + print u"Trying: {0}".format(pid) bookKeys = [] data = keydata try: bookKeys+=decryptDkeyRecords(data,pid) - except TpzDRMError, e: + except DrmException, e: pass else: bookKey = bookKeys[0] - print "Book Key Found!" + print u"Book Key Found! ({0})".format(bookKey.encode('hex')) break if not bookKey: - raise TpzDRMError("Topaz Book. No key found in " + str(len(pidlst)) + " keys tried. Read the FAQs at Alf's blog. Only if none apply, report this failure for help.") + raise DrmException(u"No key found in {0:d} keys tried. Read the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(len(pidlst))) self.setBookKey(bookKey) self.createBookDirectory() self.extractFiles() - print "Successfully Extracted Topaz contents" + print u"Successfully Extracted Topaz contents" if inCalibre: from calibre_plugins.k4mobidedrm import genbook else: @@ -309,7 +356,7 @@ class TopazBook: rv = genbook.generateBook(self.outdir, raw, fixedimage) if rv == 0: - print "\nBook Successfully generated" + print u"Book Successfully generated" return rv def createBookDirectory(self): @@ -317,16 +364,16 @@ class TopazBook: # create output directory structure if not os.path.exists(outdir): os.makedirs(outdir) - destdir = os.path.join(outdir,'img') + destdir = os.path.join(outdir,u"img") if not os.path.exists(destdir): os.makedirs(destdir) - destdir = os.path.join(outdir,'color_img') + destdir = os.path.join(outdir,u"color_img") if not os.path.exists(destdir): os.makedirs(destdir) - destdir = os.path.join(outdir,'page') + destdir = os.path.join(outdir,u"page") if not os.path.exists(destdir): os.makedirs(destdir) - destdir = os.path.join(outdir,'glyphs') + destdir = os.path.join(outdir,u"glyphs") if not os.path.exists(destdir): os.makedirs(destdir) @@ -334,149 +381,148 @@ class TopazBook: outdir = self.outdir for headerRecord in self.bookHeaderRecords: name = headerRecord - if name != "dkey" : - ext = '.dat' - if name == 'img' : ext = '.jpg' - if name == 'color' : ext = '.jpg' - print "\nProcessing Section: %s " % name + if name != 'dkey': + ext = u".dat" + if name == 'img': ext = u".jpg" + if name == 'color' : ext = u".jpg" + print u"Processing Section: {0}\n. . .".format(name), for index in range (0,len(self.bookHeaderRecords[name])) : - fnum = "%04d" % index - fname = name + fnum + ext + fname = u"{0}{1:04d}{2}".format(name,index,ext) destdir = outdir if name == 'img': - destdir = os.path.join(outdir,'img') + destdir = os.path.join(outdir,u"img") if name == 'color': - destdir = os.path.join(outdir,'color_img') + destdir = os.path.join(outdir,u"color_img") if name == 'page': - destdir = os.path.join(outdir,'page') + destdir = os.path.join(outdir,u"page") if name == 'glyphs': - destdir = os.path.join(outdir,'glyphs') + destdir = os.path.join(outdir,u"glyphs") outputFile = os.path.join(destdir,fname) - print ".", + print u".", record = self.getBookPayloadRecord(name,index) if record != '': file(outputFile, 'wb').write(record) - print " " + print u" " - def getHTMLZip(self, zipname): + def getFile(self, zipname): htmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False) - htmlzip.write(os.path.join(self.outdir,'book.html'),'book.html') - htmlzip.write(os.path.join(self.outdir,'book.opf'),'book.opf') - if os.path.isfile(os.path.join(self.outdir,'cover.jpg')): - htmlzip.write(os.path.join(self.outdir,'cover.jpg'),'cover.jpg') - htmlzip.write(os.path.join(self.outdir,'style.css'),'style.css') - zipUpDir(htmlzip, self.outdir, 'img') + htmlzip.write(os.path.join(self.outdir,u"book.html"),u"book.html") + htmlzip.write(os.path.join(self.outdir,u"book.opf"),u"book.opf") + if os.path.isfile(os.path.join(self.outdir,u"cover.jpg")): + htmlzip.write(os.path.join(self.outdir,u"cover.jpg"),u"cover.jpg") + htmlzip.write(os.path.join(self.outdir,u"style.css"),u"style.css") + zipUpDir(htmlzip, self.outdir, u"img") htmlzip.close() + def getBookType(self): + return u"Topaz" + + def getBookExtension(self): + return u".htmlz" + def getSVGZip(self, zipname): svgzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False) - svgzip.write(os.path.join(self.outdir,'index_svg.xhtml'),'index_svg.xhtml') - zipUpDir(svgzip, self.outdir, 'svg') - zipUpDir(svgzip, self.outdir, 'img') + svgzip.write(os.path.join(self.outdir,u"index_svg.xhtml"),u"index_svg.xhtml") + zipUpDir(svgzip, self.outdir, u"svg") + zipUpDir(svgzip, self.outdir, u"img") svgzip.close() - def getXMLZip(self, zipname): - xmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False) - targetdir = os.path.join(self.outdir,'xml') - zipUpDir(xmlzip, targetdir, '') - zipUpDir(xmlzip, self.outdir, 'img') - xmlzip.close() - def cleanup(self): if os.path.isdir(self.outdir): shutil.rmtree(self.outdir, True) def usage(progname): - print "Removes DRM protection from Topaz ebooks and extract the contents" - print "Usage:" - print " %s [-k ] [-p ] [-s ] " % progname - + print u"Removes DRM protection from Topaz ebooks and extracts the contents" + print u"Usage:" + print u" {0} [-k ] [-p ] [-s ] ".format(progname) # Main -def main(argv=sys.argv): - global buildXML +def cli_main(argv=unicode_argv()): progname = os.path.basename(argv[0]) - k4 = False - pids = [] - serials = [] - kInfoFiles = [] + print u"TopazExtract v{0}.".format(__version__) try: - opts, args = getopt.getopt(sys.argv[1:], "k:p:s:") + opts, args = getopt.getopt(sys.argv[1:], "k:p:s:x") except getopt.GetoptError, err: - print str(err) + print u"Error in options or arguments: {0}".format(err.args[0]) usage(progname) return 1 if len(args)<2: usage(progname) return 1 - for o, a in opts: - if o == "-k": - if a == None : - print "Invalid parameter for -k" - return 1 - kInfoFiles.append(a) - if o == "-p": - if a == None : - print "Invalid parameter for -p" - return 1 - pids = a.split(',') - if o == "-s": - if a == None : - print "Invalid parameter for -s" - return 1 - serials = a.split(',') - k4 = True - infile = args[0] outdir = args[1] - if not os.path.isfile(infile): - print "Input File Does Not Exist" + print u"Input File {0} Does Not Exist.".format(infile) return 1 + if not os.path.exists(outdir): + print u"Output Directory {0} Does Not Exist.".format(outdir) + return 1 + + kInfoFiles = [] + serials = [] + pids = [] + + for o, a in opts: + if o == '-k': + if a == None : + raise DrmException("Invalid parameter for -k") + kInfoFiles.append(a) + if o == '-p': + if a == None : + raise DrmException("Invalid parameter for -p") + pids = a.split(',') + if o == '-s': + if a == None : + raise DrmException("Invalid parameter for -s") + serials = [serial.replace(" ","") for serial in a.split(',')] + bookname = os.path.splitext(os.path.basename(infile))[0] tb = TopazBook(infile) title = tb.getBookTitle() - print "Processing Book: ", title - keysRecord, keysRecordRecord = tb.getPIDMetaInfo() - pids.extend(kgenpids.getPidList(keysRecord, keysRecordRecord, k4, serials, kInfoFiles)) + print u"Processing Book: {0}".format(title) + md1, md2 = tb.getPIDMetaInfo() + pids.extend(kgenpids.getPidList(md1, md2, serials, kInfoFiles)) try: - print "Decrypting Book" + print u"Decrypting Book" tb.processBook(pids) - print " Creating HTML ZIP Archive" - zipname = os.path.join(outdir, bookname + '_nodrm' + '.htmlz') - tb.getHTMLZip(zipname) + print u" Creating HTML ZIP Archive" + zipname = os.path.join(outdir, bookname + u"_nodrm.htmlz") + tb.getFile(zipname) - print " Creating SVG ZIP Archive" - zipname = os.path.join(outdir, bookname + '_SVG' + '.zip') + print u" Creating SVG ZIP Archive" + zipname = os.path.join(outdir, bookname + u"_SVG.zip") tb.getSVGZip(zipname) - if buildXML: - print " Creating XML ZIP Archive" - zipname = os.path.join(outdir, bookname + '_XML' + '.zip') - tb.getXMLZip(zipname) - # removing internal temporary directory of pieces tb.cleanup() - except TpzDRMError, e: - print str(e) - # tb.cleanup() + except DrmException, e: + print u"Decryption failed\n{0}".format(traceback.format_exc()) + + try: + tb.cleanup() + except: + pass return 1 except Exception, e: - print str(e) - # tb.cleanup + print u"Decryption failed\m{0}".format(traceback.format_exc()) + try: + tb.cleanup() + except: + pass return 1 return 0 if __name__ == '__main__': - sys.stdout=Unbuffered(sys.stdout) - sys.exit(main()) + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) + sys.exit(cli_main()) diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/zipfix.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/zipfix.py index c7921f2..eaee20d 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/zipfix.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/zipfix.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- import sys import zlib @@ -27,14 +28,10 @@ class fixZip: self.ztype = 'zip' if zinput.lower().find('.epub') >= 0 : self.ztype = 'epub' - print "opening input" self.inzip = zipfilerugged.ZipFile(zinput,'r') - print "opening outout" self.outzip = zipfilerugged.ZipFile(zoutput,'w') - print "opening input as raw file" # open the input zip for reading only as a raw file self.bzf = file(zinput,'rb') - print "finished initialising" def getlocalname(self, zi): local_header_offset = zi.header_offset 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 a0ef90d..d0a2bea 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/DeDRM_app.pyw +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/DeDRM_app.pyw @@ -1,9 +1,12 @@ #!/usr/bin/env python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab +# -*- coding: utf-8 -*- + +# DeDRM.pyw, version 5.5 +# By some_updates and Apprentice Alf import sys import os, os.path -sys.path.append(sys.path[0]+os.sep+'lib') +sys.path.append(os.path.join(sys.path[0],"lib")) os.environ['PYTHONIOENCODING'] = "utf-8" import shutil @@ -21,7 +24,7 @@ import re import simpleprefs -__version__ = '5.4.1' +__version__ = '5.5' class DrmException(Exception): pass @@ -327,7 +330,7 @@ class ConvDialog(Toplevel): self.running = 'inactive' self.numgood = 0 self.numbad = 0 - self.log = '' + self.log = u"" self.status = Tkinter.Label(self, text='DeDRM processing...') self.status.pack(fill=Tkconstants.X, expand=1) body = Tkinter.Frame(self) @@ -375,18 +378,16 @@ class ConvDialog(Toplevel): if len(self.filenames) > 0: filename = self.filenames.pop(0) if filename == None: - msg = '\nComplete: ' - msg += 'Successes: %d, ' % self.numgood - msg += 'Failures: %d\n' % self.numbad + msg = u"\nComplete: Successes: {0}, Failures: {1}\n".format(self.numgood,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) + file(logfile,'w').write(self.log.encode('utf8')) return infile = filename bname = os.path.basename(infile) - msg = 'Processing: ' + bname + ' ... ' + msg = u"Processing: {0} ... ".format(bname) self.log += msg self.showCmdOutput(msg) outdir = os.path.dirname(filename) @@ -400,7 +401,7 @@ class ConvDialog(Toplevel): self.running = 'active' self.processPipe() else: - msg = 'Unknown File: ' + bname + '\n' + msg = u"Unknown File: {0}\n".format(bname) self.log += msg self.showCmdOutput(msg) self.numbad += 1 @@ -433,18 +434,17 @@ class ConvDialog(Toplevel): if poll != None: self.bar.stop() if poll == 0: - msg = 'Success\n' + msg = u"\nSuccess\n" self.numgood += 1 - text = self.p2.read() - text += self.p2.readerr() + text = self.p2.read().decode('utf8') + text += self.p2.readerr().decode('utf8') self.log += text self.log += msg - if poll != 0: - msg = 'Failed\n' - text = self.p2.read() - text += self.p2.readerr() + else: + text = self.p2.read().decode('utf8') + text += self.p2.readerr().decode('utf8') msg += text - msg += '\n' + msg += u"\nFailed\n" self.numbad += 1 self.log += msg self.showCmdOutput(msg) @@ -491,7 +491,7 @@ def runit(apphome, ncmd, nparms): # 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) + 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): 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 e25a0c8..b1b0606 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/alfcrypto.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/alfcrypto.py @@ -1,11 +1,18 @@ -#! /usr/bin/env python +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# crypto library mainly by some_updates + +# pbkdf2.py pbkdf2 code taken from pbkdf2.py +# pbkdf2.py Copyright © 2004 Matt Johnston +# pbkdf2.py Copyright © 2009 Daniel Holth +# pbkdf2.py This code may be freely used and modified for any purpose. import sys, os import hmac from struct import pack import hashlib - # interface to needed routines libalfcrypto def _load_libalfcrypto(): import ctypes @@ -26,8 +33,8 @@ def _load_libalfcrypto(): name_of_lib = 'libalfcrypto32.so' else: name_of_lib = 'libalfcrypto64.so' - - libalfcrypto = sys.path[0] + os.sep + name_of_lib + + libalfcrypto = os.path.join(sys.path[0],name_of_lib) if not os.path.isfile(libalfcrypto): raise Exception('libalfcrypto not found') @@ -55,7 +62,7 @@ def _load_libalfcrypto(): # # int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key); # - # + # # void AES_cbc_encrypt(const unsigned char *in, unsigned char *out, # const unsigned long length, const AES_KEY *key, # unsigned char *ivec, const int enc); @@ -147,7 +154,7 @@ def _load_libalfcrypto(): topazCryptoDecrypt(ctx, data, out, len(data)) return out.raw - print "Using Library AlfCrypto DLL/DYLIB/SO" + print u"Using Library AlfCrypto DLL/DYLIB/SO" return (AES_CBC, Pukall_Cipher, Topaz_Cipher) @@ -164,8 +171,7 @@ def _load_python_alfcrypto(): sum2 = 0; keyXorVal = 0; if len(key)!=16: - print "Bad key length!" - return None + raise Exception('Pukall_Cipher: Bad key length.') wkey = [] for i in xrange(8): wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1])) @@ -234,6 +240,7 @@ def _load_python_alfcrypto(): cleartext = self.aes.decrypt(iv + data) return cleartext + print u"Using Library AlfCrypto Python" return (AES_CBC, Pukall_Cipher, Topaz_Cipher) diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/config.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/config.py index 9825878..9521540 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/config.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/config.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + from PyQt4.Qt import QWidget, QVBoxLayout, QLabel, QLineEdit from calibre.utils.config import JSONConfig 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 c412d7b..0f64a1b 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/convert2xml.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/convert2xml.py @@ -230,6 +230,7 @@ class PageParser(object): 'empty' : (1, 'snippets', 1, 0), 'page' : (1, 'snippets', 1, 0), + 'page.class' : (1, 'scalar_text', 0, 0), 'page.pageid' : (1, 'scalar_text', 0, 0), 'page.pagelabel' : (1, 'scalar_text', 0, 0), 'page.type' : (1, 'scalar_text', 0, 0), @@ -238,11 +239,13 @@ class PageParser(object): 'page.startID' : (1, 'scalar_number', 0, 0), 'group' : (1, 'snippets', 1, 0), + 'group.class' : (1, 'scalar_text', 0, 0), 'group.type' : (1, 'scalar_text', 0, 0), 'group._tag' : (1, 'scalar_text', 0, 0), 'group.orientation': (1, 'scalar_text', 0, 0), 'region' : (1, 'snippets', 1, 0), + 'region.class' : (1, 'scalar_text', 0, 0), 'region.type' : (1, 'scalar_text', 0, 0), 'region.x' : (1, 'scalar_number', 0, 0), 'region.y' : (1, 'scalar_number', 0, 0), diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/decryptpdb.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/decryptpdb.py index 12b8c10..f0775c1 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/decryptpdb.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/decryptpdb.py @@ -35,7 +35,7 @@ def main(argv=sys.argv): except ValueError: print ' Error parsing user supplied social drm data.' return 1 - rv = erdr2pml.decryptBook(infile, outdir, name, cc8, True) + rv = erdr2pml.decryptBook(infile, outdir, True, erdr2pml.getuser_key(name, cc8) ) if rv == 0: break return rv diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/encodebase64.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/encodebase64.py new file mode 100644 index 0000000..6bb8c37 --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/encodebase64.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# base64.py, version 1.0 +# Copyright © 2010 Apprentice Alf + +# Released under the terms of the GNU General Public Licence, version 3 or +# later. + +# Revision history: +# 1 - Initial release. To allow Applescript to do base64 encoding + +""" +Provide base64 encoding. +""" + +from __future__ import with_statement + +__license__ = 'GPL v3' + +import sys +import os +import base64 + +def usage(progname): + print "Applies base64 encoding to the supplied file, sending to standard output" + print "Usage:" + print " %s " % progname + +def cli_main(argv=sys.argv): + progname = os.path.basename(argv[0]) + + if len(argv)<2: + usage(progname) + sys.exit(2) + + keypath = argv[1] + with open(keypath, 'rb') as f: + keyder = f.read() + print keyder.encode('base64') + return 0 + + +if __name__ == '__main__': + sys.exit(cli_main()) diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/epubtest.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/epubtest.py new file mode 100644 index 0000000..a44308e --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/epubtest.py @@ -0,0 +1,169 @@ +#!/usr/bin/python +# +# This is a python script. You need a Python interpreter to run it. +# For example, ActiveState Python, which exists for windows. +# +# Changelog drmcheck +# 1.00 - Initial version, with code from various other scripts +# 1.01 - Moved authorship announcement to usage section. +# +# Changelog drmcheck +# 1.00 - Cut to drmtest.py, testing ePub files only by Apprentice Alf +# +# Written in 2011 by Paul Durrant +# Released with unlicense. See http://unlicense.org/ +# +############################################################################# +# +# This is free and unencumbered software released into the public domain. +# +# Anyone is free to copy, modify, publish, use, compile, sell, or +# distribute this software, either in source code form or as a compiled +# binary, for any purpose, commercial or non-commercial, and by any +# means. +# +# In jurisdictions that recognize copyright laws, the author or authors +# of this software dedicate any and all copyright interest in the +# software to the public domain. We make this dedication for the benefit +# of the public at large and to the detriment of our heirs and +# successors. We intend this dedication to be an overt act of +# relinquishment in perpetuity of all present and future rights to this +# software under copyright law. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +############################################################################# +# +# It's still polite to give attribution if you do reuse this code. +# + +from __future__ import with_statement + +__version__ = '1.00' + +import sys, struct, os +import zlib +import zipfile +import xml.etree.ElementTree as etree + +NSMAP = {'adept': 'http://ns.adobe.com/adept', + 'enc': 'http://www.w3.org/2001/04/xmlenc#'} + +# 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) + +def unicode_argv(): + 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] + +_FILENAME_LEN_OFFSET = 26 +_EXTRA_LEN_OFFSET = 28 +_FILENAME_OFFSET = 30 +_MAX_SIZE = 64 * 1024 + + +def uncompress(cmpdata): + dc = zlib.decompressobj(-15) + data = '' + while len(cmpdata) > 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(file, zi): + # get file name length and exta data length to find start of file data + local_header_offset = zi.header_offset + + file.seek(local_header_offset + _FILENAME_LEN_OFFSET) + leninfo = file.read(2) + local_name_length, = struct.unpack(' 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"mobidedrm.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] + Des = None -if sys.platform.startswith('win'): +if iswindows: # first try with pycrypto if inCalibre: from calibre_plugins.erdrpdb2pml import pycrypto_des @@ -168,17 +221,30 @@ class Sectionizer(object): off = self.sections[section][0] return self.contents[off:end_off] -def sanitizeFileName(s): - r = '' - for c in s: - if c in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-": - r += c - return r +# cleanup unicode filenames +# borrowed from calibre from calibre/src/calibre/__init__.py +# added in removal of control (<32) chars +# and removal of . at start and end +# and with some (heavily edited) code from Paul Durrant's kindlenamer.py +def sanitizeFileName(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"\'") + # 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 + name = re.sub(ur"\s", u" ", name).strip() + # remove leading dots + while len(name)>0 and name[0] == u".": + name = name[1:] + # remove trailing dots (Windows doesn't like them) + if name.endswith(u'.'): + name = name[:-1] + return name def fixKey(key): def fixByte(b): return b ^ ((b ^ (b<<1) ^ (b<<2) ^ (b<<3) ^ (b<<4) ^ (b<<5) ^ (b<<6) ^ (b<<7) ^ 0x80) & 0x80) - return "".join([chr(fixByte(ord(a))) for a in key]) + return "".join([chr(fixByte(ord(a))) for a in key]) def deXOR(text, sp, table): r='' @@ -191,7 +257,7 @@ def deXOR(text, sp, table): return r class EreaderProcessor(object): - def __init__(self, sect, username, creditcard): + def __init__(self, sect, user_key): self.section_reader = sect.loadSection data = self.section_reader(0) version, = struct.unpack('>H', data[0:2]) @@ -212,18 +278,10 @@ class EreaderProcessor(object): for i in xrange(len(data)): j = (j + shuf) % len(data) r[j] = data[i] - assert len("".join(r)) == len(data) + assert len("".join(r)) == len(data) return "".join(r) r = unshuff(input[0:-8], cookie_shuf) - def fixUsername(s): - r = '' - for c in s.lower(): - if (c >= 'a' and c <= 'z' or c >= '0' and c <= '9'): - r += c - return r - - user_key = struct.pack('>LL', binascii.crc32(fixUsername(username)) & 0xffffffff, binascii.crc32(creditcard[-8:])& 0xffffffff) drm_sub_version = struct.unpack('>H', r[0:2])[0] self.num_text_pages = struct.unpack('>H', r[2:4])[0] - 1 self.num_image_pages = struct.unpack('>H', r[26:26+2])[0] @@ -302,7 +360,7 @@ class EreaderProcessor(object): sect = self.section_reader(self.first_image_page + i) name = sect[4:4+32].strip('\0') data = sect[62:] - return sanitizeFileName(name), data + return sanitizeFileName(unicode(name,'windows-1252')), data # def getChapterNamePMLOffsetData(self): @@ -399,60 +457,53 @@ class EreaderProcessor(object): return r def cleanPML(pml): - # Convert special characters to proper PML code. High ASCII start at (\x80, \a128) and go up to (\xff, \a255) + # Convert special characters to proper PML code. High ASCII start at (\x80, \a128) and go up to (\xff, \a255) pml2 = pml for k in xrange(128,256): badChar = chr(k) pml2 = pml2.replace(badChar, '\\a%03d' % k) return pml2 -def convertEreaderToPml(infile, name, cc, outdir): - if not os.path.exists(outdir): - os.makedirs(outdir) +def decryptBook(infile, outpath, make_pmlz, user_key): bookname = os.path.splitext(os.path.basename(infile))[0] - print " Decoding File" - sect = Sectionizer(infile, 'PNRdPPrs') - er = EreaderProcessor(sect, name, cc) - - if er.getNumImages() > 0: - print " Extracting images" - imagedir = bookname + '_img/' - imagedirpath = os.path.join(outdir,imagedir) - if not os.path.exists(imagedirpath): - os.makedirs(imagedirpath) - for i in xrange(er.getNumImages()): - name, contents = er.getImage(i) - file(os.path.join(imagedirpath, name), 'wb').write(contents) - - print " Extracting pml" - pml_string = er.getText() - pmlfilename = bookname + ".pml" - file(os.path.join(outdir, pmlfilename),'wb').write(cleanPML(pml_string)) - - # bkinfo = er.getBookInfo() - # if bkinfo != '': - # print " Extracting book meta information" - # file(os.path.join(outdir, 'bookinfo.txt'),'wb').write(bkinfo) - - - -def decryptBook(infile, outdir, name, cc, make_pmlz): - if make_pmlz : - # ignore specified outdir, use tempdir instead + if make_pmlz: + # outpath is actually pmlz name + pmlzname = outpath outdir = tempfile.mkdtemp() + imagedirpath = os.path.join(outdir,u"images") + else: + pmlzname = None + outdir = outpath + imagedirpath = os.path.join(outdir,bookname + u"_img") + try: - print "Processing..." - convertEreaderToPml(infile, name, cc, outdir) - if make_pmlz : + if not os.path.exists(outdir): + os.makedirs(outdir) + print u"Decoding File" + sect = Sectionizer(infile, 'PNRdPPrs') + er = EreaderProcessor(sect, user_key) + + if er.getNumImages() > 0: + print u"Extracting images" + if not os.path.exists(imagedirpath): + os.makedirs(imagedirpath) + for i in xrange(er.getNumImages()): + name, contents = er.getImage(i) + file(os.path.join(imagedirpath, name), 'wb').write(contents) + + print u"Extracting pml" + pml_string = er.getText() + pmlfilename = bookname + ".pml" + file(os.path.join(outdir, pmlfilename),'wb').write(cleanPML(pml_string)) + if pmlzname is not None: import zipfile import shutil - print " Creating PMLZ file" - zipname = infile[:-4] + '.pmlz' - myZipFile = zipfile.ZipFile(zipname,'w',zipfile.ZIP_STORED, False) + print u"Creating PMLZ file {0}".format(os.path.basename(pmlzname)) + myZipFile = zipfile.ZipFile(pmlzname,'w',zipfile.ZIP_STORED, False) list = os.listdir(outdir) - for file in list: - localname = file - filePath = os.path.join(outdir,file) + for filename in list: + localname = filename + filePath = os.path.join(outdir,filename) if os.path.isfile(filePath): myZipFile.write(filePath, localname) elif os.path.isdir(filePath): @@ -466,36 +517,46 @@ def decryptBook(infile, outdir, name, cc, make_pmlz): myZipFile.close() # remove temporary directory shutil.rmtree(outdir, True) - print 'output is %s' % zipname + print u"Output is {0}".format(pmlzname) else : - print 'output in %s' % outdir + print u"Output is in {0}".format(outdir) print "done" except ValueError, e: - print "Error: %s" % e + print u"Error: {0}".format(e.args[0]) return 1 return 0 def usage(): - print "Converts DRMed eReader books to PML Source" - print "Usage:" - print " erdr2pml [options] infile.pdb [outdir] \"your name\" credit_card_number " - print " " - print "Options: " - print " -h prints this message" - print " --make-pmlz create PMLZ instead of using output directory" - print " " - print "Note:" - print " if ommitted, outdir defaults based on 'infile.pdb'" - print " It's enough to enter the last 8 digits of the credit card number" + print u"Converts DRMed eReader books to PML Source" + print u"Usage:" + print u" erdr2pml [options] infile.pdb [outpath] \"your name\" credit_card_number" + print u" " + print u"Options: " + print u" -h prints this message" + print u" -p create PMLZ instead of source folder" + print u" --make-pmlz create PMLZ instead of source folder" + print u" " + print u"Note:" + print u" if outpath is ommitted, creates source in 'infile_Source' folder" + print u" if outpath is ommitted and pmlz option, creates PMLZ 'infile.pmlz'" + print u" if source folder created, images are in infile_img folder" + print u" if pmlz file created, images are in images folder" + print u" It's enough to enter the last 8 digits of the credit card number" return +def getuser_key(name,cc): + newname = "".join(c for c in name.lower() if c >= 'a' and c <= 'z' or c >= '0' and c <= '9') + cc = cc.replace(" ","") + return struct.pack('>LL', binascii.crc32(newname) & 0xffffffff,binascii.crc32(cc[-8:])& 0xffffffff) + +def cli_main(argv=unicode_argv()): + print u"eRdr2Pml v{0}. Copyright © 2009–2012 The Dark Reverser et al.".format(__version__) -def main(argv=None): try: - opts, args = getopt.getopt(sys.argv[1:], "h", ["make-pmlz"]) + opts, args = getopt.getopt(argv[1:], "hp", ["make-pmlz"]) except getopt.GetoptError, err: - print str(err) + print err.args[0] usage() return 1 make_pmlz = False @@ -503,24 +564,31 @@ def main(argv=None): if o == "-h": usage() return 0 + elif o == "-p": + make_pmlz = True elif o == "--make-pmlz": make_pmlz = True - print "eRdr2Pml v%s. Copyright (c) 2009 The Dark Reverser" % __version__ - if len(args)!=3 and len(args)!=4: usage() return 1 if len(args)==3: - infile, name, cc = args[0], args[1], args[2] - outdir = infile[:-4] + '_Source' + infile, name, cc = args + if make_pmlz: + outpath = os.path.splitext(infile)[0] + u".pmlz" + else: + outpath = os.path.splitext(infile)[0] + u"_Source" elif len(args)==4: - infile, outdir, name, cc = args[0], args[1], args[2], args[3] + infile, outpath, name, cc = args - return decryptBook(infile, outdir, name, cc, make_pmlz) + print getuser_key(name,cc).encode('hex') + + return decryptBook(infile, outpath, make_pmlz, getuser_key(name,cc)) if __name__ == "__main__": - sys.stdout=Unbuffered(sys.stdout) - sys.exit(main()) + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) + sys.exit(cli_main()) + 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 03aa91f..2e0bd06 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignobleepub.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignobleepub.py @@ -1,13 +1,25 @@ -#! /usr/bin/python +#!/usr/bin/env python +# -*- coding: utf-8 -*- from __future__ import with_statement -# ignobleepub.pyw, version 3.5 +# ignobleepub.pyw, version 3.6 +# Copyright © 2009-2010 by i♥cabbages -# To run this program install Python 2.6 from -# and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto -# (make sure to install the version for Python 2.6). Save this script file as -# ignobleepub.pyw and double-click on it to run it. +# 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 +# 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. # Revision history: # 1 - Initial release @@ -18,21 +30,83 @@ from __future__ import with_statement # 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 +""" +Decrypt Barnes & Noble encrypted ePub books. +""" __license__ = 'GPL v3' +__version__ = "3.6" 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 -import Tkinter -import Tkconstants -import tkFileDialog -import tkMessageBox + +# 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') + +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)] + 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] + class IGNOBLEError(Exception): pass @@ -42,10 +116,11 @@ def _load_crypto_libcrypto(): Structure, c_ulong, create_string_buffer, cast from ctypes.util import find_library - if sys.platform.startswith('win'): + if iswindows: libcrypto = find_library('libeay32') else: libcrypto = find_library('crypto') + if libcrypto is None: raise IGNOBLEError('libcrypto not found') libcrypto = CDLL(libcrypto) @@ -66,9 +141,6 @@ def _load_crypto_libcrypto(): func.argtypes = argtypes return func - AES_cbc_encrypt = F(None, 'AES_cbc_encrypt', - [c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p, - c_int]) AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key', [c_char_p, c_int, AES_KEY_p]) AES_cbc_encrypt = F(None, 'AES_cbc_encrypt', @@ -123,13 +195,6 @@ def _load_crypto(): AES = _load_crypto() - - -""" -Decrypt Barnes & Noble ADEPT encrypted EPUB books. -""" - - 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#'} @@ -144,7 +209,6 @@ class ZipInfo(zipfile.ZipInfo): class Decryptor(object): def __init__(self, bookkey, encryption): enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag) - # self._aes = AES.new(bookkey, AES.MODE_CBC, '\x00'*16) self._aes = AES(bookkey) encryption = etree.fromstring(encryption) self._encrypted = encrypted = set() @@ -152,8 +216,8 @@ class Decryptor(object): enc('CipherReference')) for elem in encryption.findall(expr): path = elem.get('URI', None) - path = path.encode('utf-8') if path is not None: + path = path.encode('utf-8') encrypted.add(path) def decompress(self, bytes): @@ -171,167 +235,186 @@ class Decryptor(object): data = self.decompress(data) return data - -class DecryptionDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - self.status = Tkinter.Label(self, text='Select files for decryption') - self.status.pack(fill=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='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('bnepubkey.b64'): - self.keypath.insert(0, 'bnepubkey.b64') - button = Tkinter.Button(body, text="...", command=self.get_keypath) - button.grid(row=0, column=2) - Tkinter.Label(body, text='Input file').grid(row=1) - self.inpath = Tkinter.Entry(body, width=30) - self.inpath.grid(row=1, column=1, sticky=sticky) - button = Tkinter.Button(body, text="...", command=self.get_inpath) - button.grid(row=1, column=2) - Tkinter.Label(body, text='Output file').grid(row=2) - self.outpath = Tkinter.Entry(body, width=30) - self.outpath.grid(row=2, column=1, sticky=sticky) - button = Tkinter.Button(body, text="...", command=self.get_outpath) - button.grid(row=2, column=2) - buttons = Tkinter.Frame(self) - buttons.pack() - botton = Tkinter.Button( - buttons, text="Decrypt", width=10, command=self.decrypt) - botton.pack(side=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 B&N EPUB key file', - defaultextension='.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='Select B&N-encrypted EPUB file to decrypt', - defaultextension='.epub', filetypes=[('EPUB files', '.epub'), - ('All files', '.*')]) - 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='Select unencrypted EPUB file to produce', - defaultextension='.epub', filetypes=[('EPUB files', '.epub'), - ('All files', '.*')]) - 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'] = 'Specified key file does not exist' - return - if not inpath or not os.path.exists(inpath): - self.status['text'] = 'Specified input file does not exist' - return - if not outpath: - self.status['text'] = 'Output file not specified' - return - if inpath == outpath: - self.status['text'] = 'Must have different input and output files' - return - argv = [sys.argv[0], keypath, inpath, outpath] - self.status['text'] = 'Decrypting...' - try: - cli_main(argv) - except Exception, e: - self.status['text'] = 'Error: ' + str(e) - return - self.status['text'] = 'File successfully decrypted' - - -def decryptBook(keypath, inpath, outpath): - with open(keypath, 'rb') as f: - keyb64 = f.read() - key = keyb64.decode('base64')[:16] - # aes = AES.new(key, AES.MODE_CBC, '\x00'*16) - aes = AES(key) - +# 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: - raise IGNOBLEError('%s: not an B&N ADEPT EPUB' % (inpath,)) + 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) - 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)) - 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)) - return 0 + 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=sys.argv): +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 "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,) + print u"usage: {0} ".format(progname) return 1 keypath, inpath, outpath = argv[1:] - return decryptBook(keypath, inpath, outpath) - + 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() - if AES is None: - root.withdraw() - tkMessageBox.showerror( - "Ignoble EPUB Decrypter", - "This script requires OpenSSL or PyCrypto, which must be installed " - "separately. Read the top-of-script comment for details.") - return 1 - root.title('Ignoble EPUB Decrypter') + 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/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignoblekeygen.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignoblekeygen.py index e2c50e2..f25359c 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignoblekeygen.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignoblekeygen.py @@ -1,13 +1,25 @@ -#! /usr/bin/python +#!/usr/bin/env python +# -*- coding: utf-8 -*- from __future__ import with_statement -# ignoblekeygen.pyw, version 2.4 +# ignoblekeygen.pyw, version 2.5 +# Copyright © 2009-2010 by i♥cabbages -# To run this program install Python 2.6 from -# and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto -# (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. +# 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 @@ -16,36 +28,92 @@ from __future__ import with_statement # 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] -# use openssl's libcrypt if it exists in place of pycrypto -# code extracted from the Adobe Adept DRM removal code also by I HeartCabbages 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 sys.platform.startswith('win'): + if iswindows: libcrypto = find_library('libeay32') else: libcrypto = find_library('crypto') + if libcrypto is None: - print 'libcrypto not found' raise IGNOBLEError('libcrypto not found') libcrypto = CDLL(libcrypto) @@ -70,6 +138,7 @@ def _load_crypto_libcrypto(): 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) @@ -88,7 +157,6 @@ def _load_crypto_libcrypto(): return AES - def _load_crypto_pycrypto(): from Crypto.Cipher import AES as _AES @@ -120,25 +188,28 @@ def normalize_name(name): return ''.join(x for x in name.lower() if x != ' ') -def generate_keyfile(name, ccn, outpath): +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() - with open(outpath, 'wb') as f: - f.write(userkey.encode('base64')) - return userkey + return userkey.encode('base64') -def cli_main(argv=sys.argv): +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 " \ @@ -146,10 +217,11 @@ def cli_main(argv=sys.argv): (progname,) return 1 if len(argv) != 4: - print "usage: %s NAME CC# OUTFILE" % (progname,) + print u"usage: {0} ".format(progname) return 1 - name, ccn, outpath = argv[1:] - generate_keyfile(name, ccn, outpath) + name, ccn, keypath = argv[1:] + userkey = generate_key(name, ccn) + open(keypath,'wb').write(userkey) return 0 @@ -162,38 +234,38 @@ def gui_main(): class DecryptionDialog(Tkinter.Frame): def __init__(self, root): Tkinter.Frame.__init__(self, root, border=5) - self.status = Tkinter.Label(self, text='Enter parameters') + 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='Account Name').grid(row=0) + 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='CC#').grid(row=1) + 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='Output file').grid(row=2) + 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, 'bnepubkey.b64') - button = Tkinter.Button(body, text="...", command=self.get_keypath) + 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="Generate", width=10, command=self.generate) + 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="Quit", width=10, command=self.quit) + buttons, text=u"Quit", width=10, command=self.quit) button.pack(side=Tkconstants.RIGHT) - + def get_keypath(self): keypath = tkFileDialog.asksaveasfilename( - parent=None, title='Select B&N EPUB key file to produce', - defaultextension='.b64', + parent=None, title=u"Select B&N ePub key file to produce", + defaultextension=u".b64", filetypes=[('base64-encoded files', '.b64'), ('All Files', '.*')]) if keypath: @@ -201,27 +273,28 @@ def gui_main(): 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'] = 'Name not specified' + self.status['text'] = u"Name not specified" return if not ccn: - self.status['text'] = 'Credit card number not specified' + self.status['text'] = u"Credit card number not specified" return if not keypath: - self.status['text'] = 'Output keyfile path not specified' + self.status['text'] = u"Output keyfile path not specified" return - self.status['text'] = 'Generating...' + self.status['text'] = u"Generating..." try: - generate_keyfile(name, ccn, keypath) + userkey = generate_key(name, ccn) except Exception, e: - self.status['text'] = 'Error: ' + str(e) + self.status['text'] = u"Error: (0}".format(e.args[0]) return - self.status['text'] = 'Keyfile successfully generated' + open(keypath,'wb').write(userkey) + self.status['text'] = u"Keyfile successfully generated" root = Tkinter.Tk() if AES is None: @@ -231,7 +304,7 @@ def gui_main(): "This script requires OpenSSL or PyCrypto, which must be installed " "separately. Read the top-of-script comment for details.") return 1 - root.title('Ignoble EPUB Keyfile Generator') + 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) @@ -240,5 +313,7 @@ def gui_main(): 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/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ineptepub.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ineptepub.py index 2bb32b1..4b5a296 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ineptepub.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ineptepub.py @@ -3,11 +3,13 @@ from __future__ import with_statement -# ineptepub.pyw, version 5.6 -# Copyright © 2009-2010 i♥cabbages +# ineptepub.pyw, version 5.8 +# Copyright © 2009-2010 by i♥cabbages -# 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 +# + +# 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 @@ -31,24 +33,83 @@ from __future__ import with_statement # 5.5 - On Windows try PyCrypto first, OpenSSL next # 5.6 - Modify interface to allow use with import # 5.7 - Fix for potential problem with PyCrypto +# 5.8 - Revised to allow use in calibre plugins to eliminate need for duplicate code """ -Decrypt Adobe ADEPT-encrypted EPUB books. +Decrypt Adobe Digital Editions encrypted ePub books. """ __license__ = 'GPL v3' +__version__ = "5.8" 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 -import Tkinter -import Tkconstants -import tkFileDialog -import tkMessageBox + +# 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') + +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)] + 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] + class ADEPTError(Exception): pass @@ -58,7 +119,7 @@ def _load_crypto_libcrypto(): Structure, c_ulong, create_string_buffer, cast from ctypes.util import find_library - if sys.platform.startswith('win'): + if iswindows: libcrypto = find_library('libeay32') else: libcrypto = find_library('crypto') @@ -272,6 +333,7 @@ def _load_crypto(): except (ImportError, ADEPTError): pass return (AES, RSA) + AES, RSA = _load_crypto() META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml') @@ -314,158 +376,181 @@ class Decryptor(object): data = self.decompress(data) return data - -class DecryptionDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - self.status = Tkinter.Label(self, text='Select files for decryption') - self.status.pack(fill=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='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) - 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(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-encrypted EPUB file to decrypt', - defaultextension='.epub', filetypes=[('EPUB files', '.epub'), - ('All files', '.*')]) - 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='Select unencrypted EPUB file to produce', - defaultextension='.epub', filetypes=[('EPUB files', '.epub'), - ('All files', '.*')]) - 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'] = 'Specified key file does not exist' - return - if not inpath or not os.path.exists(inpath): - self.status['text'] = 'Specified input file does not exist' - return - if not outpath: - self.status['text'] = 'Output file not specified' - return - if inpath == outpath: - self.status['text'] = 'Must have different input and output files' - return - argv = [sys.argv[0], keypath, inpath, outpath] - self.status['text'] = 'Decrypting...' - try: - cli_main(argv) - except Exception, e: - self.status['text'] = 'Error: ' + str(e) - return - self.status['text'] = 'File successfully decrypted' - - -def decryptBook(keypath, inpath, outpath): - with open(keypath, 'rb') as f: - keyder = f.read() - rsa = RSA(keyder) +# check file to make check whether it's probably an Adobe Adept encrypted ePub +def adeptBook(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: - raise ADEPTError('%s: not an ADEPT EPUB' % (inpath,)) + 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) == 172: + return True + except: + # if we couldn't check, assume it is + return True + return False + +def decryptBook(userkey, inpath, outpath): + if AES is None: + raise ADEPTError(u"PyCrypto or OpenSSL must be installed.") + rsa = RSA(userkey) + 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) - 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)) - bookkey = rsa.decrypt(bookkey.decode('base64')) - # Padded as per RSAES-PKCS1-v1_5 - if bookkey[-17] != '\x00': - raise ADEPTError('problem decrypting session key') - 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)) + 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) != 172: + print u"{0:s} is not a secure Adobe Adept ePub.".format(os.path.basename(inpath)) + return 1 + bookkey = rsa.decrypt(bookkey.decode('base64')) + # Padded as per RSAES-PKCS1-v1_5 + if bookkey[-17] != '\x00': + print u"Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath)) + return 2 + 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=sys.argv): +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 "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,) + print u"usage: {0} ".format(progname) return 1 keypath, inpath, outpath = argv[1:] - return decryptBook(keypath, inpath, outpath) - + 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 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"adeptkey.der"): + self.keypath.insert(0, u"adeptkey.der") + 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 Adobe Adept \'.der\' key file", + defaultextension=u".der", + filetypes=[('Adobe Adept DER-encoded files', '.der'), + ('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 ADEPT-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) + return + 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( - "INEPT EPUB Decrypter", - "This script requires OpenSSL or PyCrypto, which must be" - " installed separately. Read the top-of-script comment for" - " details.") - return 1 - root.title('INEPT EPUB Decrypter') + root.title(u"Adobe Adept ePub Decrypter v.{0}".format(__version__)) root.resizable(True, False) root.minsize(300, 0) DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1) @@ -474,5 +559,7 @@ def gui_main(): 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/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ineptkey.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ineptkey.py index 723b7c6..a9bc62d 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ineptkey.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ineptkey.py @@ -6,8 +6,8 @@ from __future__ import with_statement # ineptkey.pyw, version 5.6 # Copyright © 2009-2010 i♥cabbages -# 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 +# # Windows users: Before running this program, you must first install Python 2.6 # from and PyCrypto from @@ -37,7 +37,7 @@ from __future__ import with_statement # 5.3 - On Windows try PyCrypto first, OpenSSL next # 5.4 - Modify interface to allow use of import # 5.5 - Fix for potential problem with PyCrypto -# 5.6 - Revise to allow use in Plugins to eliminate need for duplicate code +# 5.6 - Revised to allow use in Plugins to eliminate need for duplicate code """ Retrieve Adobe ADEPT user key. @@ -49,12 +49,65 @@ import sys import os import struct +# 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') +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)] + return [u"ineptkey.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 ADEPTError(Exception): pass @@ -80,13 +133,13 @@ if iswindows: _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_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', @@ -308,9 +361,9 @@ if iswindows: cuser = winreg.HKEY_CURRENT_USER try: regkey = winreg.OpenKey(cuser, DEVICE_KEY_PATH) + device = winreg.QueryValueEx(regkey, 'key')[0] except WindowsError: raise ADEPTError("Adobe Digital Editions not activated") - device = winreg.QueryValueEx(regkey, 'key')[0] keykey = CryptUnprotectData(device, entropy) userkey = None keys = [] @@ -343,7 +396,7 @@ if iswindows: if len(keys) == 0: raise ADEPTError('Could not locate privateLicenseKey') return keys - + elif isosx: import xml.etree.ElementTree as etree @@ -386,7 +439,7 @@ else: def retrieve_keys(keypath): raise ADEPTError("This script only supports Windows and Mac OS X.") return [] - + def retrieve_key(keypath): keys = retrieve_keys() with open(keypath, 'wb') as f: @@ -397,22 +450,22 @@ def extractKeyfile(keypath): try: success = retrieve_key(keypath) except ADEPTError, e: - print "Key generation Error: " + str(e) + print u"Key generation Error: {0}".format(e.args[0]) return 1 except Exception, e: - print "General Error: " + str(e) + print "General Error: {0}".format(e.args[0]) return 1 if not success: return 1 return 0 -def cli_main(argv=sys.argv): +def cli_main(argv=unicode_argv()): keypath = argv[1] return extractKeyfile(keypath) -def main(argv=sys.argv): +def gui_main(argv=unicode_argv()): import Tkinter import Tkconstants import tkMessageBox @@ -421,24 +474,24 @@ def main(argv=sys.argv): class ExceptionDialog(Tkinter.Frame): def __init__(self, root, text): Tkinter.Frame.__init__(self, root, border=5) - label = Tkinter.Label(self, text="Unexpected error:", + label = Tkinter.Label(self, text=u"Unexpected error:", anchor=Tkconstants.W, justify=Tkconstants.LEFT) label.pack(fill=Tkconstants.X, expand=0) self.text = Tkinter.Text(self) self.text.pack(fill=Tkconstants.BOTH, expand=1) - + self.text.insert(Tkconstants.END, text) root = Tkinter.Tk() root.withdraw() - progname = os.path.basename(argv[0]) - keypath = os.path.abspath("adeptkey.der") + keypath, progname = os.path.split(argv[0]) + keypath = os.path.join(keypath, u"adeptkey.der") success = False try: success = retrieve_key(keypath) except ADEPTError, e: - tkMessageBox.showerror("ADEPT Key", "Error: " + str(e)) + tkMessageBox.showerror(u"ADEPT Key", "Error: {0}".format(e.args[0])) except Exception: root.wm_state('normal') root.title('ADEPT Key') @@ -448,10 +501,12 @@ def main(argv=sys.argv): if not success: return 1 tkMessageBox.showinfo( - "ADEPT Key", "Key successfully retrieved to %s" % (keypath)) + u"ADEPT Key", u"Key successfully retrieved to {0}".format(keypath)) 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(main()) + sys.exit(gui_main()) diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ineptpdf.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ineptpdf.py index 20721d1..9f4883e 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ineptpdf.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ineptpdf.py @@ -1,13 +1,25 @@ -#! /usr/bin/env python -# ineptpdf.pyw, version 7.11 +#! /usr/bin/python +# -*- coding: utf-8 -*- from __future__ import with_statement -# To run this program install Python 2.6 from http://www.python.org/download/ -# and OpenSSL (already installed on Mac OS X and Linux) OR -# PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto -# (make sure to install the version for Python 2.6). Save this script file as -# ineptpdf.pyw and double-click on it to run it. +# ineptpdf.pyw, version 7.11 +# 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 +# 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. # Revision history: # 1 - Initial release @@ -36,12 +48,14 @@ from __future__ import with_statement # 7.9 - Bug fix for some session key errors when len(bookkey) > length required # 7.10 - Various tweaks to fix minor problems. # 7.11 - More tweaks to fix minor problems. +# 7.12 - Revised to allow use in calibre plugins to eliminate need for duplicate code """ Decrypts Adobe ADEPT-encrypted PDF files. """ __license__ = 'GPL v3' +__version__ = "7.12" import sys import os @@ -51,10 +65,63 @@ import struct import hashlib from itertools import chain, islice import xml.etree.ElementTree as etree -import Tkinter -import Tkconstants -import tkFileDialog -import tkMessageBox + +# 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)] + 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] + class ADEPTError(Exception): pass @@ -1520,9 +1587,7 @@ class PDFDocument(object): def initialize_ebx(self, password, docid, param): self.is_printable = self.is_modifiable = self.is_extractable = True - with open(password, 'rb') as f: - keyder = f.read() - rsa = RSA(keyder) + rsa = RSA(password) length = int_value(param.get('Length', 0)) / 8 rights = str_value(param.get('ADEPT_LICENSE')).decode('base64') rights = zlib.decompress(rights, -15) @@ -1907,14 +1972,14 @@ class PDFObjStrmParser(PDFParser): ### My own code, for which there is none else to blame class PDFSerializer(object): - def __init__(self, inf, keypath): + def __init__(self, inf, userkey): global GEN_XREF_STM, gen_xref_stm gen_xref_stm = GEN_XREF_STM > 1 self.version = inf.read(8) inf.seek(0) self.doc = doc = PDFDocument() parser = PDFParser(doc, inf) - doc.initialize(keypath) + doc.initialize(userkey) self.objids = objids = set() for xref in reversed(doc.xrefs): trailer = xref.trailer @@ -2097,142 +2162,144 @@ class PDFSerializer(object): self.write('endobj\n') -class DecryptionDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - ltext='Select file for decryption\n' - 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='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) - 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 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 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): - 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 - self.status['text'] = 'Specified Adept key file does not exist' - return - if not inpath or not os.path.exists(inpath): - self.status['text'] = 'Specified input file does not exist' - return - if not outpath: - self.status['text'] = 'Output file not specified' - return - if inpath == outpath: - self.status['text'] = 'Must have different input and output files' - return - # patch for non-ascii characters - 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 decryptBook(keypath, inpath, outpath): +def decryptBook(userkey, inpath, outpath): + if RSA is None: + raise ADEPTError(u"PyCrypto or OpenSSL must be installed.") with open(inpath, 'rb') as inf: try: - serializer = PDFSerializer(inf, keypath) + serializer = PDFSerializer(inf, userkey) except: - print "Error serializing pdf. Probably wrong key." - return 1 + print u"Error serializing pdf {0}. Probably wrong key.".format(os.path.basename(inpath)) + return 2 # 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 + # help construct to make sure the method runs to the end try: serializer.dump(outf) - except: - print "error writing pdf." - return 1 + except Exception, e: + print u"error writing pdf: {0}".format(e.args[0]) + return 2 return 0 -def cli_main(argv=sys.argv): +def cli_main(argv=unicode_argv()): progname = os.path.basename(argv[0]) - if RSA 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 "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,) + print u"usage: {0} ".format(progname) return 1 keypath, inpath, outpath = argv[1:] - return decryptBook(keypath, inpath, outpath) + 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 + + 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"adeptkey.der"): + self.keypath.insert(0, u"adeptkey.der") + 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 Adobe Adept \'.der\' key file", + defaultextension=u".der", + filetypes=[('Adobe Adept DER-encoded files', '.der'), + ('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 ADEPT-encrypted PDF file to decrypt", + defaultextension=u".pdf", filetypes=[('PDF files', '.pdf')]) + 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 PDF file to produce", + defaultextension=u".pdf", filetypes=[('PDF files', '.pdf')]) + 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: + self.status['text'] = u"File successfully decrypted" + else: + self.status['text'] = u"The was an error decrypting the file." + + root = Tkinter.Tk() if RSA is None: root.withdraw() @@ -2241,7 +2308,7 @@ def gui_main(): "This script requires OpenSSL or PyCrypto, which must be installed " "separately. Read the top-of-script comment for details.") return 1 - root.title('INEPT PDF Decrypter') + root.title(u"Adobe Adept PDF Decrypter v.{0}".format(__version__)) root.resizable(True, False) root.minsize(370, 0) DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1) @@ -2251,5 +2318,7 @@ def gui_main(): 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/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/k4mobidedrm.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/k4mobidedrm.py index 717b0d0..8adb107 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/k4mobidedrm.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/k4mobidedrm.py @@ -1,7 +1,11 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- from __future__ import with_statement +# ignobleepub.pyw, version 3.6 +# Copyright © 2009-2012 by DiapDealer et al. + # engine to remove drm from Kindle for Mac and Kindle for PC books # for personal use for archiving and converting your ebooks @@ -12,30 +16,51 @@ from __future__ import with_statement # be able to read OUR books on whatever device we want and to keep # readable for a long, long time -# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle, +# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle, # unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates # and many many others +# Special thanks to The Dark Reverser for MobiDeDrm and CMBDTC for cmbdtc_dump +# from which this script borrows most unashamedly. -__version__ = '4.4' +# Changelog +# 1.0 - Name change to k4mobidedrm. Adds Mac support, Adds plugin code +# 1.1 - Adds support for additional kindle.info files +# 1.2 - Better error handling for older Mobipocket +# 1.3 - Don't try to decrypt Topaz books +# 1.7 - Add support for Topaz books and Kindle serial numbers. Split code. +# 1.9 - Tidy up after Topaz, minor exception changes +# 2.1 - Topaz fix and filename sanitizing +# 2.2 - Topaz Fix and minor Mac code fix +# 2.3 - More Topaz fixes +# 2.4 - K4PC/Mac key generation fix +# 2.6 - Better handling of non-K4PC/Mac ebooks +# 2.7 - Better trailing bytes handling in mobidedrm +# 2.8 - Moved parsing of kindle.info files to mac & pc util files. +# 3.1 - Updated for new calibre interface. Now __init__ in plugin. +# 3.5 - Now support Kindle for PC/Mac 1.6 +# 3.6 - Even better trailing bytes handling in mobidedrm +# 3.7 - Add support for Amazon Print Replica ebooks. +# 3.8 - Improved Topaz support +# 4.1 - Improved Topaz support and faster decryption with alfcrypto +# 4.2 - Added support for Amazon's KF8 format ebooks +# 4.4 - Linux calls to Wine added, and improved configuration dialog +# 4.5 - Linux works again without Wine. Some Mac key file search changes +# 4.6 - First attempt to handle unicode properly +# 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. -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) +__version__ = '4.8' -import sys -import os, csv, getopt -import string + +import sys, os, re +import csv +import getopt import re import traceback import time - -buildXML = False +import htmlentitydefs class DrmException(Exception): pass @@ -54,161 +79,203 @@ else: import topazextract import kgenpids +# 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) -# cleanup bytestring filenames +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"mobidedrm.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] + +# cleanup unicode filenames # borrowed from calibre from calibre/src/calibre/__init__.py -# added in removal of non-printing chars -# and removal of . at start -# convert underscores to spaces (we're OK with spaces in file names) +# added in removal of control (<32) chars +# and removal of . at start and end +# and with some (heavily edited) code from Paul Durrant's kindlenamer.py def cleanup_name(name): - _filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+/]') - substitute='_' - one = ''.join(char for char in name if char in string.printable) - one = _filename_sanitize.sub(substitute, one) - one = re.sub(r'\s', ' ', one).strip() - one = re.sub(r'^\.+$', '_', one) - one = one.replace('..', substitute) - # Windows doesn't like path components that end with a period - if one.endswith('.'): - one = one[:-1]+substitute - # Mac and Unix don't like file names that begin with a full stop - if len(one) > 0 and one[0] == '.': - one = substitute+one[1:] - one = one.replace('_',' ') - return one - -def decryptBook(infile, outdir, k4, kInfoFiles, serials, pids): - global buildXML + # 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"\'") + # 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 + name = re.sub(ur"\s", u" ", name).strip() + # remove leading dots + while len(name)>0 and name[0] == u".": + name = name[1:] + # remove trailing dots (Windows doesn't like them) + if name.endswith(u'.'): + name = name[:-1] + return name +# must be passed unicode +def unescape(text): + def fixup(m): + text = m.group(0) + if text[:2] == u"&#": + # character reference + try: + if text[:3] == u"&#x": + return unichr(int(text[3:-1], 16)) + else: + return unichr(int(text[2:-1])) + except ValueError: + pass + else: + # named entity + try: + text = unichr(htmlentitydefs.name2codepoint[text[1:-1]]) + except KeyError: + pass + return text # leave as is + return re.sub(u"&#?\w+;", fixup, text) +def GetDecryptedBook(infile, kInfoFiles, serials, pids, starttime = time.time()): # handle the obvious cases at the beginning if not os.path.isfile(infile): - print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: Input file does not exist" - return 1 - - starttime = time.time() - print "Starting decryptBook routine." - + raise DRMException (u"Input file does not exist.") mobi = True magic3 = file(infile,'rb').read(3) if magic3 == 'TPZ': mobi = False - bookname = os.path.splitext(os.path.basename(infile))[0] - if mobi: mb = mobidedrm.MobiBook(infile) else: mb = topazextract.TopazBook(infile) - title = mb.getBookTitle() - print "Processing Book: ", title - filenametitle = cleanup_name(title) - outfilename = cleanup_name(bookname) + bookname = unescape(mb.getBookTitle()) + print u"Decrypting {1} ebook: {0}".format(bookname, mb.getBookType()) - # generate 'sensible' filename, that will sort with the original name, - # but is close to the name from the file. - outlength = len(outfilename) - comparelength = min(8,min(outlength,len(filenametitle))) - copylength = min(max(outfilename.find(' '),8),len(outfilename)) - if outlength==0: - outfilename = filenametitle - elif comparelength > 0: - if outfilename[:comparelength] == filenametitle[:comparelength]: - outfilename = filenametitle - else: - outfilename = outfilename[:copylength] + " " + filenametitle + # extend PID list with book-specific PIDs + md1, md2 = mb.getPIDMetaInfo() + pids.extend(kgenpids.getPidList(md1, md2, serials, kInfoFiles)) + print u"Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(pids)) + + try: + mb.processBook(pids) + except: + mb.cleanup + raise + + print u"Decryption succeeded after {0:.1f} seconds".format(time.time()-starttime) + return mb + + +# infile, outdir and kInfoFiles should be unicode strings +def decryptBook(infile, outdir, kInfoFiles, serials, pids): + starttime = time.time() + print "Starting decryptBook routine." + try: + 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) + return 1 + + # if we're saving to the same folder as the original, use file name_ + # if to a different folder, use book name + if os.path.normcase(os.path.normpath(outdir)) == os.path.normcase(os.path.normpath(os.path.dirname(infile))): + outfilename = os.path.splitext(os.path.basename(infile))[0] + else: + outfilename = cleanup_name(book.getBookTitle()) # avoid excessively long file names if len(outfilename)>150: outfilename = outfilename[:150] - # build pid list - md1, md2 = mb.getPIDMetaInfo() - pids.extend(kgenpids.getPidList(md1, md2, k4, serials, kInfoFiles)) + outfilename = outfilename+u"_nodrm" + outfile = os.path.join(outdir, outfilename + book.getBookExtension()) - print "Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(pids)) + book.getFile(outfile) + print u"Saved decrypted book {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename) - - try: - mb.processBook(pids) - - except mobidedrm.DrmException, e: - print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: " + str(e) + "\nDRM Removal Failed.\n" - print "Failed to decrypted book after {0:.1f} seconds".format(time.time()-starttime) - return 1 - except topazextract.TpzDRMError, e: - print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: " + str(e) + "\nDRM Removal Failed.\n" - print "Failed to decrypted book after {0:.1f} seconds".format(time.time()-starttime) - return 1 - except Exception, e: - print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: " + str(e) + "\nDRM Removal Failed.\n" - print "Failed to decrypted book after {0:.1f} seconds".format(time.time()-starttime) - return 1 - - print "Successfully decrypted book after {0:.1f} seconds".format(time.time()-starttime) - - if mobi: - if mb.getPrintReplica(): - outfile = os.path.join(outdir, outfilename + '_nodrm' + '.azw4') - elif mb.getMobiVersion() >= 8: - outfile = os.path.join(outdir, outfilename + '_nodrm' + '.azw3') - else: - outfile = os.path.join(outdir, outfilename + '_nodrm' + '.mobi') - mb.getMobiFile(outfile) - print "Saved decrypted book {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename + '_nodrm') - return 0 - - # topaz: - print " Creating NoDRM HTMLZ Archive" - zipname = os.path.join(outdir, outfilename + '_nodrm' + '.htmlz') - mb.getHTMLZip(zipname) - - print " Creating SVG ZIP Archive" - zipname = os.path.join(outdir, outfilename + '_SVG' + '.zip') - mb.getSVGZip(zipname) - - if buildXML: - print " Creating XML ZIP Archive" - zipname = os.path.join(outdir, outfilename + '_XML' + '.zip') - mb.getXMLZip(zipname) + if book.getBookType()==u"Topaz": + zipname = os.path.join(outdir, outfilename + u"_SVG.zip") + book.getSVGZip(zipname) + print u"Saved SVG ZIP Archive for {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename) # remove internal temporary directory of Topaz pieces - mb.cleanup() - print "Saved decrypted Topaz book parts after {0:.1f} seconds".format(time.time()-starttime) - return 0 + book.cleanup() def usage(progname): - print "Removes DRM protection from K4PC/M, Kindle, Mobi and Topaz ebooks" - print "Usage:" - print " %s [-k ] [-p ] [-s ] " % progname + print u"Removes DRM protection from Mobipocket, Amazon KF8, Amazon Print Replica and Amazon Topaz ebooks" + print u"Usage:" + print u" {0} [-k ] [-p ] [-s ] ".format(progname) # # Main # -def main(argv=sys.argv): +def cli_main(argv=unicode_argv()): progname = os.path.basename(argv[0]) - - k4 = False - kInfoFiles = [] - serials = [] - pids = [] - - print ('K4MobiDeDrm v%(__version__)s ' - 'provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc .' % globals()) + print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__) try: opts, args = getopt.getopt(sys.argv[1:], "k:p:s:") except getopt.GetoptError, err: - print str(err) + print u"Error in options or arguments: {0}".format(err.args[0]) usage(progname) sys.exit(2) if len(args)<2: usage(progname) sys.exit(2) + infile = args[0] + outdir = args[1] + kInfoFiles = [] + serials = [] + pids = [] + for o, a in opts: if o == "-k": if a == None : @@ -223,16 +290,13 @@ def main(argv=sys.argv): raise DrmException("Invalid parameter for -s") serials = a.split(',') - # try with built in Kindle Info files - k4 = True - if sys.platform.startswith('linux'): - k4 = False - kInfoFiles = None - infile = args[0] - outdir = args[1] - return decryptBook(infile, outdir, k4, kInfoFiles, serials, pids) + # try with built in Kindle Info files if not on Linux + k4 = not sys.platform.startswith('linux') + + return decryptBook(infile, outdir, kInfoFiles, serials, pids) if __name__ == '__main__': - sys.stdout=Unbuffered(sys.stdout) - sys.exit(main()) + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) + sys.exit(cli_main()) diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/k4mutils.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/k4mutils.py index 1fc08cb..bceb3a3 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/k4mutils.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/k4mutils.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + # standlone set of Mac OSX specific routines needed for KindleBooks from __future__ import with_statement @@ -22,7 +25,7 @@ def _load_crypto_libcrypto(): libcrypto = find_library('crypto') if libcrypto is None: - raise DrmException('libcrypto not found') + raise DrmException(u"libcrypto not found") libcrypto = CDLL(libcrypto) # From OpenSSL's crypto aes header @@ -80,14 +83,14 @@ def _load_crypto_libcrypto(): def set_decrypt_key(self, userkey, iv): self._blocksize = len(userkey) if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : - raise DrmException('AES improper key used') + raise DrmException(u"AES improper key used") return keyctx = self._keyctx = AES_KEY() self._iv = iv self._userkey = userkey rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx) if rv < 0: - raise DrmException('Failed to initialize AES key') + raise DrmException(u"Failed to initialize AES key") def decrypt(self, data): out = create_string_buffer(len(data)) @@ -95,7 +98,7 @@ def _load_crypto_libcrypto(): keyctx = self._keyctx rv = AES_cbc_encrypt(data, out, len(data), keyctx, mutable_iv, 0) if rv == 0: - raise DrmException('AES decryption failed') + raise DrmException(u"AES decryption failed") return out.raw def keyivgen(self, passwd, salt, iter, keylen): @@ -139,20 +142,20 @@ def SHA256(message): return ctx.digest() # Various character maps used to decrypt books. Probably supposed to act as obfuscation -charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" -charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM" +charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M' +charMap2 = 'ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM' # For kinf approach of K4Mac 1.6.X or later -# On K4PC charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE" +# On K4PC charMap5 = 'AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE' # For Mac they seem to re-use charMap2 here charMap5 = charMap2 # new in K4M 1.9.X -testMap8 = "YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD" +testMap8 = 'YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD' def encode(data, map): - result = "" + result = '' for char in data: value = ord(char) Q = (value ^ 0x80) // len(map) @@ -167,14 +170,14 @@ def encodeHash(data,map): # Decode the string in data with the characters in map. Returns the decoded bytes def decode(data,map): - result = "" + result = '' for i in range (0,len(data)-1,2): high = map.find(data[i]) low = map.find(data[i+1]) if (high == -1) or (low == -1) : break value = (((high * len(map)) ^ 0x80) & 0xFF) + low - result += pack("B",value) + result += pack('B',value) return result # For K4M 1.6.X and later @@ -200,7 +203,7 @@ def primes(n): # uses a sub process to get the Hard Drive Serial Number using ioreg -# returns with the serial number of drive whose BSD Name is "disk0" +# returns with the serial number of drive whose BSD Name is 'disk0' def GetVolumeSerialNumber(): sernum = os.getenv('MYSERIALNUMBER') if sernum != None: @@ -216,11 +219,11 @@ def GetVolumeSerialNumber(): foundIt = False for j in xrange(cnt): resline = reslst[j] - pp = resline.find('"Serial Number" = "') + pp = resline.find('\"Serial Number\" = \"') if pp >= 0: sernum = resline[pp+19:-1] sernum = sernum.strip() - bb = resline.find('"BSD Name" = "') + bb = resline.find('\"BSD Name\" = \"') if bb >= 0: bsdname = resline[bb+14:-1] bsdname = bsdname.strip() @@ -277,7 +280,7 @@ def GetDiskPartitionUUID(diskpart): nest += 1 if resline.find('}') >= 0: nest -= 1 - pp = resline.find('"UUID" = "') + pp = resline.find('\"UUID\" = \"') if pp >= 0: uuidnum = resline[pp+10:-1] uuidnum = uuidnum.strip() @@ -285,7 +288,7 @@ def GetDiskPartitionUUID(diskpart): if partnest == uuidnest and uuidnest > 0: foundIt = True break - bb = resline.find('"BSD Name" = "') + bb = resline.find('\"BSD Name\" = \"') if bb >= 0: bsdname = resline[bb+14:-1] bsdname = bsdname.strip() @@ -323,7 +326,7 @@ def GetMACAddressMunged(): if pp >= 0: macnum = resline[pp+6:-1] macnum = macnum.strip() - # print "original mac", macnum + # print 'original mac', macnum # now munge it up the way Kindle app does # by xoring it with 0xa5 and swapping elements 3 and 4 maclst = macnum.split(':') @@ -340,7 +343,7 @@ def GetMACAddressMunged(): mlst[2] = maclst[2] ^ 0xa5 mlst[1] = maclst[1] ^ 0xa5 mlst[0] = maclst[0] ^ 0xa5 - macnum = "%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x" % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5]) + macnum = '%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x' % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5]) foundIt = True break if not foundIt: @@ -367,6 +370,19 @@ def isNewInstall(): return False +class Memoize: + """Memoize(fn) - an instance which acts like fn but memoizes its arguments + Will only work on functions with non-mutable arguments + """ + def __init__(self, fn): + self.fn = fn + self.memo = {} + def __call__(self, *args): + if not self.memo.has_key(args): + self.memo[args] = self.fn(*args) + return self.memo[args] + +@Memoize def GetIDString(): # K4Mac now has an extensive set of ids strings it uses # in encoding pids and in creating unique passwords @@ -530,7 +546,8 @@ def getKindleInfoFiles(): # determine type of kindle info provided and return a # database of keynames and values def getDBfromFile(kInfoFile): - names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber", "max_date", "SIGVERIF"] + + names = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber', 'max_date', 'SIGVERIF'] DB = {} cnt = 0 infoReader = open(kInfoFile, 'r') @@ -545,12 +562,12 @@ def getDBfromFile(kInfoFile): for item in items: if item != '': keyhash, rawdata = item.split(':') - keyname = "unknown" + keyname = 'unknown' for name in names: if encodeHash(name,charMap2) == keyhash: keyname = name break - if keyname == "unknown": + if keyname == 'unknown': keyname = keyhash encryptedValue = decode(rawdata,charMap2) cleartext = cud.decrypt(encryptedValue) @@ -563,8 +580,8 @@ def getDBfromFile(kInfoFile): if hdr == '/': # else newer style .kinf file used by K4Mac >= 1.6.0 - # the .kinf file uses "/" to separate it into records - # so remove the trailing "/" to make it easy to use split + # the .kinf file uses '/' to separate it into records + # so remove the trailing '/' to make it easy to use split data = data[:-1] items = data.split('/') cud = CryptUnprotectDataV2() @@ -578,11 +595,11 @@ def getDBfromFile(kInfoFile): # the first 32 chars of the first record of a group # is the MD5 hash of the key name encoded by charMap5 keyhash = item[0:32] - keyname = "unknown" + keyname = 'unknown' # the raw keyhash string is also used to create entropy for the actual # CryptProtectData Blob that represents that keys contents - # "entropy" not used for K4Mac only K4PC + # 'entropy' not used for K4Mac only K4PC # entropy = SHA1(keyhash) # the remainder of the first record when decoded with charMap5 @@ -599,12 +616,12 @@ def getDBfromFile(kInfoFile): item = items.pop(0) edlst.append(item) - keyname = "unknown" + keyname = 'unknown' for name in names: if encodeHash(name,charMap5) == keyhash: keyname = name break - if keyname == "unknown": + if keyname == 'unknown': keyname = keyhash # the charMap5 encoded contents data has had a length @@ -615,10 +632,10 @@ def getDBfromFile(kInfoFile): # The offset into the charMap5 encoded contents seems to be: # len(contents) - largest prime number less than or equal to int(len(content)/3) - # (in other words split "about" 2/3rds of the way through) + # (in other words split 'about' 2/3rds of the way through) # move first offsets chars to end to align for decode by charMap5 - encdata = "".join(edlst) + encdata = ''.join(edlst) contlen = len(encdata) # now properly split and recombine @@ -667,7 +684,7 @@ def getDBfromFile(kInfoFile): # the first 32 chars of the first record of a group # is the MD5 hash of the key name encoded by charMap5 keyhash = item[0:32] - keyname = "unknown" + keyname = 'unknown' # unlike K4PC the keyhash is not used in generating entropy # entropy = SHA1(keyhash) + added_entropy @@ -687,12 +704,12 @@ def getDBfromFile(kInfoFile): item = items.pop(0) edlst.append(item) - keyname = "unknown" + keyname = 'unknown' for name in names: if encodeHash(name,testMap8) == keyhash: keyname = name break - if keyname == "unknown": + if keyname == 'unknown': keyname = keyhash # the testMap8 encoded contents data has had a length @@ -703,10 +720,10 @@ def getDBfromFile(kInfoFile): # The offset into the testMap8 encoded contents seems to be: # len(contents) - largest prime number less than or equal to int(len(content)/3) - # (in other words split "about" 2/3rds of the way through) + # (in other words split 'about' 2/3rds of the way through) # move first offsets chars to end to align for decode by testMap8 - encdata = "".join(edlst) + encdata = ''.join(edlst) contlen = len(encdata) # now properly split and recombine diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/k4pcutils.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/k4pcutils.py index 9f9ca07..476844c 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/k4pcutils.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/k4pcutils.py @@ -1,4 +1,6 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- + # K4PC Windows specific routines from __future__ import with_statement diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kgenpids.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kgenpids.py index b0fbaa4..c5de9b9 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kgenpids.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kgenpids.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- from __future__ import with_statement import sys @@ -17,26 +18,24 @@ global charMap4 if 'calibre' in sys.modules: inCalibre = True -else: - inCalibre = False - -if inCalibre: - if sys.platform.startswith('win'): + from calibre.constants import iswindows, isosx + if iswindows: from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString - - if sys.platform.startswith('darwin'): + if isosx: from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString else: - if sys.platform.startswith('win'): + inCalibre = False + iswindows = sys.platform.startswith('win') + isosx = sys.platform.startswith('darwin') + if iswindows: from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString - - if sys.platform.startswith('darwin'): + if isosx: from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString -charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" -charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" -charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" +charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M' +charMap3 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' +charMap4 = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789' # crypto digestroutines import hashlib @@ -54,7 +53,7 @@ def SHA1(message): # Encode the bytes in data with the characters in map def encode(data, map): - result = "" + result = '' for char in data: value = ord(char) Q = (value ^ 0x80) // len(map) @@ -69,14 +68,14 @@ def encodeHash(data,map): # Decode the string in data with the characters in map. Returns the decoded bytes def decode(data,map): - result = "" + result = '' for i in range (0,len(data)-1,2): high = map.find(data[i]) low = map.find(data[i+1]) if (high == -1) or (low == -1) : break value = (((high * len(map)) ^ 0x80) & 0xFF) + low - result += pack("B",value) + result += pack('B',value) return result # @@ -98,7 +97,7 @@ def getSixBitsFromBitField(bitField,offset): # 8 bits to six bits encoding from hash to generate PID string def encodePID(hash): global charMap3 - PID = "" + PID = '' for position in range (0,8): PID += charMap3[getSixBitsFromBitField(hash,position)] return PID @@ -129,7 +128,7 @@ def generatePidSeed(table,dsn) : def generateDevicePID(table,dsn,nbRoll): global charMap4 seed = generatePidSeed(table,dsn) - pidAscii = "" + pidAscii = '' pid = [(seed >>24) &0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF,(seed>>24) & 0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF] index = 0 for counter in range (0,nbRoll): @@ -176,28 +175,31 @@ def pidFromSerial(s, l): # Parse the EXTH header records and use the Kindle serial number to calculate the book pid. -def getKindlePid(pidlst, rec209, token, serialnum): +def getKindlePids(rec209, token, serialnum): + pids=[] + # Compute book PID pidHash = SHA1(serialnum+rec209+token) bookPID = encodePID(pidHash) bookPID = checksumPid(bookPID) - pidlst.append(bookPID) + pids.append(bookPID) # compute fixed pid for old pre 2.5 firmware update pid as well - bookPID = pidFromSerial(serialnum, 7) + "*" - bookPID = checksumPid(bookPID) - pidlst.append(bookPID) + kindlePID = pidFromSerial(serialnum, 7) + "*" + kindlePID = checksumPid(kindlePID) + pids.append(kindlePID) - return pidlst + return pids # parse the Kindleinfo file to calculate the book pid. -keynames = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"] +keynames = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber'] -def getK4Pids(pidlst, rec209, token, kInfoFile): +def getK4Pids(rec209, token, kInfoFile): global charMap1 kindleDatabase = None + pids = [] try: kindleDatabase = getDBfromFile(kInfoFile) except Exception, message: @@ -206,17 +208,17 @@ def getK4Pids(pidlst, rec209, token, kInfoFile): pass if kindleDatabase == None : - return pidlst + return pids try: # Get the Mazama Random number - MazamaRandomNumber = kindleDatabase["MazamaRandomNumber"] + MazamaRandomNumber = kindleDatabase['MazamaRandomNumber'] # Get the kindle account token - kindleAccountToken = kindleDatabase["kindle.account.tokens"] + kindleAccountToken = kindleDatabase['kindle.account.tokens'] except KeyError: - print "Keys not found in " + kInfoFile - return pidlst + print u"Keys not found in {0}".format(os.path.basename(kInfoFile)) + return pids # Get the ID string used encodedIDString = encodeHash(GetIDString(),charMap1) @@ -231,7 +233,7 @@ def getK4Pids(pidlst, rec209, token, kInfoFile): table = generatePidEncryptionTable() devicePID = generateDevicePID(table,DSN,4) devicePID = checksumPid(devicePID) - pidlst.append(devicePID) + pids.append(devicePID) # Compute book PIDs @@ -239,36 +241,38 @@ def getK4Pids(pidlst, rec209, token, kInfoFile): pidHash = SHA1(DSN+kindleAccountToken+rec209+token) bookPID = encodePID(pidHash) bookPID = checksumPid(bookPID) - pidlst.append(bookPID) + pids.append(bookPID) # variant 1 pidHash = SHA1(kindleAccountToken+rec209+token) bookPID = encodePID(pidHash) bookPID = checksumPid(bookPID) - pidlst.append(bookPID) + pids.append(bookPID) # variant 2 pidHash = SHA1(DSN+rec209+token) bookPID = encodePID(pidHash) bookPID = checksumPid(bookPID) - pidlst.append(bookPID) + pids.append(bookPID) - return pidlst + return pids -def getPidList(md1, md2, k4 = True, serials=[], kInfoFiles=[]): +def getPidList(md1, md2, serials=[], kInfoFiles=[]): pidlst = [] if kInfoFiles is None: kInfoFiles = [] - if k4: + if serials is None: + serials = [] + if iswindows or isosx: kInfoFiles.extend(getKindleInfoFiles()) for infoFile in kInfoFiles: try: - pidlst = getK4Pids(pidlst, md1, md2, infoFile) - except Exception, message: - print("Error getting PIDs from " + infoFile + ": " + message) + pidlst.extend(getK4Pids(md1, md2, infoFile)) + except Exception, e: + print u"Error getting PIDs from {0}: {1}".format(os.path.basename(infoFile),e.args[0]) for serialnum in serials: try: - pidlst = getKindlePid(pidlst, md1, md2, serialnum) + pidlst.extend(getKindlePids(md1, md2, serialnum)) except Exception, message: - print("Error getting PIDs from " + serialnum + ": " + message) + print u"Error getting PIDs from serial number {0}: {1}".format(serialnum ,e.args[0]) return pidlst diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kindlepid.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kindlepid.py new file mode 100644 index 0000000..38c5e4e --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kindlepid.py @@ -0,0 +1,142 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Mobipocket PID calculator v0.4 for Amazon Kindle. +# Copyright (c) 2007, 2009 Igor Skochinsky +# History: +# 0.1 Initial release +# 0.2 Added support for generating PID for iPhone (thanks to mbp) +# 0.3 changed to autoflush stdout, fixed return code usage +# 0.3 updated for unicode + +import sys +import binascii + +# 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"mobidedrm.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] + +if sys.hexversion >= 0x3000000: + print 'This script is incompatible with Python 3.x. Please install Python 2.7.x.' + sys.exit(2) + +letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789' + +def crc32(s): + return (~binascii.crc32(s,-1))&0xFFFFFFFF + +def checksumPid(s): + crc = crc32(s) + crc = crc ^ (crc >> 16) + res = s + l = len(letters) + for i in (0,1): + b = crc & 0xff + pos = (b // l) ^ (b % l) + res += letters[pos%l] + crc >>= 8 + + return res + + +def pidFromSerial(s, l): + crc = crc32(s) + + arr1 = [0]*l + for i in xrange(len(s)): + arr1[i%l] ^= ord(s[i]) + + crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff] + for i in xrange(l): + arr1[i] ^= crc_bytes[i&3] + + pid = '' + for i in xrange(l): + b = arr1[i] & 0xff + pid+=letters[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))] + + return pid + +def cli_main(argv=unicode_argv()): + print u"Mobipocket PID calculator for Amazon Kindle. Copyright © 2007, 2009 Igor Skochinsky" + if len(sys.argv)==2: + serial = sys.argv[1] + else: + print u"Usage: kindlepid.py /" + return 1 + if len(serial)==16: + if serial.startswith("B"): + print u"Kindle serial number detected" + else: + print u"Warning: unrecognized serial number. Please recheck input." + return 1 + pid = pidFromSerial(serial.encode("utf-8"),7)+'*' + print u"Mobipocket PID for Kindle serial#{0} is {1} ".format(serial,checksumPid(pid)) + return 0 + elif len(serial)==40: + print u"iPhone serial number (UDID) detected" + pid = pidFromSerial(serial.encode("utf-8"),8) + print u"Mobipocket PID for iPhone serial#{0} is {1} ".format(serial,checksumPid(pid)) + return 0 + print u"Warning: unrecognized serial number. Please recheck input." + return 1 + + +if __name__ == "__main__": + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) + sys.exit(cli_main()) diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/mobidedrm.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/mobidedrm.py index cd993e1..113f57a 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/mobidedrm.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/mobidedrm.py @@ -1,5 +1,11 @@ -#!/usr/bin/python +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# mobidedrm.py, version 0.38 +# Copyright © 2008 The Dark Reverser # +# Modified 2008–2012 by some_updates, DiapDealer and Apprentice Alf + # This is a python script. You need a Python interpreter to run it. # For example, ActiveState Python, which exists for windows. # @@ -59,26 +65,78 @@ # 0.35 - add interface to get mobi_version # 0.36 - fixed problem with TEXtREAd and getBookTitle interface # 0.37 - Fixed double announcement for stand-alone operation +# 0.38 - Unicode used wherever possible, cope with absent alfcrypto -__version__ = '0.37' +__version__ = u"0.38" import sys +import os +import struct +import binascii +try: + from alfcrypto import Pukall_Cipher +except: + print u"AlfCrypto not found. Using python PC1 implementation." -class Unbuffered: +# 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) -sys.stdout=Unbuffered(sys.stdout) -import os -import struct -import binascii -from alfcrypto import Pukall_Cipher +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"mobidedrm.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 DrmException(Exception): pass @@ -90,40 +148,45 @@ class DrmException(Exception): # Implementation of Pukall Cipher 1 def PC1(key, src, decryption=True): - return Pukall_Cipher().PC1(key,src,decryption) -# sum1 = 0; -# sum2 = 0; -# keyXorVal = 0; -# if len(key)!=16: -# print "Bad key length!" -# return None -# wkey = [] -# for i in xrange(8): -# wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1])) -# dst = "" -# for i in xrange(len(src)): -# temp1 = 0; -# byteXorVal = 0; -# for j in xrange(8): -# temp1 ^= wkey[j] -# sum2 = (sum2+j)*20021 + sum1 -# sum1 = (temp1*346)&0xFFFF -# sum2 = (sum2+sum1)&0xFFFF -# temp1 = (temp1*20021+1)&0xFFFF -# byteXorVal ^= temp1 ^ sum2 -# curByte = ord(src[i]) -# if not decryption: -# keyXorVal = curByte * 257; -# curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF -# if decryption: -# keyXorVal = curByte * 257; -# for j in xrange(8): -# wkey[j] ^= keyXorVal; -# dst+=chr(curByte) -# return dst + # if we can get it from alfcrypto, use that + try: + return Pukall_Cipher().PC1(key,src,decryption) + except NameError: + pass + + # use slow python version, since Pukall_Cipher didn't load + sum1 = 0; + sum2 = 0; + keyXorVal = 0; + if len(key)!=16: + DrmException (u"PC1: Bad key length") + wkey = [] + for i in xrange(8): + wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1])) + dst = "" + for i in xrange(len(src)): + temp1 = 0; + byteXorVal = 0; + for j in xrange(8): + temp1 ^= wkey[j] + sum2 = (sum2+j)*20021 + sum1 + sum1 = (temp1*346)&0xFFFF + sum2 = (sum2+sum1)&0xFFFF + temp1 = (temp1*20021+1)&0xFFFF + byteXorVal ^= temp1 ^ sum2 + curByte = ord(src[i]) + if not decryption: + keyXorVal = curByte * 257; + curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF + if decryption: + keyXorVal = curByte * 257; + for j in xrange(8): + wkey[j] ^= keyXorVal; + dst+=chr(curByte) + return dst def checksumPid(s): - letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" + letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789' crc = (~binascii.crc32(s,-1))&0xFFFFFFFF crc = crc ^ (crc >> 16) res = s @@ -171,17 +234,24 @@ class MobiBook: off = self.sections[section][0] return self.data_file[off:endoff] - def __init__(self, infile, announce = True): - if announce: - print ('MobiDeDrm v%(__version__)s. ' - 'Copyright 2008-2012 The Dark Reverser et al.' % globals()) + def cleanup(self): + # to match function in Topaz book + pass + + def __init__(self, infile): + print u"MobiDeDrm v{0:s}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__) + + try: + from alfcrypto import Pukall_Cipher + except: + print u"AlfCrypto not found. Using python PC1 implementation." # initial sanity check on file self.data_file = file(infile, 'rb').read() self.mobi_data = '' self.header = self.data_file[0:78] if self.header[0x3C:0x3C+8] != 'BOOKMOBI' and self.header[0x3C:0x3C+8] != 'TEXtREAd': - raise DrmException("invalid file format") + raise DrmException(u"Invalid file format") self.magic = self.header[0x3C:0x3C+8] self.crypto_type = -1 @@ -199,7 +269,7 @@ class MobiBook: self.compression, = struct.unpack('>H', self.sect[0x0:0x0+2]) if self.magic == 'TEXtREAd': - print "Book has format: ", self.magic + print u"PalmDoc format book detected." self.extra_data_flags = 0 self.mobi_length = 0 self.mobi_codepage = 1252 @@ -209,11 +279,11 @@ class MobiBook: self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18]) self.mobi_codepage, = struct.unpack('>L',self.sect[0x1c:0x20]) self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C]) - print "MOBI header version = %d, length = %d" %(self.mobi_version, self.mobi_length) + print u"MOBI header version {0:d}, header length {1:d}".format(self.mobi_version, self.mobi_length) self.extra_data_flags = 0 if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5): self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4]) - print "Extra Data Flags = %d" % self.extra_data_flags + print u"Extra Data Flags: {0:d}".format(self.extra_data_flags) if (self.compression != 17480): # multibyte utf8 data is included in the encryption for PalmDoc compression # so clear that byte so that we leave it to be decrypted. @@ -223,10 +293,10 @@ class MobiBook: self.meta_array = {} try: exth_flag, = struct.unpack('>L', self.sect[0x80:0x84]) - exth = 'NONE' + exth = '' if exth_flag & 0x40: exth = self.sect[16 + self.mobi_length:] - if (len(exth) >= 4) and (exth[:4] == 'EXTH'): + if (len(exth) >= 12) and (exth[:4] == 'EXTH'): nitems, = struct.unpack('>I', exth[8:12]) pos = 12 for i in xrange(nitems): @@ -236,10 +306,10 @@ class MobiBook: # reset the text to speech flag and clipping limit, if present if type == 401 and size == 9: # set clipping limit to 100% - self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8) + self.patchSection(0, '\144', 16 + self.mobi_length + pos + 8) elif type == 404 and size == 9: # make sure text to speech is enabled - self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8) + self.patchSection(0, '\0', 16 + self.mobi_length + pos + 8) # print type, size, content, content.encode('hex') pos += size except: @@ -265,8 +335,8 @@ class MobiBook: codec = codec_map[self.mobi_codepage] if title == '': title = self.header[:32] - title = title.split("\0")[0] - return unicode(title, codec).encode('utf-8') + title = title.split('\0')[0] + return unicode(title, codec) def getPIDMetaInfo(self): rec209 = '' @@ -297,7 +367,7 @@ class MobiBook: def parseDRM(self, data, count, pidlist): found_key = None - keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96" + keyvec1 = '\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96' for pid in pidlist: bigpid = pid.ljust(16,'\0') temp_key = PC1(keyvec1, bigpid, False) @@ -315,7 +385,7 @@ class MobiBook: break if not found_key: # Then try the default encoding that doesn't require a PID - pid = "00000000" + pid = '00000000' temp_key = keyvec1 temp_key_sum = sum(map(ord,temp_key)) & 0xff for i in xrange(count): @@ -328,82 +398,90 @@ class MobiBook: break return [found_key,pid] - def getMobiFile(self, outpath): + def getFile(self, outpath): file(outpath,'wb').write(self.mobi_data) - def getMobiVersion(self): - return self.mobi_version + def getBookType(self): + if self.print_replica: + return u"Print Replica" + if self.mobi_version >= 8: + return u"Kindle Format 8" + return u"Mobipocket" - def getPrintReplica(self): - return self.print_replica + def getBookExtension(self): + if self.print_replica: + return u".azw4" + if self.mobi_version >= 8: + return u".azw3" + return u".mobi" def processBook(self, pidlist): crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2]) - print 'Crypto Type is: ', crypto_type + print u"Crypto Type is: {0:d}".format(crypto_type) self.crypto_type = crypto_type if crypto_type == 0: - print "This book is not encrypted." + print u"This book is not encrypted." # we must still check for Print Replica self.print_replica = (self.loadSection(1)[0:4] == '%MOP') self.mobi_data = self.data_file return if crypto_type != 2 and crypto_type != 1: - raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type) + raise DrmException(u"Cannot decode unknown Mobipocket encryption type {0:d}".format(crypto_type)) if 406 in self.meta_array: data406 = self.meta_array[406] val406, = struct.unpack('>Q',data406) if val406 != 0: - raise DrmException("Cannot decode library or rented ebooks.") + raise DrmException(u"Cannot decode library or rented ebooks.") goodpids = [] for pid in pidlist: if len(pid)==10: if checksumPid(pid[0:-2]) != pid: - print "Warning: PID " + pid + " has incorrect checksum, should have been "+checksumPid(pid[0:-2]) + print u"Warning: PID {0} has incorrect checksum, should have been {1}".format(pid,checksumPid(pid[0:-2])) goodpids.append(pid[0:-2]) elif len(pid)==8: goodpids.append(pid) if self.crypto_type == 1: - t1_keyvec = "QDCVEPMU675RUBSZ" + t1_keyvec = 'QDCVEPMU675RUBSZ' if self.magic == 'TEXtREAd': bookkey_data = self.sect[0x0E:0x0E+16] elif self.mobi_version < 0: bookkey_data = self.sect[0x90:0x90+16] else: bookkey_data = self.sect[self.mobi_length+16:self.mobi_length+32] - pid = "00000000" + pid = '00000000' found_key = PC1(t1_keyvec, bookkey_data) else : # calculate the keys drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16]) if drm_count == 0: - raise DrmException("Not yet initialised with PID. Must be opened with Mobipocket Reader first.") + raise DrmException(u"Encryption not initialised. Must be opened with Mobipocket Reader first.") found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids) if not found_key: - raise DrmException("No key found in " + str(len(goodpids)) + " keys tried. Read the FAQs at Alf's blog. Only if none apply, report this failure for help.") + raise DrmException(u"No key found in {0:d} keys tried. Read the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(len(goodpids))) # kill the drm keys - self.patchSection(0, "\0" * drm_size, drm_ptr) + self.patchSection(0, '\0' * drm_size, drm_ptr) # kill the drm pointers - self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8) + self.patchSection(0, '\xff' * 4 + '\0' * 12, 0xA8) - if pid=="00000000": - print "File has default encryption, no specific PID." + if pid=='00000000': + print u"File has default encryption, no specific key needed." else: - print "File is encoded with PID "+checksumPid(pid)+"." + print u"File is encoded with PID {0}.".format(checksumPid(pid)) # clear the crypto type self.patchSection(0, "\0" * 2, 0xC) # decrypt sections - print "Decrypting. Please wait . . .", + print u"Decrypting. Please wait . . .", mobidataList = [] mobidataList.append(self.data_file[:self.sections[1][0]]) for i in xrange(1, self.records+1): data = self.loadSection(i) extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags) if i%100 == 0: - print ".", + print u".", # print "record %d, extra_size %d" %(i,extra_size) decoded_data = PC1(found_key, data[0:len(data) - extra_size]) if i==1: @@ -414,31 +492,24 @@ class MobiBook: if self.num_sections > self.records+1: mobidataList.append(self.data_file[self.sections[self.records+1][0]:]) self.mobi_data = "".join(mobidataList) - print "done" + print u"done" return -def getUnencryptedBook(infile,pid,announce=True): +def getUnencryptedBook(infile,pidlist): if not os.path.isfile(infile): - raise DrmException('Input File Not Found') - book = MobiBook(infile,announce) - book.processBook([pid]) - return book.mobi_data - -def getUnencryptedBookWithList(infile,pidlist,announce=True): - if not os.path.isfile(infile): - raise DrmException('Input File Not Found') - book = MobiBook(infile, announce) + raise DrmException(u"Input File Not Found.") + book = MobiBook(infile) book.processBook(pidlist) return book.mobi_data -def main(argv=sys.argv): - print ('MobiDeDrm v%(__version__)s. ' - 'Copyright 2008-2012 The Dark Reverser et al.' % globals()) +def cli_main(argv=unicode_argv()): + progname = os.path.basename(argv[0]) if len(argv)<3 or len(argv)>4: - print "Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks" - print "Usage:" - print " %s []" % sys.argv[0] + print u"MobiDeDrm v{0}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__) + print u"Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks" + print u"Usage:" + print u" {0} []".format(os.path.basename(sys.argv[0])) return 1 else: infile = argv[1] @@ -446,15 +517,17 @@ def main(argv=sys.argv): if len(argv) is 4: pidlist = argv[3].split(',') else: - pidlist = {} + pidlist = [] try: - stripped_file = getUnencryptedBookWithList(infile, pidlist, False) + stripped_file = getUnencryptedBook(infile, pidlist) file(outfile, 'wb').write(stripped_file) except DrmException, e: - print "Error: %s" % e + print u"MobiDeDRM v{0} Error: {0:s}".format(__version__,e.args[0]) return 1 return 0 -if __name__ == "__main__": - sys.exit(main()) +if __name__ == '__main__': + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) + sys.exit(cli_main()) diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/topazextract.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/topazextract.py index bf2ad47..a343922 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/topazextract.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/topazextract.py @@ -1,43 +1,90 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- -class Unbuffered: +# topazextract.py, version ? +# Mostly written by some_updates based on code from many others + +__version__ = '4.8' + +import sys +import os, csv, getopt +import zlib, zipfile, tempfile, shutil +import traceback +from struct import pack +from struct import unpack +from alfcrypto import Topaz_Cipher + +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) -import sys +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"mobidedrm.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] if 'calibre' in sys.modules: inCalibre = True -else: - inCalibre = False - -buildXML = False - -import os, csv, getopt -import zlib, zipfile, tempfile, shutil -from struct import pack -from struct import unpack -from alfcrypto import Topaz_Cipher - -class TpzDRMError(Exception): - pass - - -# local support routines -if inCalibre: from calibre_plugins.k4mobidedrm import kgenpids else: + inCalibre = False import kgenpids + +class DrmException(Exception): + pass + + # recursive zip creation support routine def zipUpDir(myzip, tdir, localname): currentdir = tdir - if localname != "": + if localname != u"": currentdir = os.path.join(currentdir,localname) list = os.listdir(currentdir) for file in list: @@ -73,7 +120,7 @@ def bookReadEncodedNumber(fo): # Get a length prefixed string from file def bookReadString(fo): stringLength = bookReadEncodedNumber(fo) - return unpack(str(stringLength)+"s",fo.read(stringLength))[0] + return unpack(str(stringLength)+'s',fo.read(stringLength))[0] # # crypto routines @@ -112,13 +159,13 @@ def decryptRecord(data,PID): # Try to decrypt a dkey record (contains the bookPID) def decryptDkeyRecord(data,PID): record = decryptRecord(data,PID) - fields = unpack("3sB8sB8s3s",record) - if fields[0] != "PID" or fields[5] != "pid" : - raise TpzDRMError("Didn't find PID magic numbers in record") + fields = unpack('3sB8sB8s3s',record) + if fields[0] != 'PID' or fields[5] != 'pid' : + raise DrmException(u"Didn't find PID magic numbers in record") elif fields[1] != 8 or fields[3] != 8 : - raise TpzDRMError("Record didn't contain correct length fields") + raise DrmException(u"Record didn't contain correct length fields") elif fields[2] != PID : - raise TpzDRMError("Record didn't contain PID") + raise DrmException(u"Record didn't contain PID") return fields[4] # Decrypt all dkey records (contain the book PID) @@ -131,11 +178,11 @@ def decryptDkeyRecords(data,PID): try: key = decryptDkeyRecord(data[1:length+1],PID) records.append(key) - except TpzDRMError: + except DrmException: pass data = data[1+length:] if len(records) == 0: - raise TpzDRMError("BookKey Not Found") + raise DrmException(u"BookKey Not Found") return records @@ -148,9 +195,9 @@ class TopazBook: self.bookHeaderRecords = {} self.bookMetadata = {} self.bookKey = None - magic = unpack("4s",self.fo.read(4))[0] + magic = unpack('4s',self.fo.read(4))[0] if magic != 'TPZ0': - raise TpzDRMError("Parse Error : Invalid Header, not a Topaz file") + raise DrmException(u"Parse Error : Invalid Header, not a Topaz file") self.parseTopazHeaders() self.parseMetadata() @@ -167,7 +214,7 @@ class TopazBook: # Read and parse one header record at the current book file position and return the associated data # [[offset,decompressedLength,compressedLength],...] if ord(self.fo.read(1)) != 0x63: - raise TpzDRMError("Parse Error : Invalid Header") + raise DrmException(u"Parse Error : Invalid Header") tag = bookReadString(self.fo) record = bookReadHeaderRecordData() return [tag,record] @@ -177,15 +224,15 @@ class TopazBook: # print result[0], result[1] self.bookHeaderRecords[result[0]] = result[1] if ord(self.fo.read(1)) != 0x64 : - raise TpzDRMError("Parse Error : Invalid Header") + raise DrmException(u"Parse Error : Invalid Header") self.bookPayloadOffset = self.fo.tell() def parseMetadata(self): # Parse the metadata record from the book payload and return a list of [key,values] - self.fo.seek(self.bookPayloadOffset + self.bookHeaderRecords["metadata"][0][0]) + self.fo.seek(self.bookPayloadOffset + self.bookHeaderRecords['metadata'][0][0]) tag = bookReadString(self.fo) - if tag != "metadata" : - raise TpzDRMError("Parse Error : Record Names Don't Match") + if tag != 'metadata' : + raise DrmException(u"Parse Error : Record Names Don't Match") flags = ord(self.fo.read(1)) nbRecords = ord(self.fo.read(1)) # print nbRecords @@ -210,7 +257,7 @@ class TopazBook: title = '' if 'Title' in self.bookMetadata: title = self.bookMetadata['Title'] - return title + return title.decode('utf-8') def setBookKey(self, key): self.bookKey = key @@ -223,13 +270,13 @@ class TopazBook: try: recordOffset = self.bookHeaderRecords[name][index][0] except: - raise TpzDRMError("Parse Error : Invalid Record, record not found") + raise DrmException("Parse Error : Invalid Record, record not found") self.fo.seek(self.bookPayloadOffset + recordOffset) tag = bookReadString(self.fo) if tag != name : - raise TpzDRMError("Parse Error : Invalid Record, record name doesn't match") + raise DrmException("Parse Error : Invalid Record, record name doesn't match") recordIndex = bookReadEncodedNumber(self.fo) if recordIndex < 0 : @@ -237,7 +284,7 @@ class TopazBook: recordIndex = -recordIndex -1 if recordIndex != index : - raise TpzDRMError("Parse Error : Invalid Record, index doesn't match") + raise DrmException("Parse Error : Invalid Record, index doesn't match") if (self.bookHeaderRecords[name][index][2] > 0): compressed = True @@ -250,7 +297,7 @@ class TopazBook: ctx = topazCryptoInit(self.bookKey) record = topazCryptoDecrypt(record,ctx) else : - raise TpzDRMError("Error: Attempt to decrypt without bookKey") + raise DrmException("Error: Attempt to decrypt without bookKey") if compressed: record = zlib.decompress(record) @@ -262,12 +309,12 @@ class TopazBook: fixedimage=True try: keydata = self.getBookPayloadRecord('dkey', 0) - except TpzDRMError, e: - print "no dkey record found, book may not be encrypted" - print "attempting to extrct files without a book key" + except DrmException, e: + print u"no dkey record found, book may not be encrypted" + print u"attempting to extrct files without a book key" self.createBookDirectory() self.extractFiles() - print "Successfully Extracted Topaz contents" + print u"Successfully Extracted Topaz contents" if inCalibre: from calibre_plugins.k4mobidedrm import genbook else: @@ -275,7 +322,7 @@ class TopazBook: rv = genbook.generateBook(self.outdir, raw, fixedimage) if rv == 0: - print "\nBook Successfully generated" + print u"Book Successfully generated." return rv # try each pid to decode the file @@ -283,25 +330,25 @@ class TopazBook: for pid in pidlst: # use 8 digit pids here pid = pid[0:8] - print "\nTrying: ", pid + print u"Trying: {0}".format(pid) bookKeys = [] data = keydata try: bookKeys+=decryptDkeyRecords(data,pid) - except TpzDRMError, e: + except DrmException, e: pass else: bookKey = bookKeys[0] - print "Book Key Found!" + print u"Book Key Found! ({0})".format(bookKey.encode('hex')) break if not bookKey: - raise TpzDRMError("Topaz Book. No key found in " + str(len(pidlst)) + " keys tried. Read the FAQs at Alf's blog. Only if none apply, report this failure for help.") + raise DrmException(u"No key found in {0:d} keys tried. Read the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(len(pidlst))) self.setBookKey(bookKey) self.createBookDirectory() self.extractFiles() - print "Successfully Extracted Topaz contents" + print u"Successfully Extracted Topaz contents" if inCalibre: from calibre_plugins.k4mobidedrm import genbook else: @@ -309,7 +356,7 @@ class TopazBook: rv = genbook.generateBook(self.outdir, raw, fixedimage) if rv == 0: - print "\nBook Successfully generated" + print u"Book Successfully generated" return rv def createBookDirectory(self): @@ -317,16 +364,16 @@ class TopazBook: # create output directory structure if not os.path.exists(outdir): os.makedirs(outdir) - destdir = os.path.join(outdir,'img') + destdir = os.path.join(outdir,u"img") if not os.path.exists(destdir): os.makedirs(destdir) - destdir = os.path.join(outdir,'color_img') + destdir = os.path.join(outdir,u"color_img") if not os.path.exists(destdir): os.makedirs(destdir) - destdir = os.path.join(outdir,'page') + destdir = os.path.join(outdir,u"page") if not os.path.exists(destdir): os.makedirs(destdir) - destdir = os.path.join(outdir,'glyphs') + destdir = os.path.join(outdir,u"glyphs") if not os.path.exists(destdir): os.makedirs(destdir) @@ -334,149 +381,148 @@ class TopazBook: outdir = self.outdir for headerRecord in self.bookHeaderRecords: name = headerRecord - if name != "dkey" : - ext = '.dat' - if name == 'img' : ext = '.jpg' - if name == 'color' : ext = '.jpg' - print "\nProcessing Section: %s " % name + if name != 'dkey': + ext = u".dat" + if name == 'img': ext = u".jpg" + if name == 'color' : ext = u".jpg" + print u"Processing Section: {0}\n. . .".format(name), for index in range (0,len(self.bookHeaderRecords[name])) : - fnum = "%04d" % index - fname = name + fnum + ext + fname = u"{0}{1:04d}{2}".format(name,index,ext) destdir = outdir if name == 'img': - destdir = os.path.join(outdir,'img') + destdir = os.path.join(outdir,u"img") if name == 'color': - destdir = os.path.join(outdir,'color_img') + destdir = os.path.join(outdir,u"color_img") if name == 'page': - destdir = os.path.join(outdir,'page') + destdir = os.path.join(outdir,u"page") if name == 'glyphs': - destdir = os.path.join(outdir,'glyphs') + destdir = os.path.join(outdir,u"glyphs") outputFile = os.path.join(destdir,fname) - print ".", + print u".", record = self.getBookPayloadRecord(name,index) if record != '': file(outputFile, 'wb').write(record) - print " " + print u" " - def getHTMLZip(self, zipname): + def getFile(self, zipname): htmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False) - htmlzip.write(os.path.join(self.outdir,'book.html'),'book.html') - htmlzip.write(os.path.join(self.outdir,'book.opf'),'book.opf') - if os.path.isfile(os.path.join(self.outdir,'cover.jpg')): - htmlzip.write(os.path.join(self.outdir,'cover.jpg'),'cover.jpg') - htmlzip.write(os.path.join(self.outdir,'style.css'),'style.css') - zipUpDir(htmlzip, self.outdir, 'img') + htmlzip.write(os.path.join(self.outdir,u"book.html"),u"book.html") + htmlzip.write(os.path.join(self.outdir,u"book.opf"),u"book.opf") + if os.path.isfile(os.path.join(self.outdir,u"cover.jpg")): + htmlzip.write(os.path.join(self.outdir,u"cover.jpg"),u"cover.jpg") + htmlzip.write(os.path.join(self.outdir,u"style.css"),u"style.css") + zipUpDir(htmlzip, self.outdir, u"img") htmlzip.close() + def getBookType(self): + return u"Topaz" + + def getBookExtension(self): + return u".htmlz" + def getSVGZip(self, zipname): svgzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False) - svgzip.write(os.path.join(self.outdir,'index_svg.xhtml'),'index_svg.xhtml') - zipUpDir(svgzip, self.outdir, 'svg') - zipUpDir(svgzip, self.outdir, 'img') + svgzip.write(os.path.join(self.outdir,u"index_svg.xhtml"),u"index_svg.xhtml") + zipUpDir(svgzip, self.outdir, u"svg") + zipUpDir(svgzip, self.outdir, u"img") svgzip.close() - def getXMLZip(self, zipname): - xmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False) - targetdir = os.path.join(self.outdir,'xml') - zipUpDir(xmlzip, targetdir, '') - zipUpDir(xmlzip, self.outdir, 'img') - xmlzip.close() - def cleanup(self): if os.path.isdir(self.outdir): shutil.rmtree(self.outdir, True) def usage(progname): - print "Removes DRM protection from Topaz ebooks and extract the contents" - print "Usage:" - print " %s [-k ] [-p ] [-s ] " % progname - + print u"Removes DRM protection from Topaz ebooks and extracts the contents" + print u"Usage:" + print u" {0} [-k ] [-p ] [-s ] ".format(progname) # Main -def main(argv=sys.argv): - global buildXML +def cli_main(argv=unicode_argv()): progname = os.path.basename(argv[0]) - k4 = False - pids = [] - serials = [] - kInfoFiles = [] + print u"TopazExtract v{0}.".format(__version__) try: - opts, args = getopt.getopt(sys.argv[1:], "k:p:s:") + opts, args = getopt.getopt(sys.argv[1:], "k:p:s:x") except getopt.GetoptError, err: - print str(err) + print u"Error in options or arguments: {0}".format(err.args[0]) usage(progname) return 1 if len(args)<2: usage(progname) return 1 - for o, a in opts: - if o == "-k": - if a == None : - print "Invalid parameter for -k" - return 1 - kInfoFiles.append(a) - if o == "-p": - if a == None : - print "Invalid parameter for -p" - return 1 - pids = a.split(',') - if o == "-s": - if a == None : - print "Invalid parameter for -s" - return 1 - serials = a.split(',') - k4 = True - infile = args[0] outdir = args[1] - if not os.path.isfile(infile): - print "Input File Does Not Exist" + print u"Input File {0} Does Not Exist.".format(infile) return 1 + if not os.path.exists(outdir): + print u"Output Directory {0} Does Not Exist.".format(outdir) + return 1 + + kInfoFiles = [] + serials = [] + pids = [] + + for o, a in opts: + if o == '-k': + if a == None : + raise DrmException("Invalid parameter for -k") + kInfoFiles.append(a) + if o == '-p': + if a == None : + raise DrmException("Invalid parameter for -p") + pids = a.split(',') + if o == '-s': + if a == None : + raise DrmException("Invalid parameter for -s") + serials = [serial.replace(" ","") for serial in a.split(',')] + bookname = os.path.splitext(os.path.basename(infile))[0] tb = TopazBook(infile) title = tb.getBookTitle() - print "Processing Book: ", title - keysRecord, keysRecordRecord = tb.getPIDMetaInfo() - pids.extend(kgenpids.getPidList(keysRecord, keysRecordRecord, k4, serials, kInfoFiles)) + print u"Processing Book: {0}".format(title) + md1, md2 = tb.getPIDMetaInfo() + pids.extend(kgenpids.getPidList(md1, md2, serials, kInfoFiles)) try: - print "Decrypting Book" + print u"Decrypting Book" tb.processBook(pids) - print " Creating HTML ZIP Archive" - zipname = os.path.join(outdir, bookname + '_nodrm' + '.htmlz') - tb.getHTMLZip(zipname) + print u" Creating HTML ZIP Archive" + zipname = os.path.join(outdir, bookname + u"_nodrm.htmlz") + tb.getFile(zipname) - print " Creating SVG ZIP Archive" - zipname = os.path.join(outdir, bookname + '_SVG' + '.zip') + print u" Creating SVG ZIP Archive" + zipname = os.path.join(outdir, bookname + u"_SVG.zip") tb.getSVGZip(zipname) - if buildXML: - print " Creating XML ZIP Archive" - zipname = os.path.join(outdir, bookname + '_XML' + '.zip') - tb.getXMLZip(zipname) - # removing internal temporary directory of pieces tb.cleanup() - except TpzDRMError, e: - print str(e) - # tb.cleanup() + except DrmException, e: + print u"Decryption failed\n{0}".format(traceback.format_exc()) + + try: + tb.cleanup() + except: + pass return 1 except Exception, e: - print str(e) - # tb.cleanup + print u"Decryption failed\m{0}".format(traceback.format_exc()) + try: + tb.cleanup() + except: + pass return 1 return 0 if __name__ == '__main__': - sys.stdout=Unbuffered(sys.stdout) - sys.exit(main()) + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) + sys.exit(cli_main()) diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/zipfix.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/zipfix.py index c7921f2..eaee20d 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/zipfix.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/zipfix.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- import sys import zlib @@ -27,14 +28,10 @@ class fixZip: self.ztype = 'zip' if zinput.lower().find('.epub') >= 0 : self.ztype = 'epub' - print "opening input" self.inzip = zipfilerugged.ZipFile(zinput,'r') - print "opening outout" self.outzip = zipfilerugged.ZipFile(zoutput,'w') - print "opening input as raw file" # open the input zip for reading only as a raw file self.bzf = file(zinput,'rb') - print "finished initialising" def getlocalname(self, zi): local_header_offset = zi.header_offset diff --git a/DeDRM_Windows_Application/DeDRM_ReadMe.txt b/DeDRM_Windows_Application/DeDRM_ReadMe.txt index 2c73c84..df13eb5 100644 --- a/DeDRM_Windows_Application/DeDRM_ReadMe.txt +++ b/DeDRM_Windows_Application/DeDRM_ReadMe.txt @@ -1,9 +1,9 @@ -ReadMe_DeDRM_v5.4.1_WinApp ------------------------ +ReadMe_DeDRM_v5.5_WinApp +======================== -DeDRM_v5.4.1_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 the"tools" python software in one easy to use program that remembers preferences and settings. +DeDRM_v5.5_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 should work out of the box with Kindle for PC ebooks and Adobe Adept epub and pdf ebooks. +It will work without manual configuration for Kindle for PC ebooks and Adobe Adept epub and pdf ebooks. To remove the DRM from standalone Kindle ebooks, eReader pdb ebooks, Barnes and Noble epubs, and Mobipocket ebooks requires the user to double-click the DeDRM_Drop_Target and set some additional Preferences including: @@ -16,14 +16,16 @@ Once these preferences have been set, the user can simply drag and drop ebooks o This program requires that a 32 bit version of Python 2.X (tested with Python 2.5 through Python 2.7) and PyCrypto be installed on your computer before it will work. See below for where to get theese programs for Windows. +NB Although the individual scripts have been updated to work with unicode file names, the Windows DeDRM script has not yet been updated for technical reasons. Therefore, if you try to use it with paths or file names that contain non-ASCII characters, it might not work. + 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.4.1 folder from tools_v5.4.1/DeDRM_Applications/Windows to your "My Documents" folder. +1. Drag the DeDRM_5.5 folder from tools_v5.5/DeDRM_Applications/Windows to your "My Documents" folder. -2. Open the DeDRM_5.4.1 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.5 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/Additional_Tools/FindTopazEbooks.pyw b/Other_Tools/Additional_Tools/FindTopazEbooks.pyw deleted file mode 100644 index e39025b..0000000 --- a/Other_Tools/Additional_Tools/FindTopazEbooks.pyw +++ /dev/null @@ -1,217 +0,0 @@ -#!/usr/bin/env python - -# This is a simple tool to identify all Amazon Topaz ebooks in a specific directory. -# There always seems to be confusion since Topaz books downloaded to K4PC/Mac can have -# almost any extension (.azw, .azw1, .prc, tpz). While the .azw1 and .tpz extensions -# are fairly easy to indentify, the others are not (without opening the files in an editor). - -# To run the tool with the GUI frontend, just double-click on the 'FindTopazFiles.pyw' file -# and select the folder where all of the ebooks in question are located. Then click 'Search'. -# The program will list the file names of the ebooks that are indentified as being Topaz. -# You can then isolate those books and use the Topaz tools to decrypt and convert them. - -# You can also run the script from a command line... supplying the folder to search -# as a parameter: python FindTopazEbooks.pyw "C:\My Folder" (change appropriately for -# your particular O.S.) - -# ** NOTE: This program does NOT decrypt or modify Topaz files in any way. It simply identifies them. - -# PLEASE DO NOT PIRATE EBOOKS! - -# We want all authors and publishers, and eBook stores to live -# long and prosperous lives but at the same time we just want to -# be able to read OUR books on whatever device we want and to keep -# readable for a long, long time - -# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle, -# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates -# and many many others - -# Revision history: -# 1 - Initial release. - -from __future__ import with_statement - -__license__ = 'GPL v3' - -import sys -import os -os.environ['PYTHONIOENCODING'] = "utf-8" -import re -import shutil -import Tkinter -import Tkconstants -import tkFileDialog -import tkMessageBox - - -class ScrolledText(Tkinter.Text): - def __init__(self, master=None, **kw): - self.frame = Tkinter.Frame(master) - self.vbar = Tkinter.Scrollbar(self.frame) - self.vbar.pack(side=Tkconstants.RIGHT, fill=Tkconstants.Y) - kw.update({'yscrollcommand': self.vbar.set}) - Tkinter.Text.__init__(self, self.frame, **kw) - self.pack(side=Tkconstants.LEFT, fill=Tkconstants.BOTH, expand=True) - self.vbar['command'] = self.yview - # Copy geometry methods of self.frame without overriding Text - # methods = hack! - text_meths = vars(Tkinter.Text).keys() - methods = vars(Tkinter.Pack).keys() + vars(Tkinter.Grid).keys() + vars(Tkinter.Place).keys() - methods = set(methods).difference(text_meths) - for m in methods: - if m[0] != '_' and m != 'config' and m != 'configure': - setattr(self, m, getattr(self.frame, m)) - - def __str__(self): - return str(self.frame) - - -def cli_main(argv=sys.argv, obj=None): - progname = os.path.basename(argv[0]) - if len(argv) != 2: - print "usage: %s DIRECTORY" % (progname,) - return 1 - - if obj == None: - print "\nTopaz search results:\n" - else: - obj.stext.insert(Tkconstants.END,"Topaz search results:\n\n") - - inpath = argv[1] - files = os.listdir(inpath) - filefilter = re.compile("(\.azw$)|(\.azw1$)|(\.prc$)|(\.tpz$)", re.IGNORECASE) - files = filter(filefilter.search, files) - - if files: - topazcount = 0 - totalcount = 0 - for filename in files: - with open(os.path.join(inpath, filename), 'rb') as f: - try: - if f.read().startswith('TPZ'): - f.close() - basename, extension = os.path.splitext(filename) - if obj == None: - print " %s is a Topaz formatted ebook." % filename - """ - if extension == '.azw' or extension == '.prc': - print " renaming to %s" % (basename + '.tpz') - shutil.move(os.path.join(inpath, filename), - os.path.join(inpath, basename + '.tpz')) - """ - else: - msg1 = " %s is a Topaz formatted ebook.\n" % filename - obj.stext.insert(Tkconstants.END,msg1) - """ - if extension == '.azw' or extension == '.prc': - msg2 = " renaming to %s\n" % (basename + '.tpz') - obj.stext.insert(Tkconstants.END,msg2) - shutil.move(os.path.join(inpath, filename), - os.path.join(inpath, basename + '.tpz')) - """ - topazcount += 1 - except: - if obj == None: - print " Error reading %s." % filename - else: - msg = " Error reading or %s.\n" % filename - obj.stext.insert(Tkconstants.END,msg) - pass - totalcount += 1 - if topazcount == 0: - if obj == None: - print "\nNo Topaz books found in %s." % inpath - else: - msg = "\nNo Topaz books found in %s.\n\n" % inpath - obj.stext.insert(Tkconstants.END,msg) - else: - if obj == None: - print "\n%i Topaz books found in %s\n%i total books checked.\n" % (topazcount, inpath, totalcount) - else: - msg = "\n%i Topaz books found in %s\n%i total books checked.\n\n" %(topazcount, inpath, totalcount) - obj.stext.insert(Tkconstants.END,msg) - else: - if obj == None: - print "No typical Topaz file extensions found in %s.\n" % inpath - else: - msg = "No typical Topaz file extensions found in %s.\n\n" % inpath - obj.stext.insert(Tkconstants.END,msg) - - return 0 - - -class DecryptionDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - ltext='Search a directory for Topaz eBooks\n' - 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='Directory to Search').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) - msg1 = 'Topaz search results \n\n' - self.stext = ScrolledText(body, bd=5, relief=Tkconstants.RIDGE, - height=15, width=60, wrap=Tkconstants.WORD) - self.stext.grid(row=4, column=0, columnspan=2,sticky=sticky) - #self.stext.insert(Tkconstants.END,msg1) - buttons = Tkinter.Frame(self) - buttons.pack() - - - self.botton = Tkinter.Button( - buttons, text="Search", width=10, command=self.search) - self.botton.pack(side=Tkconstants.LEFT) - Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) - self.button = Tkinter.Button( - buttons, text="Quit", width=10, command=self.quit) - self.button.pack(side=Tkconstants.RIGHT) - - def get_inpath(self): - cwd = os.getcwdu() - cwd = cwd.encode('utf-8') - inpath = tkFileDialog.askdirectory( - parent=None, title='Directory to search', - initialdir=cwd, initialfile=None) - if inpath: - inpath = os.path.normpath(inpath) - self.inpath.delete(0, Tkconstants.END) - self.inpath.insert(0, inpath) - return - - - def search(self): - inpath = self.inpath.get() - if not inpath or not os.path.exists(inpath): - self.status['text'] = 'Specified directory does not exist' - return - argv = [sys.argv[0], inpath] - self.status['text'] = 'Searching...' - self.botton.configure(state='disabled') - cli_main(argv, self) - self.status['text'] = 'Search a directory for Topaz files' - self.botton.configure(state='normal') - - return - - -def gui_main(): - root = Tkinter.Tk() - root.title('Topaz eBook Finder') - 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/Additional_Tools/KindlePID.pyw b/Other_Tools/Additional_Tools/KindlePID.pyw deleted file mode 100644 index ae3fb8a..0000000 --- a/Other_Tools/Additional_Tools/KindlePID.pyw +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/env python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab - -import sys -sys.path.append('lib') -import os, os.path, urllib -os.environ['PYTHONIOENCODING'] = "utf-8" -import subprocess -from subprocess import Popen, PIPE, STDOUT -import subasyncio -from subasyncio import Process -import Tkinter -import Tkconstants -import tkFileDialog -import tkMessageBox -from scrolltextwidget import ScrolledText - -class MainDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - self.root = root - self.interval = 2000 - self.p2 = None - self.status = Tkinter.Label(self, text='Find your Kindle PID') - 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='Kindle Serial # or iPhone UDID').grid(row=1, sticky=Tkconstants.E) - self.serialnum = Tkinter.StringVar() - self.serialinfo = Tkinter.Entry(body, width=45, textvariable=self.serialnum) - self.serialinfo.grid(row=1, column=1, sticky=sticky) - - msg1 = 'Conversion Log \n\n' - self.stext = ScrolledText(body, bd=5, relief=Tkconstants.RIDGE, height=15, width=60, wrap=Tkconstants.WORD) - self.stext.grid(row=3, column=0, columnspan=2,sticky=sticky) - self.stext.insert(Tkconstants.END,msg1) - - buttons = Tkinter.Frame(self) - buttons.pack() - self.sbotton = Tkinter.Button( - buttons, text="Start", width=10, command=self.convertit) - self.sbotton.pack(side=Tkconstants.LEFT) - - Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) - self.qbutton = Tkinter.Button( - buttons, text="Quit", width=10, command=self.quitting) - self.qbutton.pack(side=Tkconstants.RIGHT) - - # 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): - poll = self.p2.wait('nowait') - if poll != None: - text = self.p2.readerr() - text += self.p2.read() - msg = text + '\n\n' + 'Kindle PID Successfully Determined\n' - if poll != 0: - msg = text + '\n\n' + 'Error: Kindle PID Failed\n' - self.showCmdOutput(msg) - self.p2 = None - self.sbotton.configure(state='normal') - return - text = self.p2.readerr() - text += self.p2.read() - self.showCmdOutput(text) - # make sure we get invoked again by event loop after interval - self.stext.after(self.interval,self.processPipe) - return - - # post output from subprocess in scrolled text widget - def showCmdOutput(self, msg): - if msg and msg !='': - if sys.platform.startswith('win'): - msg = msg.replace('\r\n','\n') - self.stext.insert(Tkconstants.END,msg) - self.stext.yview_pickplace(Tkconstants.END) - return - - # run as a subprocess via pipes and collect stdout - def pidrdr(self, serial): - # os.putenv('PYTHONUNBUFFERED', '1') - pengine = sys.executable - if pengine is None or pengine == '': - pengine = "python" - pengine = os.path.normpath(pengine) - cmdline = pengine + ' ./lib/kindlepid.py "' + serial + '"' - if sys.platform[0:3] == 'win': - # search_path = os.environ['PATH'] - # search_path = search_path.lower() - # if search_path.find('python') >= 0: - # cmdline = 'python lib\kindlepid.py "' + serial + '"' - # else : - # cmdline = 'lib\kindlepid.py "' + serial + '"' - cmdline = pengine + ' lib\\kindlepid.py "' + serial + '"' - cmdline = cmdline.encode(sys.getfilesystemencoding()) - p2 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False) - return p2 - - def quitting(self): - # kill any still running subprocess - if self.p2 != None: - if (self.p2.wait('nowait') == None): - self.p2.terminate() - self.root.destroy() - - # actually ready to run the subprocess and get its output - def convertit(self): - # now disable the button to prevent multiple launches - self.sbotton.configure(state='disabled') - serial = self.serialinfo.get() - if not serial or serial == '': - self.status['text'] = 'No Kindle Serial Number or iPhone UDID specified' - self.sbotton.configure(state='normal') - return - - log = 'Command = "python kindlepid.py"\n' - log += 'Serial = "' + serial + '"\n' - log += '\n\n' - log += 'Please Wait ...\n\n' - self.stext.insert(Tkconstants.END,log) - self.p2 = self.pidrdr(serial) - - # python does not seem to allow you to create - # your own eventloop which every other gui does - strange - # so need to use the widget "after" command to force - # event loop to run non-gui events every interval - self.stext.after(self.interval,self.processPipe) - return - - -def main(argv=None): - root = Tkinter.Tk() - root.title('Kindle and iPhone PID Calculator') - root.resizable(True, False) - root.minsize(300, 0) - MainDialog(root).pack(fill=Tkconstants.X, expand=1) - root.mainloop() - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/Other_Tools/Additional_Tools/Kindleizer.pyw b/Other_Tools/Additional_Tools/Kindleizer.pyw deleted file mode 100644 index a725626..0000000 --- a/Other_Tools/Additional_Tools/Kindleizer.pyw +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab - -import sys -sys.path.append('lib') -import os, os.path, urllib -import subprocess -from subprocess import Popen, PIPE, STDOUT -import subasyncio -from subasyncio import Process -import Tkinter -import Tkconstants -import tkFileDialog -import tkMessageBox -from scrolltextwidget import ScrolledText - -class MainDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - self.root = root - self.interval = 2000 - self.p2 = None - self.status = Tkinter.Label(self, text='Fix Encrypted Mobi eBooks so the Kindle can read them') - 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='Mobi eBook input file').grid(row=0, sticky=Tkconstants.E) - self.mobipath = Tkinter.Entry(body, width=50) - self.mobipath.grid(row=0, column=1, sticky=sticky) - cwd = os.getcwdu() - cwd = cwd.encode('utf-8') - self.mobipath.insert(0, cwd) - button = Tkinter.Button(body, text="...", command=self.get_mobipath) - button.grid(row=0, column=2) - - Tkinter.Label(body, text='10 Character PID').grid(row=1, sticky=Tkconstants.E) - self.pidnum = Tkinter.StringVar() - self.pidinfo = Tkinter.Entry(body, width=12, textvariable=self.pidnum) - self.pidinfo.grid(row=1, column=1, sticky=sticky) - - msg1 = 'Conversion Log \n\n' - self.stext = ScrolledText(body, bd=5, relief=Tkconstants.RIDGE, height=15, width=60, wrap=Tkconstants.WORD) - self.stext.grid(row=2, column=0, columnspan=2,sticky=sticky) - self.stext.insert(Tkconstants.END,msg1) - - buttons = Tkinter.Frame(self) - buttons.pack() - self.sbotton = Tkinter.Button( - buttons, text="Start", width=10, command=self.convertit) - self.sbotton.pack(side=Tkconstants.LEFT) - - Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) - self.qbutton = Tkinter.Button( - buttons, text="Quit", width=10, command=self.quitting) - self.qbutton.pack(side=Tkconstants.RIGHT) - - # 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): - poll = self.p2.wait('nowait') - if poll != None: - text = self.p2.readerr() - text += self.p2.read() - msg = text + '\n\n' + 'Fix for Kindle successful\n' - if poll != 0: - msg = text + '\n\n' + 'Error: Fix for Kindle Failed\n' - self.showCmdOutput(msg) - self.p2 = None - self.sbotton.configure(state='normal') - return - text = self.p2.readerr() - text += self.p2.read() - self.showCmdOutput(text) - # make sure we get invoked again by event loop after interval - self.stext.after(self.interval,self.processPipe) - return - - # post output from subprocess in scrolled text widget - def showCmdOutput(self, msg): - if msg and msg !='': - msg = msg.encode('utf-8') - if sys.platform.startswith('win'): - msg = msg.replace('\r\n','\n') - self.stext.insert(Tkconstants.END,msg) - self.stext.yview_pickplace(Tkconstants.END) - return - - # run as a subprocess via pipes and collect stdout - def krdr(self, infile, pidnum): - # os.putenv('PYTHONUNBUFFERED', '1') - cmdline = 'python ./lib/kindlefix.py "' + infile + '" "' + pidnum + '"' - if sys.platform[0:3] == 'win': - search_path = os.environ['PATH'] - search_path = search_path.lower() - if search_path.find('python') >= 0: - cmdline = 'python lib\kindlefix.py "' + infile + '" "' + pidnum + '"' - else : - cmdline = 'lib\kindlefix.py "' + infile + '" "' + pidnum + '"' - - cmdline = cmdline.encode(sys.getfilesystemencoding()) - p2 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False) - return p2 - - - def get_mobipath(self): - mobipath = tkFileDialog.askopenfilename( - parent=None, title='Select Mobi eBook File', - defaultextension='.prc', filetypes=[('Mobi eBook File', '.prc'), ('Mobi eBook File', '.mobi'), - ('All Files', '.*')]) - if mobipath: - mobipath = os.path.normpath(mobipath) - self.mobipath.delete(0, Tkconstants.END) - self.mobipath.insert(0, mobipath) - return - - def quitting(self): - # kill any still running subprocess - if self.p2 != None: - if (self.p2.wait('nowait') == None): - self.p2.terminate() - self.root.destroy() - - # actually ready to run the subprocess and get its output - def convertit(self): - # now disable the button to prevent multiple launches - self.sbotton.configure(state='disabled') - mobipath = self.mobipath.get() - pidnum = self.pidinfo.get() - if not mobipath or not os.path.exists(mobipath): - self.status['text'] = 'Specified Mobi eBook file does not exist' - self.sbotton.configure(state='normal') - return - if not pidnum or pidnum == '': - self.status['text'] = 'No PID specified' - self.sbotton.configure(state='normal') - return - - log = 'Command = "python kindlefix.py"\n' - log += 'Mobi Path = "'+ mobipath + '"\n' - log += 'PID = "' + pidnum + '"\n' - log += '\n\n' - log += 'Please Wait ...\n\n' - log = log.encode('utf-8') - self.stext.insert(Tkconstants.END,log) - self.p2 = self.krdr(mobipath, pidnum) - - # python does not seem to allow you to create - # your own eventloop which every other gui does - strange - # so need to use the widget "after" command to force - # event loop to run non-gui events every interval - self.stext.after(self.interval,self.processPipe) - return - - -def main(argv=None): - root = Tkinter.Tk() - root.title('Fix Encrypted Mobi eBooks to work with the Kindle') - root.resizable(True, False) - root.minsize(300, 0) - MainDialog(root).pack(fill=Tkconstants.X, expand=1) - root.mainloop() - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/Other_Tools/Additional_Tools/MobiDeDRM.pyw b/Other_Tools/Additional_Tools/MobiDeDRM.pyw deleted file mode 100644 index 0e4308e..0000000 --- a/Other_Tools/Additional_Tools/MobiDeDRM.pyw +++ /dev/null @@ -1,203 +0,0 @@ -#!/usr/bin/env python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab - -import sys -sys.path.append('lib') -import os, os.path, urllib -os.environ['PYTHONIOENCODING'] = "utf-8" -import subprocess -from subprocess import Popen, PIPE, STDOUT -import subasyncio -from subasyncio import Process -import Tkinter -import Tkconstants -import tkFileDialog -import tkMessageBox -from scrolltextwidget import ScrolledText - -class MainDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - self.root = root - self.interval = 2000 - self.p2 = None - self.status = Tkinter.Label(self, text='Remove Encryption from a Mobi eBook') - 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='Mobi eBook input file').grid(row=0, sticky=Tkconstants.E) - self.mobipath = Tkinter.Entry(body, width=50) - self.mobipath.grid(row=0, column=1, sticky=sticky) - cwd = os.getcwdu() - cwd = cwd.encode('utf-8') - self.mobipath.insert(0, cwd) - button = Tkinter.Button(body, text="...", command=self.get_mobipath) - button.grid(row=0, column=2) - - Tkinter.Label(body, text='Name for Unencrypted Output File').grid(row=1, sticky=Tkconstants.E) - self.outpath = Tkinter.Entry(body, width=50) - self.outpath.grid(row=1, column=1, sticky=sticky) - self.outpath.insert(0, '') - button = Tkinter.Button(body, text="...", command=self.get_outpath) - button.grid(row=1, column=2) - - Tkinter.Label(body, text='10 Character PID').grid(row=2, sticky=Tkconstants.E) - self.pidnum = Tkinter.StringVar() - self.pidinfo = Tkinter.Entry(body, width=12, textvariable=self.pidnum) - self.pidinfo.grid(row=2, column=1, sticky=sticky) - - msg1 = 'Conversion Log \n\n' - self.stext = ScrolledText(body, bd=5, relief=Tkconstants.RIDGE, height=15, width=60, wrap=Tkconstants.WORD) - self.stext.grid(row=3, column=0, columnspan=2,sticky=sticky) - self.stext.insert(Tkconstants.END,msg1) - - buttons = Tkinter.Frame(self) - buttons.pack() - self.sbotton = Tkinter.Button( - buttons, text="Start", width=10, command=self.convertit) - self.sbotton.pack(side=Tkconstants.LEFT) - - Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) - self.qbutton = Tkinter.Button( - buttons, text="Quit", width=10, command=self.quitting) - self.qbutton.pack(side=Tkconstants.RIGHT) - - # 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): - poll = self.p2.wait('nowait') - if poll != None: - text = self.p2.readerr() - text += self.p2.read() - msg = text + '\n\n' + 'Encryption successfully removed\n' - if poll != 0: - msg = text + '\n\n' + 'Error: Encryption Removal Failed\n' - self.showCmdOutput(msg) - self.p2 = None - self.sbotton.configure(state='normal') - return - text = self.p2.readerr() - text += self.p2.read() - self.showCmdOutput(text) - # make sure we get invoked again by event loop after interval - self.stext.after(self.interval,self.processPipe) - return - - # post output from subprocess in scrolled text widget - def showCmdOutput(self, msg): - if msg and msg !='': - if sys.platform.startswith('win'): - msg = msg.replace('\r\n','\n') - self.stext.insert(Tkconstants.END,msg) - self.stext.yview_pickplace(Tkconstants.END) - return - - # run as a subprocess via pipes and collect stdout - def mobirdr(self, infile, outfile, pidnum): - pengine = sys.executable - if pengine is None or pengine == '': - pengine = "python" - pengine = os.path.normpath(pengine) - # os.putenv('PYTHONUNBUFFERED', '1') - cmdline = pengine + ' ./lib/mobidedrm.py "' + infile + '" "' + outfile + '" "' + pidnum + '"' - if sys.platform[0:3] == 'win': - # search_path = os.environ['PATH'] - # search_path = search_path.lower() - # if search_path.find('python') >= 0: - # cmdline = 'python lib\mobidedrm.py "' + infile + '" "' + outfile + '" "' + pidnum + '"' - # else : - # cmdline = 'lib\mobidedrm.py "' + infile + '" "' + outfile + '" "' + pidnum + '"' - cmdline = pengine + ' lib\\mobidedrm.py "' + infile + '" "' + outfile + '" "' + pidnum + '"' - - cmdline = cmdline.encode(sys.getfilesystemencoding()) - p2 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False) - return p2 - - - def get_mobipath(self): - mobipath = tkFileDialog.askopenfilename( - parent=None, title='Select Mobi eBook File', - defaultextension='.prc', filetypes=[('Mobi eBook File', '.prc'), ('Mobi eBook File', '.azw'),('Mobi eBook File', '.mobi'), - ('All Files', '.*')]) - if mobipath: - mobipath = os.path.normpath(mobipath) - self.mobipath.delete(0, Tkconstants.END) - self.mobipath.insert(0, mobipath) - return - - def get_outpath(self): - mobipath = self.mobipath.get() - initname = os.path.basename(mobipath) - p = initname.find('.') - if p >= 0: initname = initname[0:p] - initname += '_nodrm.mobi' - outpath = tkFileDialog.asksaveasfilename( - parent=None, title='Select Unencrypted Mobi File to produce', - defaultextension='.mobi', initialfile=initname, - filetypes=[('Mobi files', '.mobi'), ('All files', '.*')]) - if outpath: - outpath = os.path.normpath(outpath) - self.outpath.delete(0, Tkconstants.END) - self.outpath.insert(0, outpath) - return - - def quitting(self): - # kill any still running subprocess - if self.p2 != None: - if (self.p2.wait('nowait') == None): - self.p2.terminate() - self.root.destroy() - - # actually ready to run the subprocess and get its output - def convertit(self): - # now disable the button to prevent multiple launches - self.sbotton.configure(state='disabled') - mobipath = self.mobipath.get() - outpath = self.outpath.get() - pidnum = self.pidinfo.get() - if not mobipath or not os.path.exists(mobipath): - self.status['text'] = 'Specified Mobi eBook file does not exist' - self.sbotton.configure(state='normal') - return - if not outpath: - self.status['text'] = 'No output file specified' - self.sbotton.configure(state='normal') - return - if not pidnum or pidnum == '': - self.status['text'] = 'No PID specified' - self.sbotton.configure(state='normal') - return - - log = 'Command = "python mobidedrm.py"\n' - log += 'Mobi Path = "'+ mobipath + '"\n' - log += 'Output File = "' + outpath + '"\n' - log += 'PID = "' + pidnum + '"\n' - log += '\n\n' - log += 'Please Wait ...\n\n' - self.stext.insert(Tkconstants.END,log) - self.p2 = self.mobirdr(mobipath, outpath, pidnum) - - # python does not seem to allow you to create - # your own eventloop which every other gui does - strange - # so need to use the widget "after" command to force - # event loop to run non-gui events every interval - self.stext.after(self.interval,self.processPipe) - return - - -def main(argv=None): - root = Tkinter.Tk() - root.title('Mobi eBook Encryption Removal') - root.resizable(True, False) - root.minsize(300, 0) - MainDialog(root).pack(fill=Tkconstants.X, expand=1) - root.mainloop() - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/Other_Tools/Additional_Tools/lib/kindlefix.py b/Other_Tools/Additional_Tools/lib/kindlefix.py deleted file mode 100644 index 6a0b57d..0000000 --- a/Other_Tools/Additional_Tools/lib/kindlefix.py +++ /dev/null @@ -1,172 +0,0 @@ -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 prc, struct -from binascii import hexlify - -def strByte(s,off=0): - return struct.unpack(">B",s[off])[0]; - -def strSWord(s,off=0): - return struct.unpack(">h",s[off:off+2])[0]; - -def strWord(s,off=0): - return struct.unpack(">H",s[off:off+2])[0]; - -def strDWord(s,off=0): - return struct.unpack(">L",s[off:off+4])[0]; - -def strPutDWord(s,off,i): - return s[:off]+struct.pack(">L",i)+s[off+4:]; - -keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96" - -#implementation of Pukall Cipher 1 -def PC1(key, src, decryption=True): - sum1 = 0; - sum2 = 0; - keyXorVal = 0; - if len(key)!=16: - print "Bad key length!" - return None - wkey = [] - for i in xrange(8): - wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1])) - - dst = "" - for i in xrange(len(src)): - temp1 = 0; - byteXorVal = 0; - for j in xrange(8): - temp1 ^= wkey[j] - sum2 = (sum2+j)*20021 + sum1 - sum1 = (temp1*346)&0xFFFF - sum2 = (sum2+sum1)&0xFFFF - temp1 = (temp1*20021+1)&0xFFFF - byteXorVal ^= temp1 ^ sum2 - - curByte = ord(src[i]) - if not decryption: - keyXorVal = curByte * 257; - curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF - if decryption: - keyXorVal = curByte * 257; - for j in xrange(8): - wkey[j] ^= keyXorVal; - - dst+=chr(curByte) - - return dst - -def find_key(rec0, pid): - off1 = strDWord(rec0, 0xA8) - if off1==0xFFFFFFFF or off1==0: - print "No DRM" - return None - size1 = strDWord(rec0, 0xB0) - cnt = strDWord(rec0, 0xAC) - flag = strDWord(rec0, 0xB4) - - temp_key = PC1(keyvec1, pid.ljust(16,'\0'), False) - cksum = 0 - #print pid, "->", hexlify(temp_key) - for i in xrange(len(temp_key)): - cksum += ord(temp_key[i]) - cksum &= 0xFF - temp_key = temp_key.ljust(16,'\0') - #print "pid cksum: %02X"%cksum - - #print "Key records: %02X-%02X, count: %d, flag: %02X"%(off1, off1+size1, cnt, flag) - iOff = off1 - drm_key = None - for i in xrange(cnt): - dwCheck = strDWord(rec0, iOff) - dwSize = strDWord(rec0, iOff+4) - dwType = strDWord(rec0, iOff+8) - nCksum = strByte(rec0, iOff+0xC) - #print "Key record %d: check=%08X, size=%d, type=%d, cksum=%02X"%(i, dwCheck, dwSize, dwType, nCksum) - if nCksum==cksum: - drmInfo = PC1(temp_key, rec0[iOff+0x10:iOff+0x30]) - dw0, dw4, dw18, dw1c = struct.unpack(">II16xII", drmInfo) - #print "Decrypted drmInfo:", "%08X, %08X, %s, %08X, %08X"%(dw0, dw4, hexlify(drmInfo[0x8:0x18]), dw18, dw1c) - #print "Decrypted drmInfo:", hexlify(drmInfo) - if dw0==dwCheck: - print "Found the matching record; setting the CustomDRM flag for Kindle" - drmInfo = strPutDWord(drmInfo,4,(dw4|0x800)) - dw0, dw4, dw18, dw1c = struct.unpack(">II16xII", drmInfo) - #print "Updated drmInfo:", "%08X, %08X, %s, %08X, %08X"%(dw0, dw4, hexlify(drmInfo[0x8:0x18]), dw18, dw1c) - return rec0[:iOff+0x10] + PC1(temp_key, drmInfo, False) + rec0[:iOff+0x30] - iOff += dwSize - return None - -def replaceext(filename, newext): - nameparts = filename.split(".") - if len(nameparts)>1: - return (".".join(nameparts[:-1]))+newext - else: - return nameparts[0]+newext - -def main(argv=sys.argv): - print "The Kindleizer v0.2. Copyright (c) 2007 Igor Skochinsky" - if len(sys.argv) != 3: - print "Fixes encrypted Mobipocket books to be readable by Kindle" - print "Usage: kindlefix.py file.mobi PID" - return 1 - fname = sys.argv[1] - pid = sys.argv[2] - if len(pid)==10 and pid[-3]=='*': - pid = pid[:-2] - if len(pid)!=8 or pid[-1]!='*': - print "PID is not valid! (should be in format AAAAAAA*DD)" - return 3 - db = prc.File(fname) - #print dir(db) - if db.getDBInfo()["creator"]!='MOBI': - print "Not a Mobi file!" - return 1 - rec0 = db.getRecord(0)[0] - enc = strSWord(rec0, 0xC) - print "Encryption:", enc - if enc!=2: - print "Unknown encryption type" - return 1 - - if len(rec0)<0x28 or rec0[0x10:0x14] != 'MOBI': - print "bad file format" - return 1 - print "Mobi publication type:", strDWord(rec0, 0x18) - formatVer = strDWord(rec0, 0x24) - print "Mobi format version:", formatVer - last_rec = strWord(rec0, 8) - dwE0 = 0 - if formatVer>=4: - new_rec0 = find_key(rec0, pid) - if new_rec0: - db.setRecordIdx(0,new_rec0) - else: - print "PID doesn't match this file" - return 2 - else: - print "Wrong Mobi format version" - return 1 - - outfname = replaceext(fname, ".azw") - if outfname==fname: - outfname = replaceext(fname, "_fixed.azw") - db.save(outfname) - print "Output written to "+outfname - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/Other_Tools/Additional_Tools/lib/kindlepid.py b/Other_Tools/Additional_Tools/lib/kindlepid.py deleted file mode 100644 index 5041bd4..0000000 --- a/Other_Tools/Additional_Tools/lib/kindlepid.py +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/python -# Mobipocket PID calculator v0.2 for Amazon Kindle. -# Copyright (c) 2007, 2009 Igor Skochinsky -# History: -# 0.1 Initial release -# 0.2 Added support for generating PID for iPhone (thanks to mbp) -# 0.3 changed to autoflush stdout, fixed return code usage -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 binascii - -if sys.hexversion >= 0x3000000: - print "This script is incompatible with Python 3.x. Please install Python 2.6.x from python.org" - sys.exit(2) - -letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" - -def crc32(s): - return (~binascii.crc32(s,-1))&0xFFFFFFFF - -def checksumPid(s): - crc = crc32(s) - crc = crc ^ (crc >> 16) - res = s - l = len(letters) - for i in (0,1): - b = crc & 0xff - pos = (b // l) ^ (b % l) - res += letters[pos%l] - crc >>= 8 - - return res - - -def pidFromSerial(s, l): - crc = crc32(s) - - arr1 = [0]*l - for i in xrange(len(s)): - arr1[i%l] ^= ord(s[i]) - - crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff] - for i in xrange(l): - arr1[i] ^= crc_bytes[i&3] - - pid = "" - for i in xrange(l): - b = arr1[i] & 0xff - pid+=letters[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))] - - return pid - -def main(argv=sys.argv): - print "Mobipocket PID calculator for Amazon Kindle. Copyright (c) 2007, 2009 Igor Skochinsky" - if len(sys.argv)==2: - serial = sys.argv[1] - else: - print "Usage: kindlepid.py /" - return 1 - if len(serial)==16: - if serial.startswith("B"): - print "Kindle serial number detected" - else: - print "Warning: unrecognized serial number. Please recheck input." - return 1 - pid = pidFromSerial(serial,7)+"*" - print "Mobipocked PID for Kindle serial# "+serial+" is "+checksumPid(pid) - return 0 - elif len(serial)==40: - print "iPhone serial number (UDID) detected" - pid = pidFromSerial(serial,8) - print "Mobipocked PID for iPhone serial# "+serial+" is "+checksumPid(pid) - return 0 - else: - print "Warning: unrecognized serial number. Please recheck input." - return 1 - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/Other_Tools/Additional_Tools/lib/mobidedrm.py b/Other_Tools/Additional_Tools/lib/mobidedrm.py deleted file mode 100644 index 1ad2bac..0000000 --- a/Other_Tools/Additional_Tools/lib/mobidedrm.py +++ /dev/null @@ -1,460 +0,0 @@ -#!/usr/bin/python -# -# This is a python script. You need a Python interpreter to run it. -# For example, ActiveState Python, which exists for windows. -# -# Changelog -# 0.01 - Initial version -# 0.02 - Huffdic compressed books were not properly decrypted -# 0.03 - Wasn't checking MOBI header length -# 0.04 - Wasn't sanity checking size of data record -# 0.05 - It seems that the extra data flags take two bytes not four -# 0.06 - And that low bit does mean something after all :-) -# 0.07 - The extra data flags aren't present in MOBI header < 0xE8 in size -# 0.08 - ...and also not in Mobi header version < 6 -# 0.09 - ...but they are there with Mobi header version 6, header size 0xE4! -# 0.10 - Outputs unencrypted files as-is, so that when run as a Calibre -# import filter it works when importing unencrypted files. -# Also now handles encrypted files that don't need a specific PID. -# 0.11 - use autoflushed stdout and proper return values -# 0.12 - Fix for problems with metadata import as Calibre plugin, report errors -# 0.13 - Formatting fixes: retabbed file, removed trailing whitespace -# and extra blank lines, converted CR/LF pairs at ends of each line, -# and other cosmetic fixes. -# 0.14 - Working out when the extra data flags are present has been problematic -# Versions 7 through 9 have tried to tweak the conditions, but have been -# only partially successful. Closer examination of lots of sample -# files reveals that a confusion has arisen because trailing data entries -# are not encrypted, but it turns out that the multibyte entries -# in utf8 file are encrypted. (Although neither kind gets compressed.) -# This knowledge leads to a simplification of the test for the -# trailing data byte flags - version 5 and higher AND header size >= 0xE4. -# 0.15 - Now outputs 'heartbeat', and is also quicker for long files. -# 0.16 - And reverts to 'done' not 'done.' at the end for unswindle compatibility. -# 0.17 - added modifications to support its use as an imported python module -# both inside calibre and also in other places (ie K4DeDRM tools) -# 0.17a- disabled the standalone plugin feature since a plugin can not import -# a plugin -# 0.18 - It seems that multibyte entries aren't encrypted in a v7 file... -# Removed the disabled Calibre plug-in code -# Permit use of 8-digit PIDs -# 0.19 - It seems that multibyte entries aren't encrypted in a v6 file either. -# 0.20 - Correction: It seems that multibyte entries are encrypted in a v6 file. -# 0.21 - Added support for multiple pids -# 0.22 - revised structure to hold MobiBook as a class to allow an extended interface -# 0.23 - fixed problem with older files with no EXTH section -# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well -# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption -# 0.26 - Now enables Text-To-Speech flag and sets clipping limit to 100% -# 0.27 - Correct pid metadata token generation to match that used by skindle (Thank You Bart!) -# 0.28 - slight additional changes to metadata token generation (None -> '') -# 0.29 - It seems that the ideas about when multibyte trailing characters were -# included in the encryption were wrong. They are for DOC compressed -# files, but they are not for HUFF/CDIC compress files! -# 0.30 - Modified interface slightly to work better with new calibre plugin style -# 0.31 - The multibyte encrytion info is true for version 7 files too. -# 0.32 - Added support for "Print Replica" Kindle ebooks -# 0.33 - Performance improvements for large files (concatenation) -# 0.34 - Performance improvements in decryption (libalfcrypto) -# 0.35 - add interface to get mobi_version -# 0.36 - fixed problem with TEXtREAd and getBookTitle interface -# 0.37 - Fixed double announcement for stand-alone operation - - -__version__ = '0.37' - -import sys - -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) -sys.stdout=Unbuffered(sys.stdout) - -import os -import struct -import binascii -from alfcrypto import Pukall_Cipher - -class DrmException(Exception): - pass - - -# -# MobiBook Utility Routines -# - -# Implementation of Pukall Cipher 1 -def PC1(key, src, decryption=True): - return Pukall_Cipher().PC1(key,src,decryption) -# sum1 = 0; -# sum2 = 0; -# keyXorVal = 0; -# if len(key)!=16: -# print "Bad key length!" -# return None -# wkey = [] -# for i in xrange(8): -# wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1])) -# dst = "" -# for i in xrange(len(src)): -# temp1 = 0; -# byteXorVal = 0; -# for j in xrange(8): -# temp1 ^= wkey[j] -# sum2 = (sum2+j)*20021 + sum1 -# sum1 = (temp1*346)&0xFFFF -# sum2 = (sum2+sum1)&0xFFFF -# temp1 = (temp1*20021+1)&0xFFFF -# byteXorVal ^= temp1 ^ sum2 -# curByte = ord(src[i]) -# if not decryption: -# keyXorVal = curByte * 257; -# curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF -# if decryption: -# keyXorVal = curByte * 257; -# for j in xrange(8): -# wkey[j] ^= keyXorVal; -# dst+=chr(curByte) -# return dst - -def checksumPid(s): - letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" - crc = (~binascii.crc32(s,-1))&0xFFFFFFFF - crc = crc ^ (crc >> 16) - res = s - l = len(letters) - for i in (0,1): - b = crc & 0xff - pos = (b // l) ^ (b % l) - res += letters[pos%l] - crc >>= 8 - return res - -def getSizeOfTrailingDataEntries(ptr, size, flags): - def getSizeOfTrailingDataEntry(ptr, size): - bitpos, result = 0, 0 - if size <= 0: - return result - while True: - v = ord(ptr[size-1]) - result |= (v & 0x7F) << bitpos - bitpos += 7 - size -= 1 - if (v & 0x80) != 0 or (bitpos >= 28) or (size == 0): - return result - num = 0 - testflags = flags >> 1 - while testflags: - if testflags & 1: - num += getSizeOfTrailingDataEntry(ptr, size - num) - testflags >>= 1 - # Check the low bit to see if there's multibyte data present. - # if multibyte data is included in the encryped data, we'll - # have already cleared this flag. - if flags & 1: - num += (ord(ptr[size - num - 1]) & 0x3) + 1 - return num - - - -class MobiBook: - def loadSection(self, section): - if (section + 1 == self.num_sections): - endoff = len(self.data_file) - else: - endoff = self.sections[section + 1][0] - off = self.sections[section][0] - return self.data_file[off:endoff] - - def __init__(self, infile, announce = True): - if announce: - print ('MobiDeDrm v%(__version__)s. ' - 'Copyright 2008-2012 The Dark Reverser et al.' % globals()) - - # initial sanity check on file - self.data_file = file(infile, 'rb').read() - self.mobi_data = '' - self.header = self.data_file[0:78] - if self.header[0x3C:0x3C+8] != 'BOOKMOBI' and self.header[0x3C:0x3C+8] != 'TEXtREAd': - raise DrmException("invalid file format") - self.magic = self.header[0x3C:0x3C+8] - self.crypto_type = -1 - - # build up section offset and flag info - self.num_sections, = struct.unpack('>H', self.header[76:78]) - self.sections = [] - for i in xrange(self.num_sections): - offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', self.data_file[78+i*8:78+i*8+8]) - flags, val = a1, a2<<16|a3<<8|a4 - self.sections.append( (offset, flags, val) ) - - # parse information from section 0 - self.sect = self.loadSection(0) - self.records, = struct.unpack('>H', self.sect[0x8:0x8+2]) - self.compression, = struct.unpack('>H', self.sect[0x0:0x0+2]) - - if self.magic == 'TEXtREAd': - print "Book has format: ", self.magic - self.extra_data_flags = 0 - self.mobi_length = 0 - self.mobi_codepage = 1252 - self.mobi_version = -1 - self.meta_array = {} - return - self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18]) - self.mobi_codepage, = struct.unpack('>L',self.sect[0x1c:0x20]) - self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C]) - print "MOBI header version = %d, length = %d" %(self.mobi_version, self.mobi_length) - self.extra_data_flags = 0 - if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5): - self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4]) - print "Extra Data Flags = %d" % self.extra_data_flags - if (self.compression != 17480): - # multibyte utf8 data is included in the encryption for PalmDoc compression - # so clear that byte so that we leave it to be decrypted. - self.extra_data_flags &= 0xFFFE - - # if exth region exists parse it for metadata array - self.meta_array = {} - try: - exth_flag, = struct.unpack('>L', self.sect[0x80:0x84]) - exth = 'NONE' - if exth_flag & 0x40: - exth = self.sect[16 + self.mobi_length:] - if (len(exth) >= 4) and (exth[:4] == 'EXTH'): - nitems, = struct.unpack('>I', exth[8:12]) - pos = 12 - for i in xrange(nitems): - type, size = struct.unpack('>II', exth[pos: pos + 8]) - content = exth[pos + 8: pos + size] - self.meta_array[type] = content - # reset the text to speech flag and clipping limit, if present - if type == 401 and size == 9: - # set clipping limit to 100% - self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8) - elif type == 404 and size == 9: - # make sure text to speech is enabled - self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8) - # print type, size, content, content.encode('hex') - pos += size - except: - self.meta_array = {} - pass - self.print_replica = False - - def getBookTitle(self): - codec_map = { - 1252 : 'windows-1252', - 65001 : 'utf-8', - } - title = '' - codec = 'windows-1252' - if self.magic == 'BOOKMOBI': - if 503 in self.meta_array: - title = self.meta_array[503] - else: - toff, tlen = struct.unpack('>II', self.sect[0x54:0x5c]) - tend = toff + tlen - title = self.sect[toff:tend] - if self.mobi_codepage in codec_map.keys(): - codec = codec_map[self.mobi_codepage] - if title == '': - title = self.header[:32] - title = title.split("\0")[0] - return unicode(title, codec).encode('utf-8') - - def getPIDMetaInfo(self): - rec209 = '' - token = '' - if 209 in self.meta_array: - rec209 = self.meta_array[209] - data = rec209 - # The 209 data comes in five byte groups. Interpret the last four bytes - # of each group as a big endian unsigned integer to get a key value - # if that key exists in the meta_array, append its contents to the token - for i in xrange(0,len(data),5): - val, = struct.unpack('>I',data[i+1:i+5]) - sval = self.meta_array.get(val,'') - token += sval - return rec209, token - - def patch(self, off, new): - self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):] - - def patchSection(self, section, new, in_off = 0): - if (section + 1 == self.num_sections): - endoff = len(self.data_file) - else: - endoff = self.sections[section + 1][0] - off = self.sections[section][0] - assert off + in_off + len(new) <= endoff - self.patch(off + in_off, new) - - def parseDRM(self, data, count, pidlist): - found_key = None - keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96" - for pid in pidlist: - bigpid = pid.ljust(16,'\0') - temp_key = PC1(keyvec1, bigpid, False) - temp_key_sum = sum(map(ord,temp_key)) & 0xff - found_key = None - for i in xrange(count): - verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30]) - if cksum == temp_key_sum: - cookie = PC1(temp_key, cookie) - ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie) - if verification == ver and (flags & 0x1F) == 1: - found_key = finalkey - break - if found_key != None: - break - if not found_key: - # Then try the default encoding that doesn't require a PID - pid = "00000000" - temp_key = keyvec1 - temp_key_sum = sum(map(ord,temp_key)) & 0xff - for i in xrange(count): - verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30]) - if cksum == temp_key_sum: - cookie = PC1(temp_key, cookie) - ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie) - if verification == ver: - found_key = finalkey - break - return [found_key,pid] - - def getMobiFile(self, outpath): - file(outpath,'wb').write(self.mobi_data) - - def getMobiVersion(self): - return self.mobi_version - - def getPrintReplica(self): - return self.print_replica - - def processBook(self, pidlist): - crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2]) - print 'Crypto Type is: ', crypto_type - self.crypto_type = crypto_type - if crypto_type == 0: - print "This book is not encrypted." - # we must still check for Print Replica - self.print_replica = (self.loadSection(1)[0:4] == '%MOP') - self.mobi_data = self.data_file - return - if crypto_type != 2 and crypto_type != 1: - raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type) - if 406 in self.meta_array: - data406 = self.meta_array[406] - val406, = struct.unpack('>Q',data406) - if val406 != 0: - raise DrmException("Cannot decode library or rented ebooks.") - - goodpids = [] - for pid in pidlist: - if len(pid)==10: - if checksumPid(pid[0:-2]) != pid: - print "Warning: PID " + pid + " has incorrect checksum, should have been "+checksumPid(pid[0:-2]) - goodpids.append(pid[0:-2]) - elif len(pid)==8: - goodpids.append(pid) - - if self.crypto_type == 1: - t1_keyvec = "QDCVEPMU675RUBSZ" - if self.magic == 'TEXtREAd': - bookkey_data = self.sect[0x0E:0x0E+16] - elif self.mobi_version < 0: - bookkey_data = self.sect[0x90:0x90+16] - else: - bookkey_data = self.sect[self.mobi_length+16:self.mobi_length+32] - pid = "00000000" - found_key = PC1(t1_keyvec, bookkey_data) - else : - # calculate the keys - drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16]) - if drm_count == 0: - raise DrmException("Not yet initialised with PID. Must be opened with Mobipocket Reader first.") - found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids) - if not found_key: - raise DrmException("No key found in " + str(len(goodpids)) + " keys tried. Please report this failure for help.") - # kill the drm keys - self.patchSection(0, "\0" * drm_size, drm_ptr) - # kill the drm pointers - self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8) - - if pid=="00000000": - print "File has default encryption, no specific PID." - else: - print "File is encoded with PID "+checksumPid(pid)+"." - - # clear the crypto type - self.patchSection(0, "\0" * 2, 0xC) - - # decrypt sections - print "Decrypting. Please wait . . .", - mobidataList = [] - mobidataList.append(self.data_file[:self.sections[1][0]]) - for i in xrange(1, self.records+1): - data = self.loadSection(i) - extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags) - if i%100 == 0: - print ".", - # print "record %d, extra_size %d" %(i,extra_size) - decoded_data = PC1(found_key, data[0:len(data) - extra_size]) - if i==1: - self.print_replica = (decoded_data[0:4] == '%MOP') - mobidataList.append(decoded_data) - if extra_size > 0: - mobidataList.append(data[-extra_size:]) - if self.num_sections > self.records+1: - mobidataList.append(self.data_file[self.sections[self.records+1][0]:]) - self.mobi_data = "".join(mobidataList) - print "done" - return - -def getUnencryptedBook(infile,pid,announce=True): - if not os.path.isfile(infile): - raise DrmException('Input File Not Found') - book = MobiBook(infile,announce) - book.processBook([pid]) - return book.mobi_data - -def getUnencryptedBookWithList(infile,pidlist,announce=True): - if not os.path.isfile(infile): - raise DrmException('Input File Not Found') - book = MobiBook(infile, announce) - book.processBook(pidlist) - return book.mobi_data - - -def main(argv=sys.argv): - print ('MobiDeDrm v%(__version__)s. ' - 'Copyright 2008-2012 The Dark Reverser et al.' % globals()) - if len(argv)<3 or len(argv)>4: - print "Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks" - print "Usage:" - print " %s []" % sys.argv[0] - return 1 - else: - infile = argv[1] - outfile = argv[2] - if len(argv) is 4: - pidlist = argv[3].split(',') - else: - pidlist = {} - try: - stripped_file = getUnencryptedBookWithList(infile, pidlist, False) - file(outfile, 'wb').write(stripped_file) - except DrmException, e: - print "Error: %s" % e - return 1 - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/Other_Tools/Additional_Tools/lib/mobihuff.py b/Other_Tools/Additional_Tools/lib/mobihuff.py deleted file mode 100644 index fe30719..0000000 --- a/Other_Tools/Additional_Tools/lib/mobihuff.py +++ /dev/null @@ -1,189 +0,0 @@ -# This is a python script. You need a Python interpreter to run it. -# For example, ActiveState Python, which exists for windows. -# -# Big Thanks to Igor SKOCHINSKY for providing me with all his information -# and source code relating to the inner workings of this compression scheme. -# Without it, I wouldn't be able to solve this as easily. -# -# Changelog -# 0.01 - Initial version -# 0.02 - Fix issue with size computing -# 0.03 - Fix issue with some files -# 0.04 - make stdout self flushing and fix return values - -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 struct - -class BitReader: - def __init__(self, data): - self.data, self.pos, self.nbits = data + "\x00\x00\x00\x00", 0, len(data) * 8 - def peek(self, n): - r, g = 0, 0 - while g < n: - r, g = (r << 8) | ord(self.data[(self.pos+g)>>3]), g + 8 - ((self.pos+g) & 7) - return (r >> (g - n)) & ((1 << n) - 1) - def eat(self, n): - self.pos += n - return self.pos <= self.nbits - def left(self): - return self.nbits - self.pos - -class HuffReader: - def __init__(self, huffs): - self.huffs = huffs - h = huffs[0] - if huffs[0][0:4] != 'HUFF' or huffs[0][4:8] != '\x00\x00\x00\x18': - raise ValueError('invalid huff1 header') - if huffs[1][0:4] != 'CDIC' or huffs[1][4:8] != '\x00\x00\x00\x10': - raise ValueError('invalid huff2 header') - self.entry_bits, = struct.unpack('>L', huffs[1][12:16]) - off1,off2 = struct.unpack('>LL', huffs[0][16:24]) - self.dict1 = struct.unpack('<256L', huffs[0][off1:off1+256*4]) - self.dict2 = struct.unpack('<64L', huffs[0][off2:off2+64*4]) - self.dicts = huffs[1:] - self.r = '' - - def _unpack(self, bits, depth = 0): - if depth > 32: - raise ValueError('corrupt file') - while bits.left(): - dw = bits.peek(32) - v = self.dict1[dw >> 24] - codelen = v & 0x1F - assert codelen != 0 - code = dw >> (32 - codelen) - r = (v >> 8) - if not (v & 0x80): - while code < self.dict2[(codelen-1)*2]: - codelen += 1 - code = dw >> (32 - codelen) - r = self.dict2[(codelen-1)*2+1] - r -= code - assert codelen != 0 - if not bits.eat(codelen): - return - dicno = r >> self.entry_bits - off1 = 16 + (r - (dicno << self.entry_bits)) * 2 - dic = self.dicts[dicno] - off2 = 16 + ord(dic[off1]) * 256 + ord(dic[off1+1]) - blen = ord(dic[off2]) * 256 + ord(dic[off2+1]) - slice = dic[off2+2:off2+2+(blen&0x7fff)] - if blen & 0x8000: - self.r += slice - else: - self._unpack(BitReader(slice), depth + 1) - - def unpack(self, data): - self.r = '' - self._unpack(BitReader(data)) - return self.r - -class Sectionizer: - def __init__(self, filename, ident): - self.contents = file(filename, 'rb').read() - self.header = self.contents[0:72] - self.num_sections, = struct.unpack('>H', self.contents[76:78]) - if self.header[0x3C:0x3C+8] != ident: - raise ValueError('Invalid file format') - self.sections = [] - for i in xrange(self.num_sections): - offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', self.contents[78+i*8:78+i*8+8]) - flags, val = a1, a2<<16|a3<<8|a4 - self.sections.append( (offset, flags, val) ) - def loadSection(self, section): - if section + 1 == self.num_sections: - end_off = len(self.contents) - else: - end_off = self.sections[section + 1][0] - off = self.sections[section][0] - return self.contents[off:end_off] - - -def getSizeOfTrailingDataEntry(ptr, size): - bitpos, result = 0, 0 - while True: - v = ord(ptr[size-1]) - result |= (v & 0x7F) << bitpos - bitpos += 7 - size -= 1 - if (v & 0x80) != 0 or (bitpos >= 28) or (size == 0): - return result - -def getSizeOfTrailingDataEntries(ptr, size, flags): - num = 0 - flags >>= 1 - while flags: - if flags & 1: - num += getSizeOfTrailingDataEntry(ptr, size - num) - flags >>= 1 - return num - -def unpackBook(input_file): - sect = Sectionizer(input_file, 'BOOKMOBI') - - header = sect.loadSection(0) - - crypto_type, = struct.unpack('>H', header[0xC:0xC+2]) - if crypto_type != 0: - raise ValueError('The book is encrypted. Run mobidedrm first') - - if header[0:2] != 'DH': - raise ValueError('invalid compression type') - - extra_flags, = struct.unpack('>L', header[0xF0:0xF4]) - records, = struct.unpack('>H', header[0x8:0x8+2]) - - huffoff,huffnum = struct.unpack('>LL', header[0x70:0x78]) - huffs = [sect.loadSection(i) for i in xrange(huffoff, huffoff+huffnum)] - huff = HuffReader(huffs) - - def decompressSection(nr): - data = sect.loadSection(nr) - trail_size = getSizeOfTrailingDataEntries(data, len(data), extra_flags) - return huff.unpack(data[0:len(data)-trail_size]) - - r = '' - for i in xrange(1, records+1): - r += decompressSection(i) - return r - -def main(argv=sys.argv): - print "MobiHuff v0.03" - print " Copyright (c) 2008 The Dark Reverser " - if len(sys.argv)!=3: - print "" - print "Description:" - print " Unpacks the new mobipocket huffdic compression." - print " This program works with unencrypted files only." - print "Usage:" - print " mobihuff.py infile.mobi outfile.html" - return 1 - else: - infile = sys.argv[1] - outfile = sys.argv[2] - try: - print "Decompressing...", - result = unpackBook(infile) - file(outfile, 'wb').write(result) - print "done" - except ValueError, e: - print - print "Error: %s" % e - return 1 - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/Other_Tools/Additional_Tools/lib/prc.py b/Other_Tools/Additional_Tools/lib/prc.py deleted file mode 100644 index c65370c..0000000 --- a/Other_Tools/Additional_Tools/lib/prc.py +++ /dev/null @@ -1,529 +0,0 @@ -# -# $Id: prc.py,v 1.3 2001/12/27 08:48:02 rob Exp $ -# -# Copyright 1998-2001 Rob Tillotson -# All Rights Reserved -# -# Permission to use, copy, modify, and distribute this software and -# its documentation for any purpose and without fee or royalty is -# hereby granted, provided that the above copyright notice appear in -# all copies and that both the copyright notice and this permission -# notice appear in supporting documentation or portions thereof, -# including modifications, that you you make. -# -# THE AUTHOR ROB TILLOTSON DISCLAIMS ALL WARRANTIES WITH REGARD TO -# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -# SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE! -# -"""PRC/PDB file I/O in pure Python. - - This module serves two purposes: one, it allows access to Palm OS(tm) - database files on the desktop in pure Python without requiring - pilot-link (hence, it may be useful for import/export utilities), - and two, it caches the contents of the file in memory so it can - be freely modified using an identical API to databases over a - DLP connection. -""" - -__version__ = '$Id: prc.py,v 1.3 2001/12/27 08:48:02 rob Exp $' - -__copyright__ = 'Copyright 1998-2001 Rob Tillotson ' - - -# temporary hack until we get gettext support again -def _(s): return s - -# -# DBInfo structure: -# -# int more -# unsigned int flags -# unsigned int miscflags -# unsigned long type -# unsigned long creator -# unsigned int version -# unsigned long modnum -# time_t createDate, modifydate, backupdate -# unsigned int index -# char name[34] -# -# -# DB Header: -# 32 name -# 2 flags -# 2 version -# 4 creation time -# 4 modification time -# 4 backup time -# 4 modification number -# 4 appinfo offset -# 4 sortinfo offset -# 4 type -# 4 creator -# 4 unique id seed (garbage?) -# 4 next record list id (normally 0) -# 2 num of records for this header -# (maybe 2 more bytes) -# -# Resource entry header: (if low bit of attr = 1) -# 4 type -# 2 id -# 4 offset -# -# record entry header: (if low bit of attr = 0) -# 4 offset -# 1 attributes -# 3 unique id -# -# then 2 bytes of 0 -# -# then appinfo then sortinfo -# - -import sys, os, stat, struct - -PI_HDR_SIZE = 78 -PI_RESOURCE_ENT_SIZE = 10 -PI_RECORD_ENT_SIZE = 8 - -PILOT_TIME_DELTA = 2082844800L - -flagResource = 0x0001 -flagReadOnly = 0x0002 -flagAppInfoDirty = 0x0004 -flagBackup = 0x0008 -flagOpen = 0x8000 -# 2.x -flagNewer = 0x0010 -flagReset = 0x0020 -# -flagExcludeFromSync = 0x0080 - -attrDeleted = 0x80 -attrDirty = 0x40 -attrBusy = 0x20 -attrSecret = 0x10 -attrArchived = 0x08 - -default_info = { - 'name': '', - 'type': 'DATA', - 'creator': ' ', - 'createDate': 0, - 'modifyDate': 0, - 'backupDate': 0, - 'modnum': 0, - 'version': 0, - 'flagReset': 0, - 'flagResource': 0, - 'flagNewer': 0, - 'flagExcludeFromSync': 0, - 'flagAppInfoDirty': 0, - 'flagReadOnly': 0, - 'flagBackup': 0, - 'flagOpen': 0, - 'more': 0, - 'index': 0 - } - -def null_terminated(s): - for x in range(0, len(s)): - if s[x] == '\000': return s[:x] - return s - -def trim_null(s): - return string.split(s, '\0')[0] - -def pad_null(s, l): - if len(s) > l - 1: - s = s[:l-1] - s = s + '\0' - if len(s) < l: s = s + '\0' * (l - len(s)) - return s - -# -# new stuff - -# Record object to be put in tree... -class PRecord: - def __init__(self, attr=0, id=0, category=0, raw=''): - self.raw = raw - self.id = id - self.attr = attr - self.category = category - - # comparison and hashing are done by ID; - # thus, the id value *may not be changed* once - # the object is created. - def __cmp__(self, obj): - if type(obj) == type(0): - return cmp(self.id, obj) - else: - return cmp(self.id, obj.id) - - def __hash__(self): - return self.id - -class PResource: - def __init__(self, typ=' ', id=0, raw=''): - self.raw = raw - self.id = id - self.type = typ - - def __cmp__(self, obj): - if type(obj) == type(()): - return cmp( (self.type, self.id), obj) - else: - return cmp( (self.type, self.id), (obj.type, obj.id) ) - - def __hash__(self): - return hash((self.type, self.id)) - - -class PCache: - def __init__(self): - self.data = [] - self.appblock = '' - self.sortblock = '' - self.dirty = 0 - self.next = 0 - self.info = {} - self.info.update(default_info) - # if allow_zero_ids is 1, then this prc behaves appropriately - # for a desktop database. That is, it never attempts to assign - # an ID, and lets new records be inserted with an ID of zero. - self.allow_zero_ids = 0 - - # pi-file API - def getRecords(self): return len(self.data) - def getAppBlock(self): return self.appblock and self.appblock or None - def setAppBlock(self, raw): - self.dirty = 1 - self.appblock = raw - def getSortBlock(self): return self.sortblock and self.sortblock or None - def setSortBlock(self, raw): - self.dirty = 1 - self.appblock = raw - def checkID(self, id): return id in self.data - def getRecord(self, i): - try: r = self.data[i] - except: return None - return r.raw, i, r.id, r.attr, r.category - def getRecordByID(self, id): - try: - i = self.data.index(id) - r = self.data[i] - except: return None - return r.raw, i, r.id, r.attr, r.category - def getResource(self, i): - try: r = self.data[i] - except: return None - return r.raw, r.type, r.id - def getDBInfo(self): return self.info - def setDBInfo(self, info): - self.dirty = 1 - self.info = {} - self.info.update(info) - - def updateDBInfo(self, info): - self.dirty = 1 - self.info.update(info) - - def setRecord(self, attr, id, cat, data): - if not self.allow_zero_ids and not id: - if not len(self.data): id = 1 - else: - xid = self.data[0].id + 1 - while xid in self.data: xid = xid + 1 - id = xid - - r = PRecord(attr, id, cat, data) - if id and id in self.data: - self.data.remove(id) - self.data.append(r) - self.dirty = 1 - return id - - def setRecordIdx(self, i, data): - self.data[i].raw = data - self.dirty = 1 - - def setResource(self, typ, id, data): - if (typ, id) in self.data: - self.data.remove((typ,id)) - r = PResource(typ, id, data) - self.data.append(r) - self.dirty = 1 - return id - - def getNextRecord(self, cat): - while self.next < len(self.data): - r = self.data[self.next] - i = self.next - self.next = self.next + 1 - if r.category == cat: - return r.raw, i, r.id, r.attr, r.category - return '' - - def getNextModRecord(self, cat=-1): - while self.next < len(self.data): - r = self.data[self.next] - i = self.next - self.next = self.next + 1 - if (r.attr & attrModified) and (cat < 0 or r.category == cat): - return r.raw, i, r.id, r.attr, r.category - - def getResourceByID(self, type, id): - try: r = self.data[self.data.index((type,id))] - except: return None - return r.raw, r.type, r.id - - def deleteRecord(self, id): - if not id in self.data: return None - self.data.remove(id) - self.dirty = 1 - - def deleteRecords(self): - self.data = [] - self.dirty = 1 - - def deleteResource(self, type, id): - if not (type,id) in self.data: return None - self.data.remove((type,id)) - self.dirty = 1 - - def deleteResources(self): - self.data = [] - self.dirty = 1 - - def getRecordIDs(self, sort=0): - m = map(lambda x: x.id, self.data) - if sort: m.sort() - return m - - def moveCategory(self, frm, to): - for r in self.data: - if r.category == frm: - r.category = to - self.dirty = 1 - - def deleteCategory(self, cat): - raise RuntimeError, _("unimplemented") - - def purge(self): - ndata = [] - # change to filter later - for r in self.data: - if (r.attr & attrDeleted): - continue - ndata.append(r) - self.data = ndata - self.dirty = 1 - - def resetNext(self): - self.next = 0 - - def resetFlags(self): - # special behavior for resources - if not self.info.get('flagResource',0): - # use map() - for r in self.data: - r.attr = r.attr & ~attrDirty - self.dirty = 1 - -import pprint -class File(PCache): - def __init__(self, name=None, read=1, write=0, info={}): - PCache.__init__(self) - self.filename = name - self.info.update(info) - self.writeback = write - self.isopen = 0 - - if read: - self.load(name) - self.isopen = 1 - - def close(self): - if self.writeback and self.dirty: - self.save(self.filename) - self.isopen = 0 - - def __del__(self): - if self.isopen: self.close() - - def load(self, f): - if type(f) == type(''): f = open(f, 'rb') - - data = f.read() - self.unpack(data) - - def unpack(self, data): - if len(data) < PI_HDR_SIZE: raise IOError, _("file too short") - (name, flags, ver, ctime, mtime, btime, mnum, appinfo, sortinfo, - typ, creator, uid, nextrec, numrec) \ - = struct.unpack('>32shhLLLlll4s4sllh', data[:PI_HDR_SIZE]) - - if nextrec or appinfo < 0 or sortinfo < 0 or numrec < 0: - raise IOError, _("invalid database header") - - self.info = { - 'name': null_terminated(name), - 'type': typ, - 'creator': creator, - 'createDate': ctime - PILOT_TIME_DELTA, - 'modifyDate': mtime - PILOT_TIME_DELTA, - 'backupDate': btime - PILOT_TIME_DELTA, - 'modnum': mnum, - 'version': ver, - 'flagReset': flags & flagReset, - 'flagResource': flags & flagResource, - 'flagNewer': flags & flagNewer, - 'flagExcludeFromSync': flags & flagExcludeFromSync, - 'flagAppInfoDirty': flags & flagAppInfoDirty, - 'flagReadOnly': flags & flagReadOnly, - 'flagBackup': flags & flagBackup, - 'flagOpen': flags & flagOpen, - 'more': 0, - 'index': 0 - } - - rsrc = flags & flagResource - if rsrc: s = PI_RESOURCE_ENT_SIZE - else: s = PI_RECORD_ENT_SIZE - - entries = [] - - pos = PI_HDR_SIZE - for x in range(0,numrec): - hstr = data[pos:pos+s] - pos = pos + s - if not hstr or len(hstr) < s: - raise IOError, _("bad database header") - - if rsrc: - (typ, id, offset) = struct.unpack('>4shl', hstr) - entries.append((offset, typ, id)) - else: - (offset, auid) = struct.unpack('>ll', hstr) - attr = (auid & 0xff000000) >> 24 - uid = auid & 0x00ffffff - entries.append((offset, attr, uid)) - - offset = len(data) - entries.reverse() - for of, q, id in entries: - size = offset - of - if size < 0: raise IOError, _("bad pdb/prc record entry (size < 0)") - d = data[of:offset] - offset = of - if len(d) != size: raise IOError, _("failed to read record") - if rsrc: - r = PResource(q, id, d) - self.data.append(r) - else: - r = PRecord(q & 0xf0, id, q & 0x0f, d) - self.data.append(r) - self.data.reverse() - - if sortinfo: - sortinfo_size = offset - sortinfo - offset = sortinfo - else: - sortinfo_size = 0 - - if appinfo: - appinfo_size = offset - appinfo - offset = appinfo - else: - appinfo_size = 0 - - if appinfo_size < 0 or sortinfo_size < 0: - raise IOError, _("bad database header (appinfo or sortinfo size < 0)") - - if appinfo_size: - self.appblock = data[appinfo:appinfo+appinfo_size] - if len(self.appblock) != appinfo_size: - raise IOError, _("failed to read appinfo block") - - if sortinfo_size: - self.sortblock = data[sortinfo:sortinfo+sortinfo_size] - if len(self.sortblock) != sortinfo_size: - raise IOError, _("failed to read sortinfo block") - - def save(self, f): - """Dump the cache to a file. - """ - if type(f) == type(''): f = open(f, 'wb') - - # first, we need to precalculate the offsets. - if self.info.get('flagResource'): - entries_len = 10 * len(self.data) - else: entries_len = 8 * len(self.data) - - off = PI_HDR_SIZE + entries_len + 2 - if self.appblock: - appinfo_offset = off - off = off + len(self.appblock) - else: - appinfo_offset = 0 - if self.sortblock: - sortinfo_offset = off - off = off + len(self.sortblock) - else: - sortinfo_offset = 0 - - rec_offsets = [] - for x in self.data: - rec_offsets.append(off) - off = off + len(x.raw) - - info = self.info - flg = 0 - if info.get('flagResource',0): flg = flg | flagResource - if info.get('flagReadOnly',0): flg = flg | flagReadOnly - if info.get('flagAppInfoDirty',0): flg = flg | flagAppInfoDirty - if info.get('flagBackup',0): flg = flg | flagBackup - if info.get('flagOpen',0): flg = flg | flagOpen - if info.get('flagNewer',0): flg = flg | flagNewer - if info.get('flagReset',0): flg = flg | flagReset - # excludefromsync doesn't actually get stored? - hdr = struct.pack('>32shhLLLlll4s4sllh', - pad_null(info.get('name',''), 32), - flg, - info.get('version',0), - info.get('createDate',0L)+PILOT_TIME_DELTA, - info.get('modifyDate',0L)+PILOT_TIME_DELTA, - info.get('backupDate',0L)+PILOT_TIME_DELTA, - info.get('modnum',0), - appinfo_offset, # appinfo - sortinfo_offset, # sortinfo - info.get('type',' '), - info.get('creator',' '), - 0, # uid??? - 0, # nextrec??? - len(self.data)) - - f.write(hdr) - - entries = [] - record_data = [] - rsrc = self.info.get('flagResource') - for x, off in map(None, self.data, rec_offsets): - if rsrc: - record_data.append(x.raw) - entries.append(struct.pack('>4shl', x.type, x.id, off)) - else: - record_data.append(x.raw) - a = ((x.attr | x.category) << 24) | x.id - entries.append(struct.pack('>ll', off, a)) - - for x in entries: f.write(x) - f.write('\0\0') # padding? dunno, it's always there. - if self.appblock: f.write(self.appblock) - if self.sortblock: f.write(self.sortblock) - for x in record_data: f.write(x) diff --git a/Other_Tools/Additional_Tools/lib/scrolltextwidget.py b/Other_Tools/Additional_Tools/lib/scrolltextwidget.py deleted file mode 100644 index 98b4147..0000000 --- a/Other_Tools/Additional_Tools/lib/scrolltextwidget.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab - -import Tkinter -import Tkconstants - -# basic scrolled text widget -class ScrolledText(Tkinter.Text): - def __init__(self, master=None, **kw): - self.frame = Tkinter.Frame(master) - self.vbar = Tkinter.Scrollbar(self.frame) - self.vbar.pack(side=Tkconstants.RIGHT, fill=Tkconstants.Y) - kw.update({'yscrollcommand': self.vbar.set}) - Tkinter.Text.__init__(self, self.frame, **kw) - self.pack(side=Tkconstants.LEFT, fill=Tkconstants.BOTH, expand=True) - self.vbar['command'] = self.yview - # Copy geometry methods of self.frame without overriding Text - # methods = hack! - text_meths = vars(Tkinter.Text).keys() - methods = vars(Tkinter.Pack).keys() + vars(Tkinter.Grid).keys() + vars(Tkinter.Place).keys() - methods = set(methods).difference(text_meths) - for m in methods: - if m[0] != '_' and m != 'config' and m != 'configure': - setattr(self, m, getattr(self.frame, m)) - - def __str__(self): - return str(self.frame) diff --git a/Other_Tools/Additional_Tools/lib/subasyncio.py b/Other_Tools/Additional_Tools/lib/subasyncio.py deleted file mode 100644 index ed13aa1..0000000 --- a/Other_Tools/Additional_Tools/lib/subasyncio.py +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab - -import os, sys -import signal -import threading -import subprocess -from subprocess import Popen, PIPE, STDOUT - -# **heavily** chopped up and modfied version of asyncproc.py -# to make it actually work on Windows as well as Mac/Linux -# For the original see: -# "http://www.lysator.liu.se/~bellman/download/" -# author is "Thomas Bellman " -# available under GPL version 3 or Later - -# create an asynchronous subprocess whose output can be collected in -# a non-blocking manner - -# What a mess! Have to use threads just to get non-blocking io -# in a cross-platform manner - -# luckily all thread use is hidden within this class - -class Process(object): - def __init__(self, *params, **kwparams): - if len(params) <= 3: - kwparams.setdefault('stdin', subprocess.PIPE) - if len(params) <= 4: - kwparams.setdefault('stdout', subprocess.PIPE) - if len(params) <= 5: - kwparams.setdefault('stderr', subprocess.PIPE) - self.__pending_input = [] - self.__collected_outdata = [] - self.__collected_errdata = [] - self.__exitstatus = None - self.__lock = threading.Lock() - self.__inputsem = threading.Semaphore(0) - self.__quit = False - - self.__process = subprocess.Popen(*params, **kwparams) - - if self.__process.stdin: - self.__stdin_thread = threading.Thread( - name="stdin-thread", - target=self.__feeder, args=(self.__pending_input, - self.__process.stdin)) - self.__stdin_thread.setDaemon(True) - self.__stdin_thread.start() - - if self.__process.stdout: - self.__stdout_thread = threading.Thread( - name="stdout-thread", - target=self.__reader, args=(self.__collected_outdata, - self.__process.stdout)) - self.__stdout_thread.setDaemon(True) - self.__stdout_thread.start() - - if self.__process.stderr: - self.__stderr_thread = threading.Thread( - name="stderr-thread", - target=self.__reader, args=(self.__collected_errdata, - self.__process.stderr)) - self.__stderr_thread.setDaemon(True) - self.__stderr_thread.start() - - def pid(self): - return self.__process.pid - - def kill(self, signal): - self.__process.send_signal(signal) - - # check on subprocess (pass in 'nowait') to act like poll - def wait(self, flag): - if flag.lower() == 'nowait': - rc = self.__process.poll() - else: - rc = self.__process.wait() - if rc != None: - if self.__process.stdin: - self.closeinput() - if self.__process.stdout: - self.__stdout_thread.join() - if self.__process.stderr: - self.__stderr_thread.join() - return self.__process.returncode - - def terminate(self): - if self.__process.stdin: - self.closeinput() - self.__process.terminate() - - # thread gets data from subprocess stdout - def __reader(self, collector, source): - while True: - data = os.read(source.fileno(), 65536) - self.__lock.acquire() - collector.append(data) - self.__lock.release() - if data == "": - source.close() - break - return - - # thread feeds data to subprocess stdin - def __feeder(self, pending, drain): - while True: - self.__inputsem.acquire() - self.__lock.acquire() - if not pending and self.__quit: - drain.close() - self.__lock.release() - break - data = pending.pop(0) - self.__lock.release() - drain.write(data) - - # non-blocking read of data from subprocess stdout - def read(self): - self.__lock.acquire() - outdata = "".join(self.__collected_outdata) - del self.__collected_outdata[:] - self.__lock.release() - return outdata - - # non-blocking read of data from subprocess stderr - def readerr(self): - self.__lock.acquire() - errdata = "".join(self.__collected_errdata) - del self.__collected_errdata[:] - self.__lock.release() - return errdata - - # non-blocking write to stdin of subprocess - def write(self, data): - if self.__process.stdin is None: - raise ValueError("Writing to process with stdin not a pipe") - self.__lock.acquire() - self.__pending_input.append(data) - self.__inputsem.release() - self.__lock.release() - - # close stdinput of subprocess - def closeinput(self): - self.__lock.acquire() - self.__quit = True - self.__inputsem.release() - self.__lock.release() - diff --git a/Other_Tools/Adobe_PDF_Tools/README_ineptpdf.txt b/Other_Tools/Adobe_PDF_Tools/README_ineptpdf.txt deleted file mode 100644 index 2b03d83..0000000 --- a/Other_Tools/Adobe_PDF_Tools/README_ineptpdf.txt +++ /dev/null @@ -1,18 +0,0 @@ -From Apprentice Alf's Blog - -Adobe Adept PDF, .pdf - -This directory includes modified versions of the I♥CABBAGES Adobe Adept inept scripts for pdfs. These scripts have been modified to work with OpenSSL on Windows as well as Linux and Mac OS X. If a Windows User has OpenSSL installed, these scripts will make use of it in place of PyCrypto. - -The wonderful I♥CABBAGES has produced scripts that will remove the DRM from ePubs and PDFs encryped with Adobe’s DRM. These scripts require installation of the PyCrypto python package *or* the OpenSSL library on Windows. For Mac OS X and Linux boxes, these scripts use the already installed OpenSSL libcrypto so there is no additional requirements for these platforms. - -For more info, see the author's blog: -http://i-u2665-cabbages.blogspot.com/2009_02_01_archive.html - -There are two scripts: - -The first is called ineptkey_vX.X.pyw. Simply double-click to launch it and it will create a key file that is needed later to actually remove the DRM. This script need only be run once unless you change your ADE account information. - -The second is called in ineptpdf_vX.X.pyw. Simply double-click to launch it. It will ask for your previously generated key file and the path to the book you want to remove the DRM from. - -Both of these scripts are gui python programs. Python 2.X (32 bit) is already installed in Mac OSX. We recommend ActiveState's Active Python Version 2.X (32 bit) for Windows users. diff --git a/Other_Tools/Adobe_PDF_Tools/ineptkey.pyw b/Other_Tools/Adobe_PDF_Tools/ineptkey.pyw deleted file mode 100644 index daa9889..0000000 --- a/Other_Tools/Adobe_PDF_Tools/ineptkey.pyw +++ /dev/null @@ -1,468 +0,0 @@ -#! /usr/bin/python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# ineptkey.pyw, version 5.5 -# Copyright © 2009-2010 i♥cabbages - -# Released under the terms of the GNU General Public Licence, version 3 or -# later. - -# Windows users: Before running this program, you must first install Python 2.6 -# from and PyCrypto from -# (make certain -# to install the version for Python 2.6). Then save this script file as -# ineptkey.pyw and double-click on it to run it. It will create a file named -# adeptkey.der in the same directory. This is your ADEPT user key. -# -# Mac OS X users: Save this script file as ineptkey.pyw. You can run this -# program from the command line (pythonw ineptkey.pyw) or by double-clicking -# it when it has been associated with PythonLauncher. It will create a file -# named adeptkey.der in the same directory. This is your ADEPT user key. - -# Revision history: -# 1 - Initial release, for Adobe Digital Editions 1.7 -# 2 - Better algorithm for finding pLK; improved error handling -# 3 - Rename to INEPT -# 4 - Series of changes by joblack (and others?) -- -# 4.1 - quick beta fix for ADE 1.7.2 (anon) -# 4.2 - added old 1.7.1 processing -# 4.3 - better key search -# 4.4 - Make it working on 64-bit Python -# 5 - Clean up and improve 4.x changes; -# Clean up and merge OS X support by unknown -# 5.1 - add support for using OpenSSL on Windows in place of PyCrypto -# 5.2 - added support for output of key to a particular file -# 5.3 - On Windows try PyCrypto first, OpenSSL next -# 5.4 - Modify interface to allow use of import -# 5.5 - Fix for potential problem with PyCrypto - -""" -Retrieve Adobe ADEPT user key. -""" - -__license__ = 'GPL v3' - -import sys -import os -import struct -import Tkinter -import Tkconstants -import tkMessageBox -import traceback - -class ADEPTError(Exception): - pass - -if sys.platform.startswith('win'): - from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \ - create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \ - string_at, Structure, c_void_p, cast, c_size_t, memmove, CDLL, c_int, \ - c_long, c_ulong - - from ctypes.wintypes import LPVOID, DWORD, BOOL - import _winreg as winreg - - def _load_crypto_libcrypto(): - from ctypes.util import find_library - libcrypto = find_library('libeay32') - if libcrypto is None: - raise ADEPTError('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_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): - self._blocksize = len(userkey) - if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : - raise ADEPTError('AES improper key used') - key = self._key = AES_KEY() - rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key) - if rv < 0: - raise ADEPTError('Failed to initialize AES key') - 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 ADEPTError('AES decryption failed') - return out.raw - 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 - for loader in (_load_crypto_pycrypto, _load_crypto_libcrypto): - try: - AES = loader() - break - except (ImportError, ADEPTError): - pass - return AES - - AES = _load_crypto() - - - DEVICE_KEY_PATH = r'Software\Adobe\Adept\Device' - PRIVATE_LICENCE_KEY_PATH = r'Software\Adobe\Adept\Activation' - - MAX_PATH = 255 - - kernel32 = windll.kernel32 - advapi32 = windll.advapi32 - crypt32 = windll.crypt32 - - def GetSystemDirectory(): - GetSystemDirectoryW = kernel32.GetSystemDirectoryW - GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint] - GetSystemDirectoryW.restype = c_uint - def GetSystemDirectory(): - buffer = create_unicode_buffer(MAX_PATH + 1) - GetSystemDirectoryW(buffer, len(buffer)) - return buffer.value - return GetSystemDirectory - GetSystemDirectory = GetSystemDirectory() - - def GetVolumeSerialNumber(): - GetVolumeInformationW = kernel32.GetVolumeInformationW - GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint, - POINTER(c_uint), POINTER(c_uint), - POINTER(c_uint), c_wchar_p, c_uint] - GetVolumeInformationW.restype = c_uint - def GetVolumeSerialNumber(path): - vsn = c_uint(0) - GetVolumeInformationW( - path, None, 0, byref(vsn), None, None, None, 0) - return vsn.value - return GetVolumeSerialNumber - GetVolumeSerialNumber = GetVolumeSerialNumber() - - def GetUserName(): - GetUserNameW = advapi32.GetUserNameW - GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)] - GetUserNameW.restype = c_uint - def GetUserName(): - buffer = create_unicode_buffer(32) - size = c_uint(len(buffer)) - while not GetUserNameW(buffer, byref(size)): - buffer = create_unicode_buffer(len(buffer) * 2) - size.value = len(buffer) - return buffer.value.encode('utf-16-le')[::2] - return GetUserName - GetUserName = GetUserName() - - PAGE_EXECUTE_READWRITE = 0x40 - MEM_COMMIT = 0x1000 - MEM_RESERVE = 0x2000 - - def VirtualAlloc(): - _VirtualAlloc = kernel32.VirtualAlloc - _VirtualAlloc.argtypes = [LPVOID, c_size_t, DWORD, DWORD] - _VirtualAlloc.restype = LPVOID - def VirtualAlloc(addr, size, alloctype=(MEM_COMMIT | MEM_RESERVE), - protect=PAGE_EXECUTE_READWRITE): - return _VirtualAlloc(addr, size, alloctype, protect) - return VirtualAlloc - VirtualAlloc = VirtualAlloc() - - MEM_RELEASE = 0x8000 - - def VirtualFree(): - _VirtualFree = kernel32.VirtualFree - _VirtualFree.argtypes = [LPVOID, c_size_t, DWORD] - _VirtualFree.restype = BOOL - def VirtualFree(addr, size=0, freetype=MEM_RELEASE): - return _VirtualFree(addr, size, freetype) - return VirtualFree - VirtualFree = VirtualFree() - - class NativeFunction(object): - def __init__(self, restype, argtypes, insns): - self._buf = buf = VirtualAlloc(None, len(insns)) - memmove(buf, insns, len(insns)) - ftype = CFUNCTYPE(restype, *argtypes) - self._native = ftype(buf) - - def __call__(self, *args): - return self._native(*args) - - def __del__(self): - if self._buf is not None: - VirtualFree(self._buf) - self._buf = None - - if struct.calcsize("P") == 4: - CPUID0_INSNS = ( - "\x53" # push %ebx - "\x31\xc0" # xor %eax,%eax - "\x0f\xa2" # cpuid - "\x8b\x44\x24\x08" # mov 0x8(%esp),%eax - "\x89\x18" # mov %ebx,0x0(%eax) - "\x89\x50\x04" # mov %edx,0x4(%eax) - "\x89\x48\x08" # mov %ecx,0x8(%eax) - "\x5b" # pop %ebx - "\xc3" # ret - ) - CPUID1_INSNS = ( - "\x53" # push %ebx - "\x31\xc0" # xor %eax,%eax - "\x40" # inc %eax - "\x0f\xa2" # cpuid - "\x5b" # pop %ebx - "\xc3" # ret - ) - else: - CPUID0_INSNS = ( - "\x49\x89\xd8" # mov %rbx,%r8 - "\x49\x89\xc9" # mov %rcx,%r9 - "\x48\x31\xc0" # xor %rax,%rax - "\x0f\xa2" # cpuid - "\x4c\x89\xc8" # mov %r9,%rax - "\x89\x18" # mov %ebx,0x0(%rax) - "\x89\x50\x04" # mov %edx,0x4(%rax) - "\x89\x48\x08" # mov %ecx,0x8(%rax) - "\x4c\x89\xc3" # mov %r8,%rbx - "\xc3" # retq - ) - CPUID1_INSNS = ( - "\x53" # push %rbx - "\x48\x31\xc0" # xor %rax,%rax - "\x48\xff\xc0" # inc %rax - "\x0f\xa2" # cpuid - "\x5b" # pop %rbx - "\xc3" # retq - ) - - def cpuid0(): - _cpuid0 = NativeFunction(None, [c_char_p], CPUID0_INSNS) - buf = create_string_buffer(12) - def cpuid0(): - _cpuid0(buf) - return buf.raw - return cpuid0 - cpuid0 = cpuid0() - - cpuid1 = NativeFunction(c_uint, [], CPUID1_INSNS) - - class DataBlob(Structure): - _fields_ = [('cbData', c_uint), - ('pbData', c_void_p)] - DataBlob_p = POINTER(DataBlob) - - def CryptUnprotectData(): - _CryptUnprotectData = crypt32.CryptUnprotectData - _CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p, - c_void_p, c_void_p, c_uint, DataBlob_p] - _CryptUnprotectData.restype = c_uint - def CryptUnprotectData(indata, entropy): - indatab = create_string_buffer(indata) - indata = DataBlob(len(indata), cast(indatab, c_void_p)) - entropyb = create_string_buffer(entropy) - entropy = DataBlob(len(entropy), cast(entropyb, c_void_p)) - outdata = DataBlob() - if not _CryptUnprotectData(byref(indata), None, byref(entropy), - None, None, 0, byref(outdata)): - raise ADEPTError("Failed to decrypt user key key (sic)") - return string_at(outdata.pbData, outdata.cbData) - return CryptUnprotectData - CryptUnprotectData = CryptUnprotectData() - - def retrieve_key(keypath): - if AES is None: - tkMessageBox.showerror( - "ADEPT Key", - "This script requires PyCrypto or OpenSSL which must be installed " - "separately. Read the top-of-script comment for details.") - return False - root = GetSystemDirectory().split('\\')[0] + '\\' - serial = GetVolumeSerialNumber(root) - vendor = cpuid0() - signature = struct.pack('>I', cpuid1())[1:] - user = GetUserName() - entropy = struct.pack('>I12s3s13s', serial, vendor, signature, user) - cuser = winreg.HKEY_CURRENT_USER - try: - regkey = winreg.OpenKey(cuser, DEVICE_KEY_PATH) - except WindowsError: - raise ADEPTError("Adobe Digital Editions not activated") - device = winreg.QueryValueEx(regkey, 'key')[0] - keykey = CryptUnprotectData(device, entropy) - userkey = None - try: - plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH) - except WindowsError: - raise ADEPTError("Could not locate ADE activation") - for i in xrange(0, 16): - try: - plkparent = winreg.OpenKey(plkroot, "%04d" % (i,)) - except WindowsError: - break - ktype = winreg.QueryValueEx(plkparent, None)[0] - if ktype != 'credentials': - continue - for j in xrange(0, 16): - try: - plkkey = winreg.OpenKey(plkparent, "%04d" % (j,)) - except WindowsError: - break - ktype = winreg.QueryValueEx(plkkey, None)[0] - if ktype != 'privateLicenseKey': - continue - userkey = winreg.QueryValueEx(plkkey, 'value')[0] - break - if userkey is not None: - break - if userkey is None: - raise ADEPTError('Could not locate privateLicenseKey') - userkey = userkey.decode('base64') - aes = AES(keykey) - userkey = aes.decrypt(userkey) - userkey = userkey[26:-ord(userkey[-1])] - with open(keypath, 'wb') as f: - f.write(userkey) - return True - -elif sys.platform.startswith('darwin'): - import xml.etree.ElementTree as etree - import Carbon.File - import Carbon.Folder - import Carbon.Folders - import MacOS - - ACTIVATION_PATH = 'Adobe/Digital Editions/activation.dat' - NSMAP = {'adept': 'http://ns.adobe.com/adept', - 'enc': 'http://www.w3.org/2001/04/xmlenc#'} - - def find_folder(domain, dtype): - try: - fsref = Carbon.Folder.FSFindFolder(domain, dtype, False) - return Carbon.File.pathname(fsref) - except MacOS.Error: - return None - - def find_app_support_file(subpath): - dtype = Carbon.Folders.kApplicationSupportFolderType - for domain in Carbon.Folders.kUserDomain, Carbon.Folders.kLocalDomain: - path = find_folder(domain, dtype) - if path is None: - continue - path = os.path.join(path, subpath) - if os.path.isfile(path): - return path - return None - - def retrieve_key(keypath): - actpath = find_app_support_file(ACTIVATION_PATH) - if actpath is None: - raise ADEPTError("Could not locate ADE activation") - tree = etree.parse(actpath) - adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) - expr = '//%s/%s' % (adept('credentials'), adept('privateLicenseKey')) - userkey = tree.findtext(expr) - userkey = userkey.decode('base64') - userkey = userkey[26:] - with open(keypath, 'wb') as f: - f.write(userkey) - return True - -elif sys.platform.startswith('cygwin'): - def retrieve_key(keypath): - tkMessageBox.showerror( - "ADEPT Key", - "This script requires a Windows-native Python, and cannot be run " - "under Cygwin. Please install a Windows-native Python and/or " - "check your file associations.") - return False - -else: - def retrieve_key(keypath): - tkMessageBox.showerror( - "ADEPT Key", - "This script only supports Windows and Mac OS X. For Linux " - "you should be able to run ADE and this script under Wine (with " - "an appropriate version of Windows Python installed).") - return False - -class ExceptionDialog(Tkinter.Frame): - def __init__(self, root, text): - Tkinter.Frame.__init__(self, root, border=5) - label = Tkinter.Label(self, text="Unexpected error:", - anchor=Tkconstants.W, justify=Tkconstants.LEFT) - label.pack(fill=Tkconstants.X, expand=0) - self.text = Tkinter.Text(self) - self.text.pack(fill=Tkconstants.BOTH, expand=1) - - self.text.insert(Tkconstants.END, text) - - -def extractKeyfile(keypath): - try: - success = retrieve_key(keypath) - except ADEPTError, e: - print "Key generation Error: " + str(e) - return 1 - except Exception, e: - print "General Error: " + str(e) - return 1 - if not success: - return 1 - return 0 - - -def cli_main(argv=sys.argv): - keypath = argv[1] - return extractKeyfile(keypath) - - -def main(argv=sys.argv): - root = Tkinter.Tk() - root.withdraw() - progname = os.path.basename(argv[0]) - keypath = 'adeptkey.der' - success = False - try: - success = retrieve_key(keypath) - except ADEPTError, e: - tkMessageBox.showerror("ADEPT Key", "Error: " + str(e)) - except Exception: - root.wm_state('normal') - root.title('ADEPT Key') - text = traceback.format_exc() - ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1) - root.mainloop() - if not success: - return 1 - tkMessageBox.showinfo( - "ADEPT Key", "Key successfully retrieved to %s" % (keypath)) - return 0 - -if __name__ == '__main__': - if len(sys.argv) > 1: - sys.exit(cli_main()) - sys.exit(main()) diff --git a/Other_Tools/Adobe_PDF_Tools/ineptpdf8.pyw b/Other_Tools/Adobe_PDF_Tools/ineptpdf8.pyw deleted file mode 100644 index 433f5cb..0000000 --- a/Other_Tools/Adobe_PDF_Tools/ineptpdf8.pyw +++ /dev/null @@ -1,3160 +0,0 @@ -#! /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()) \ No newline at end of file diff --git a/Other_Tools/Adobe_ePub_Tools/README_ineptepub.txt b/Other_Tools/Adobe_ePub_Tools/README_ineptepub.txt deleted file mode 100644 index 25813a4..0000000 --- a/Other_Tools/Adobe_ePub_Tools/README_ineptepub.txt +++ /dev/null @@ -1,18 +0,0 @@ -From Apprentice Alf's Blog - -Adobe Adept ePub, .epub - -This directory includes modified versions of the I♥CABBAGES Adobe Adept inept scripts for epubs. These scripts have been modified to work with OpenSSL on Windows as well as Linux and Mac OS X. His original scripts can be found in the clearly labelled folder. If a Windows User has OpenSSL installed, these scripts will make use of it in place of PyCrypto. - -The wonderful I♥CABBAGES has produced scripts that will remove the DRM from ePubs and PDFs encryped with Adobe’s DRM. These scripts require installation of the PyCrypto python package *or* the OpenSSL library on Windows. For Mac OS X and Linux boxes, these scripts use the already installed OpenSSL libcrypto so there is no additional requirements for these platforms. - -For more info, see the author's blog: -http://i-u2665-cabbages.blogspot.com/2009_02_01_archive.html - -There are two scripts: - -The first is called ineptkey_vX.X.pyw. Simply double-click to launch it and it will create a key file that is needed later to actually remove the DRM. This script need only be run once unless you change your ADE account information. - -The second is called in ineptepub_vX.X.pyw. Simply double-click to launch it. It will ask for your previously generated key file and the path to the book you want to remove the DRM from. - -Both of these scripts are gui python programs. Python 2.X (32 bit) is already installed in Mac OSX. We recommend ActiveState's Active Python Version 2.X (32 bit) for Windows users. diff --git a/Other_Tools/Adobe_ePub_Tools/ineptkey.pyw b/Other_Tools/Adobe_ePub_Tools/ineptkey.pyw deleted file mode 100644 index 723b7c6..0000000 --- a/Other_Tools/Adobe_ePub_Tools/ineptkey.pyw +++ /dev/null @@ -1,457 +0,0 @@ -#! /usr/bin/python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# ineptkey.pyw, version 5.6 -# Copyright © 2009-2010 i♥cabbages - -# Released under the terms of the GNU General Public Licence, version 3 or -# later. - -# Windows users: Before running this program, you must first install Python 2.6 -# from and PyCrypto from -# (make certain -# to install the version for Python 2.6). Then save this script file as -# ineptkey.pyw and double-click on it to run it. It will create a file named -# adeptkey.der in the same directory. This is your ADEPT user key. -# -# Mac OS X users: Save this script file as ineptkey.pyw. You can run this -# program from the command line (pythonw ineptkey.pyw) or by double-clicking -# it when it has been associated with PythonLauncher. It will create a file -# named adeptkey.der in the same directory. This is your ADEPT user key. - -# Revision history: -# 1 - Initial release, for Adobe Digital Editions 1.7 -# 2 - Better algorithm for finding pLK; improved error handling -# 3 - Rename to INEPT -# 4 - Series of changes by joblack (and others?) -- -# 4.1 - quick beta fix for ADE 1.7.2 (anon) -# 4.2 - added old 1.7.1 processing -# 4.3 - better key search -# 4.4 - Make it working on 64-bit Python -# 5 - Clean up and improve 4.x changes; -# Clean up and merge OS X support by unknown -# 5.1 - add support for using OpenSSL on Windows in place of PyCrypto -# 5.2 - added support for output of key to a particular file -# 5.3 - On Windows try PyCrypto first, OpenSSL next -# 5.4 - Modify interface to allow use of import -# 5.5 - Fix for potential problem with PyCrypto -# 5.6 - Revise to allow use in Plugins to eliminate need for duplicate code - -""" -Retrieve Adobe ADEPT user key. -""" - -__license__ = 'GPL v3' - -import sys -import os -import struct - -try: - from calibre.constants import iswindows, isosx -except: - iswindows = sys.platform.startswith('win') - isosx = sys.platform.startswith('darwin') - -class ADEPTError(Exception): - pass - -if iswindows: - from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \ - create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \ - string_at, Structure, c_void_p, cast, c_size_t, memmove, CDLL, c_int, \ - c_long, c_ulong - - from ctypes.wintypes import LPVOID, DWORD, BOOL - import _winreg as winreg - - def _load_crypto_libcrypto(): - from ctypes.util import find_library - libcrypto = find_library('libeay32') - if libcrypto is None: - raise ADEPTError('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_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): - self._blocksize = len(userkey) - if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : - raise ADEPTError('AES improper key used') - key = self._key = AES_KEY() - rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key) - if rv < 0: - raise ADEPTError('Failed to initialize AES key') - 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 ADEPTError('AES decryption failed') - return out.raw - 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 - for loader in (_load_crypto_pycrypto, _load_crypto_libcrypto): - try: - AES = loader() - break - except (ImportError, ADEPTError): - pass - return AES - - AES = _load_crypto() - - - DEVICE_KEY_PATH = r'Software\Adobe\Adept\Device' - PRIVATE_LICENCE_KEY_PATH = r'Software\Adobe\Adept\Activation' - - MAX_PATH = 255 - - kernel32 = windll.kernel32 - advapi32 = windll.advapi32 - crypt32 = windll.crypt32 - - def GetSystemDirectory(): - GetSystemDirectoryW = kernel32.GetSystemDirectoryW - GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint] - GetSystemDirectoryW.restype = c_uint - def GetSystemDirectory(): - buffer = create_unicode_buffer(MAX_PATH + 1) - GetSystemDirectoryW(buffer, len(buffer)) - return buffer.value - return GetSystemDirectory - GetSystemDirectory = GetSystemDirectory() - - def GetVolumeSerialNumber(): - GetVolumeInformationW = kernel32.GetVolumeInformationW - GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint, - POINTER(c_uint), POINTER(c_uint), - POINTER(c_uint), c_wchar_p, c_uint] - GetVolumeInformationW.restype = c_uint - def GetVolumeSerialNumber(path): - vsn = c_uint(0) - GetVolumeInformationW( - path, None, 0, byref(vsn), None, None, None, 0) - return vsn.value - return GetVolumeSerialNumber - GetVolumeSerialNumber = GetVolumeSerialNumber() - - def GetUserName(): - GetUserNameW = advapi32.GetUserNameW - GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)] - GetUserNameW.restype = c_uint - def GetUserName(): - buffer = create_unicode_buffer(32) - size = c_uint(len(buffer)) - while not GetUserNameW(buffer, byref(size)): - buffer = create_unicode_buffer(len(buffer) * 2) - size.value = len(buffer) - return buffer.value.encode('utf-16-le')[::2] - return GetUserName - GetUserName = GetUserName() - - PAGE_EXECUTE_READWRITE = 0x40 - MEM_COMMIT = 0x1000 - MEM_RESERVE = 0x2000 - - def VirtualAlloc(): - _VirtualAlloc = kernel32.VirtualAlloc - _VirtualAlloc.argtypes = [LPVOID, c_size_t, DWORD, DWORD] - _VirtualAlloc.restype = LPVOID - def VirtualAlloc(addr, size, alloctype=(MEM_COMMIT | MEM_RESERVE), - protect=PAGE_EXECUTE_READWRITE): - return _VirtualAlloc(addr, size, alloctype, protect) - return VirtualAlloc - VirtualAlloc = VirtualAlloc() - - MEM_RELEASE = 0x8000 - - def VirtualFree(): - _VirtualFree = kernel32.VirtualFree - _VirtualFree.argtypes = [LPVOID, c_size_t, DWORD] - _VirtualFree.restype = BOOL - def VirtualFree(addr, size=0, freetype=MEM_RELEASE): - return _VirtualFree(addr, size, freetype) - return VirtualFree - VirtualFree = VirtualFree() - - class NativeFunction(object): - def __init__(self, restype, argtypes, insns): - self._buf = buf = VirtualAlloc(None, len(insns)) - memmove(buf, insns, len(insns)) - ftype = CFUNCTYPE(restype, *argtypes) - self._native = ftype(buf) - - def __call__(self, *args): - return self._native(*args) - - def __del__(self): - if self._buf is not None: - VirtualFree(self._buf) - self._buf = None - - if struct.calcsize("P") == 4: - CPUID0_INSNS = ( - "\x53" # push %ebx - "\x31\xc0" # xor %eax,%eax - "\x0f\xa2" # cpuid - "\x8b\x44\x24\x08" # mov 0x8(%esp),%eax - "\x89\x18" # mov %ebx,0x0(%eax) - "\x89\x50\x04" # mov %edx,0x4(%eax) - "\x89\x48\x08" # mov %ecx,0x8(%eax) - "\x5b" # pop %ebx - "\xc3" # ret - ) - CPUID1_INSNS = ( - "\x53" # push %ebx - "\x31\xc0" # xor %eax,%eax - "\x40" # inc %eax - "\x0f\xa2" # cpuid - "\x5b" # pop %ebx - "\xc3" # ret - ) - else: - CPUID0_INSNS = ( - "\x49\x89\xd8" # mov %rbx,%r8 - "\x49\x89\xc9" # mov %rcx,%r9 - "\x48\x31\xc0" # xor %rax,%rax - "\x0f\xa2" # cpuid - "\x4c\x89\xc8" # mov %r9,%rax - "\x89\x18" # mov %ebx,0x0(%rax) - "\x89\x50\x04" # mov %edx,0x4(%rax) - "\x89\x48\x08" # mov %ecx,0x8(%rax) - "\x4c\x89\xc3" # mov %r8,%rbx - "\xc3" # retq - ) - CPUID1_INSNS = ( - "\x53" # push %rbx - "\x48\x31\xc0" # xor %rax,%rax - "\x48\xff\xc0" # inc %rax - "\x0f\xa2" # cpuid - "\x5b" # pop %rbx - "\xc3" # retq - ) - - def cpuid0(): - _cpuid0 = NativeFunction(None, [c_char_p], CPUID0_INSNS) - buf = create_string_buffer(12) - def cpuid0(): - _cpuid0(buf) - return buf.raw - return cpuid0 - cpuid0 = cpuid0() - - cpuid1 = NativeFunction(c_uint, [], CPUID1_INSNS) - - class DataBlob(Structure): - _fields_ = [('cbData', c_uint), - ('pbData', c_void_p)] - DataBlob_p = POINTER(DataBlob) - - def CryptUnprotectData(): - _CryptUnprotectData = crypt32.CryptUnprotectData - _CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p, - c_void_p, c_void_p, c_uint, DataBlob_p] - _CryptUnprotectData.restype = c_uint - def CryptUnprotectData(indata, entropy): - indatab = create_string_buffer(indata) - indata = DataBlob(len(indata), cast(indatab, c_void_p)) - entropyb = create_string_buffer(entropy) - entropy = DataBlob(len(entropy), cast(entropyb, c_void_p)) - outdata = DataBlob() - if not _CryptUnprotectData(byref(indata), None, byref(entropy), - None, None, 0, byref(outdata)): - raise ADEPTError("Failed to decrypt user key key (sic)") - return string_at(outdata.pbData, outdata.cbData) - return CryptUnprotectData - CryptUnprotectData = CryptUnprotectData() - - def retrieve_keys(): - if AES is None: - raise ADEPTError("PyCrypto or OpenSSL must be installed") - root = GetSystemDirectory().split('\\')[0] + '\\' - serial = GetVolumeSerialNumber(root) - vendor = cpuid0() - signature = struct.pack('>I', cpuid1())[1:] - user = GetUserName() - entropy = struct.pack('>I12s3s13s', serial, vendor, signature, user) - cuser = winreg.HKEY_CURRENT_USER - try: - regkey = winreg.OpenKey(cuser, DEVICE_KEY_PATH) - except WindowsError: - raise ADEPTError("Adobe Digital Editions not activated") - device = winreg.QueryValueEx(regkey, 'key')[0] - keykey = CryptUnprotectData(device, entropy) - userkey = None - keys = [] - try: - plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH) - except WindowsError: - raise ADEPTError("Could not locate ADE activation") - for i in xrange(0, 16): - try: - plkparent = winreg.OpenKey(plkroot, "%04d" % (i,)) - except WindowsError: - break - ktype = winreg.QueryValueEx(plkparent, None)[0] - if ktype != 'credentials': - continue - for j in xrange(0, 16): - try: - plkkey = winreg.OpenKey(plkparent, "%04d" % (j,)) - except WindowsError: - break - ktype = winreg.QueryValueEx(plkkey, None)[0] - if ktype != 'privateLicenseKey': - continue - userkey = winreg.QueryValueEx(plkkey, 'value')[0] - userkey = userkey.decode('base64') - aes = AES(keykey) - userkey = aes.decrypt(userkey) - userkey = userkey[26:-ord(userkey[-1])] - keys.append(userkey) - if len(keys) == 0: - raise ADEPTError('Could not locate privateLicenseKey') - return keys - - -elif isosx: - import xml.etree.ElementTree as etree - import subprocess - - NSMAP = {'adept': 'http://ns.adobe.com/adept', - 'enc': 'http://www.w3.org/2001/04/xmlenc#'} - - def findActivationDat(): - home = os.getenv('HOME') - cmdline = 'find "' + home + '/Library/Application Support/Adobe/Digital Editions" -name "activation.dat"' - cmdline = cmdline.encode(sys.getfilesystemencoding()) - p2 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) - out1, out2 = p2.communicate() - reslst = out1.split('\n') - cnt = len(reslst) - for j in xrange(cnt): - resline = reslst[j] - pp = resline.find('activation.dat') - if pp >= 0: - ActDatPath = resline - break - if os.path.exists(ActDatPath): - return ActDatPath - return None - - def retrieve_keys(): - actpath = findActivationDat() - if actpath is None: - raise ADEPTError("Could not locate ADE activation") - tree = etree.parse(actpath) - adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) - expr = '//%s/%s' % (adept('credentials'), adept('privateLicenseKey')) - userkey = tree.findtext(expr) - userkey = userkey.decode('base64') - userkey = userkey[26:] - return [userkey] - -else: - def retrieve_keys(keypath): - raise ADEPTError("This script only supports Windows and Mac OS X.") - return [] - -def retrieve_key(keypath): - keys = retrieve_keys() - with open(keypath, 'wb') as f: - f.write(keys[0]) - return True - -def extractKeyfile(keypath): - try: - success = retrieve_key(keypath) - except ADEPTError, e: - print "Key generation Error: " + str(e) - return 1 - except Exception, e: - print "General Error: " + str(e) - return 1 - if not success: - return 1 - return 0 - - -def cli_main(argv=sys.argv): - keypath = argv[1] - return extractKeyfile(keypath) - - -def main(argv=sys.argv): - import Tkinter - import Tkconstants - import tkMessageBox - import traceback - - class ExceptionDialog(Tkinter.Frame): - def __init__(self, root, text): - Tkinter.Frame.__init__(self, root, border=5) - label = Tkinter.Label(self, text="Unexpected error:", - anchor=Tkconstants.W, justify=Tkconstants.LEFT) - label.pack(fill=Tkconstants.X, expand=0) - self.text = Tkinter.Text(self) - self.text.pack(fill=Tkconstants.BOTH, expand=1) - - self.text.insert(Tkconstants.END, text) - - - root = Tkinter.Tk() - root.withdraw() - progname = os.path.basename(argv[0]) - keypath = os.path.abspath("adeptkey.der") - success = False - try: - success = retrieve_key(keypath) - except ADEPTError, e: - tkMessageBox.showerror("ADEPT Key", "Error: " + str(e)) - except Exception: - root.wm_state('normal') - root.title('ADEPT Key') - text = traceback.format_exc() - ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1) - root.mainloop() - if not success: - return 1 - tkMessageBox.showinfo( - "ADEPT Key", "Key successfully retrieved to %s" % (keypath)) - return 0 - -if __name__ == '__main__': - if len(sys.argv) > 1: - sys.exit(cli_main()) - sys.exit(main()) 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 e0471f7..6c41ed5 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 @@ -1,22 +1,32 @@ +INTRODUCTION +============ + To obtain unencrypted content from the B&N, you have to download it directly from the website. Unrooted Nook devices will not let you save your content to your PC. If the downloaded file is encrypted, install and configure the ignoble plugin in Calibre to decrypt that. -Some content is not downloadable from the website, for instance magazines. The Greasemonkey script included in the tools modifies the myNook page of the Barnes and Noble website to show a download button for non-downloadable content. This will work until Barnes & Noble changes their website. + +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. Prerequisites -1) Firefox: http://getfirefox.com +------------- +1) Firefox: http://www.getfirefox.com 2) Greasemokey extension: https://addons.mozilla.org/nl/firefox/addon/greasemonkey/ One time installation -1) Install Firefox if not already done so; +--------------------- +1) Install Firefox if not already done so 2) Follow the above link to GreaseMonkey and click Add to Firefox 3) Restart Firefox -4) Go to (link to the script, best hosted somewhere, as .js usually opens in an editor) -5) A pop up should appear, stating you are about to install a GreaseMonkey user script. +4) Go to http://userscripts.org/scripts/source/152985.user.js +5) A popup should appear, stating you are about to install a GreaseMonkey user script. 6) Click on install Use +--- 1) Log in into your B&N account 2) Go to MyNook -3) An “Alternative download” should apppear next to normally non-downloadable content. +3) An “Alternative download” should appear next to normally non-downloadable content. Note that this will not work for content such as Nook applications, and some children books. diff --git a/Other_Tools/Barnes_and_Noble_EPUB_Tools/README_ignoble_epub.txt b/Other_Tools/Barnes_and_Noble_EPUB_Tools/README_ignoble_epub.txt deleted file mode 100644 index 0a67cf2..0000000 --- a/Other_Tools/Barnes_and_Noble_EPUB_Tools/README_ignoble_epub.txt +++ /dev/null @@ -1,24 +0,0 @@ -Readme.txt - -Barnes and Noble EPUB ebooks use a form of Social DRM which requires information on your Credit Card Number and the Name on the Credit card used to purchase the book to actually unencrypt the book. - -For more info, see the author's blog: -http://i-u2665-cabbages.blogspot.com/2009_12_01_archive.html - -The original scripts by IHeartCabbages are available here as well. These scripts have been modified to allow the use of OpenSSL in place of PyCrypto to make them easier to run on Linux and Mac OS X, as well as to fix some minor bugs. - -There are 2 scripts: - -The first is ignoblekeygen_v2.4.pyw. Double-click to launch it and provide the required information, and this program will generate a key file needed to remove the DRM from the books. The require information is - -* 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. -* Credit Card number: This is the credit card number that was on file with Barnes & Noble at the time of download of the ebooks. - -This key file need only be generated once unless either you change the default credit card number or your name on your B&N account. - -The second is ignobleepub_vX.X.pyw. Double-click it and it will ask for your key file and the path to the book to remove the DRM from. - -All of these scripts are gui python programs. Python 2.X (32 bit) is already installed in Mac OSX. We recommend ActiveState's Active Python Version 2.X (32 bit) for Windows users. - -These scripts are based on the IHeartCabbages original scripts that allow the replacement of the requirement for PyCrypto with OpenSSL's libcrypto which is already installed on all Mac OS X machines and Linux Boxes. Window's Users will still have to install PyCrypto or OpenSSL to get these scripts to work properly. - diff --git a/Other_Tools/Barnes_and_Noble_EPUB_Tools/ignobleepub.pyw b/Other_Tools/Barnes_and_Noble_EPUB_Tools/ignobleepub.pyw deleted file mode 100644 index 6b1a1d2..0000000 --- a/Other_Tools/Barnes_and_Noble_EPUB_Tools/ignobleepub.pyw +++ /dev/null @@ -1,336 +0,0 @@ -#! /usr/bin/python - -from __future__ import with_statement - -# ignobleepub.pyw, version 3.5 - -# To run this program install Python 2.6 from -# and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto -# (make sure to install the version for Python 2.6). Save this script file as -# ignobleepub.pyw and double-click on it to run it. - -# 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 - -__license__ = 'GPL v3' - -import sys -import os -import zlib -import zipfile -from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED -from contextlib import closing -import xml.etree.ElementTree as etree -import Tkinter -import Tkconstants -import tkFileDialog -import tkMessageBox - -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 sys.platform.startswith('win'): - libcrypto = find_library('libeay32') - else: - libcrypto = find_library('crypto') - if libcrypto is None: - raise IGNOBLEError('libcrypto not found') - libcrypto = CDLL(libcrypto) - - AES_MAXNR = 14 - - c_char_pp = POINTER(c_char_p) - c_int_p = POINTER(c_int) - - class AES_KEY(Structure): - _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), - ('rounds', c_int)] - AES_KEY_p = POINTER(AES_KEY) - - def F(restype, name, argtypes): - func = getattr(libcrypto, name) - func.restype = restype - func.argtypes = argtypes - return func - - AES_cbc_encrypt = F(None, 'AES_cbc_encrypt', - [c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p, - c_int]) - AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key', - [c_char_p, c_int, AES_KEY_p]) - 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): - 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') - - 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 - - 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() - - - -""" -Decrypt Barnes & Noble ADEPT encrypted EPUB books. -""" - - -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.new(bookkey, AES.MODE_CBC, '\x00'*16) - 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) - path = path.encode('utf-8') - if path is not None: - 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 - - -class DecryptionDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - self.status = Tkinter.Label(self, text='Select files for decryption') - self.status.pack(fill=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='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('bnepubkey.b64'): - self.keypath.insert(0, 'bnepubkey.b64') - button = Tkinter.Button(body, text="...", command=self.get_keypath) - button.grid(row=0, column=2) - Tkinter.Label(body, text='Input file').grid(row=1) - self.inpath = Tkinter.Entry(body, width=30) - self.inpath.grid(row=1, column=1, sticky=sticky) - button = Tkinter.Button(body, text="...", command=self.get_inpath) - button.grid(row=1, column=2) - Tkinter.Label(body, text='Output file').grid(row=2) - self.outpath = Tkinter.Entry(body, width=30) - self.outpath.grid(row=2, column=1, sticky=sticky) - button = Tkinter.Button(body, text="...", command=self.get_outpath) - button.grid(row=2, column=2) - buttons = Tkinter.Frame(self) - buttons.pack() - botton = Tkinter.Button( - buttons, text="Decrypt", width=10, command=self.decrypt) - botton.pack(side=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 B&N EPUB key file', - defaultextension='.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='Select B&N-encrypted EPUB file to decrypt', - defaultextension='.epub', filetypes=[('EPUB files', '.epub'), - ('All files', '.*')]) - 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='Select unencrypted EPUB file to produce', - defaultextension='.epub', filetypes=[('EPUB files', '.epub'), - ('All files', '.*')]) - 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'] = 'Specified key file does not exist' - return - if not inpath or not os.path.exists(inpath): - self.status['text'] = 'Specified input file does not exist' - return - if not outpath: - self.status['text'] = 'Output file not specified' - return - if inpath == outpath: - self.status['text'] = 'Must have different input and output files' - return - argv = [sys.argv[0], keypath, inpath, outpath] - self.status['text'] = 'Decrypting...' - try: - cli_main(argv) - except Exception, e: - self.status['text'] = 'Error: ' + str(e) - return - self.status['text'] = 'File successfully decrypted' - - -def decryptBook(keypath, inpath, outpath): - with open(keypath, 'rb') as f: - keyb64 = f.read() - key = keyb64.decode('base64')[:16] - # aes = AES.new(key, AES.MODE_CBC, '\x00'*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: - raise IGNOBLEError('%s: not an B&N ADEPT EPUB' % (inpath,)) - for name in META_NAMES: - namelist.remove(name) - 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)) - 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)) - return 0 - - -def cli_main(argv=sys.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 "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,) - return 1 - keypath, inpath, outpath = argv[1:] - return decryptBook(keypath, inpath, outpath) - - -def gui_main(): - root = Tkinter.Tk() - if AES is None: - root.withdraw() - tkMessageBox.showerror( - "Ignoble EPUB Decrypter", - "This script requires OpenSSL or PyCrypto, which must be installed " - "separately. Read the top-of-script comment for details.") - return 1 - root.title('Ignoble EPUB Decrypter') - 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.exit(cli_main()) - sys.exit(gui_main()) diff --git a/Other_Tools/Barnes_and_Noble_EPUB_Tools/ignoblekey.pyw b/Other_Tools/Barnes_and_Noble_EPUB_Tools/ignoblekey.pyw deleted file mode 100644 index 6f0798a..0000000 --- a/Other_Tools/Barnes_and_Noble_EPUB_Tools/ignoblekey.pyw +++ /dev/null @@ -1,112 +0,0 @@ -#! /usr/bin/python - -# ignoblekey.pyw, version 2 - -# To run this program install Python 2.6 from -# Save this script file as ignoblekey.pyw and double-click on it to run it. - -# Revision history: -# 1 - Initial release -# 2 - Add some missing code - -""" -Retrieve B&N DesktopReader EPUB user AES key. -""" - -from __future__ import with_statement - -__license__ = 'GPL v3' - -import sys -import os -import binascii -import glob -import Tkinter -import Tkconstants -import tkMessageBox -import traceback - -BN_KEY_KEY = 'uhk00000000' -BN_APPDATA_DIR = r'Barnes & Noble\DesktopReader' - -class IgnobleError(Exception): - pass - -def retrieve_key(inpath, outpath): - # The B&N DesktopReader 'ClientAPI' file is just a sqlite3 DB. Requiring - # users to install sqlite3 and bindings seems like overkill for retrieving - # one value, so we go in hot and dirty. - with open(inpath, 'rb') as f: - data = f.read() - if BN_KEY_KEY not in data: - raise IgnobleError('B&N user key not found; unexpected DB format?') - index = data.rindex(BN_KEY_KEY) + len(BN_KEY_KEY) + 1 - data = data[index:index + 40] - for i in xrange(20, len(data)): - try: - keyb64 = data[:i] - if len(keyb64.decode('base64')) == 20: - break - except binascii.Error: - pass - else: - raise IgnobleError('Problem decoding key; unexpected DB format?') - with open(outpath, 'wb') as f: - f.write(keyb64 + '\n') - -def cli_main(argv=sys.argv): - progname = os.path.basename(argv[0]) - args = argv[1:] - if len(args) != 2: - sys.stderr.write("USAGE: %s CLIENTDB KEYFILE" % (progname,)) - return 1 - inpath, outpath = args - retrieve_key(inpath, outpath) - return 0 - -def find_bnclientdb_path(): - appdata = os.environ['APPDATA'] - bndir = os.path.join(appdata, BN_APPDATA_DIR) - if not os.path.isdir(bndir): - raise IgnobleError('Could not locate B&N Reader installation') - dbpath = glob.glob(os.path.join(bndir, 'ClientAPI_*.db')) - if len(dbpath) == 0: - raise IgnobleError('Problem locating B&N Reader DB') - return sorted(dbpath)[-1] - -class ExceptionDialog(Tkinter.Frame): - def __init__(self, root, text): - Tkinter.Frame.__init__(self, root, border=5) - label = Tkinter.Label(self, text="Unexpected error:", - anchor=Tkconstants.W, justify=Tkconstants.LEFT) - label.pack(fill=Tkconstants.X, expand=0) - self.text = Tkinter.Text(self) - self.text.pack(fill=Tkconstants.BOTH, expand=1) - self.text.insert(Tkconstants.END, text) - -def gui_main(argv=sys.argv): - root = Tkinter.Tk() - root.withdraw() - progname = os.path.basename(argv[0]) - keypath = 'bnepubkey.b64' - try: - dbpath = find_bnclientdb_path() - retrieve_key(dbpath, keypath) - except IgnobleError, e: - tkMessageBox.showerror("Ignoble Key", "Error: " + str(e)) - return 1 - except Exception: - root.wm_state('normal') - root.title('Ignoble Key') - text = traceback.format_exc() - ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1) - root.mainloop() - return 1 - tkMessageBox.showinfo( - "Ignoble Key", "Key successfully retrieved to %s" % (keypath)) - return 0 - -if __name__ == '__main__': - if len(sys.argv) > 1: - sys.exit(cli_main()) - sys.exit(gui_main()) diff --git a/Other_Tools/KindleBooks/KindleBooks.pyw b/Other_Tools/KindleBooks/KindleBooks.pyw deleted file mode 100644 index 0f8021f..0000000 --- a/Other_Tools/KindleBooks/KindleBooks.pyw +++ /dev/null @@ -1,261 +0,0 @@ -#!/usr/bin/env python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab - -import sys -sys.path.append('lib') -import os, os.path, urllib -os.environ['PYTHONIOENCODING'] = "utf-8" - -import Tkinter -import Tkconstants -import tkFileDialog -import tkMessageBox -from scrolltextwidget import ScrolledText -import subprocess -from subprocess import Popen, PIPE, STDOUT -import subasyncio -from subasyncio import Process - -class MainDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - self.root = root - self.interval = 1000 - self.p2 = None - self.status = Tkinter.Label(self, text='Remove Encryption from a Kindle/Mobi/Topaz eBook') - 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='Kindle/Mobi/Topaz eBook input file').grid(row=0, sticky=Tkconstants.E) - self.mobipath = Tkinter.Entry(body, width=50) - self.mobipath.grid(row=0, column=1, sticky=sticky) - cwd = os.getcwdu() - cwd = cwd.encode('utf-8') - self.mobipath.insert(0, cwd) - button = Tkinter.Button(body, text="...", command=self.get_mobipath) - button.grid(row=0, column=2) - - Tkinter.Label(body, text='Directory for the Unencrypted Output File(s)').grid(row=1, sticky=Tkconstants.E) - self.outpath = Tkinter.Entry(body, width=50) - self.outpath.grid(row=1, column=1, sticky=sticky) - cwd = os.getcwdu() - cwd = cwd.encode('utf-8') - outname = cwd - self.outpath.insert(0, outname) - button = Tkinter.Button(body, text="...", command=self.get_outpath) - button.grid(row=1, column=2) - - Tkinter.Label(body, text='Optional Alternative Kindle.info file').grid(row=2, sticky=Tkconstants.E) - self.altinfopath = Tkinter.Entry(body, width=50) - self.altinfopath.grid(row=2, column=1, sticky=sticky) - #cwd = os.getcwdu() - #cwd = cwd.encode('utf-8') - #self.altinfopath.insert(0, cwd) - button = Tkinter.Button(body, text="...", command=self.get_altinfopath) - button.grid(row=2, column=2) - - Tkinter.Label(body, text='Optional Comma Separated List of 10 Character PIDs (no spaces)').grid(row=3, sticky=Tkconstants.E) - self.pidnums = Tkinter.StringVar() - self.pidinfo = Tkinter.Entry(body, width=50, textvariable=self.pidnums) - self.pidinfo.grid(row=3, column=1, sticky=sticky) - - Tkinter.Label(body, text='Optional Comma Separated List of 16 Character Kindle Serial Numbers (no spaces)').grid(row=4, sticky=Tkconstants.E) - self.sernums = Tkinter.StringVar() - self.serinfo = Tkinter.Entry(body, width=50, textvariable=self.sernums) - self.serinfo.grid(row=4, column=1, sticky=sticky) - - - msg1 = 'Conversion Log \n\n' - self.stext = ScrolledText(body, bd=5, relief=Tkconstants.RIDGE, height=15, width=60, wrap=Tkconstants.WORD) - self.stext.grid(row=6, column=0, columnspan=2,sticky=sticky) - self.stext.insert(Tkconstants.END,msg1) - - buttons = Tkinter.Frame(self) - buttons.pack() - self.sbotton = Tkinter.Button( - buttons, text="Start", width=10, command=self.convertit) - self.sbotton.pack(side=Tkconstants.LEFT) - - Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) - self.qbutton = Tkinter.Button( - buttons, text="Quit", width=10, command=self.quitting) - self.qbutton.pack(side=Tkconstants.RIGHT) - - # 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): - poll = self.p2.wait('nowait') - if poll != None: - text = self.p2.readerr() - text += self.p2.read() - msg = text + '\n\n' + 'Encryption successfully removed\n' - if poll == 1: - msg = text + '\n\n' + 'Error: Encryption Removal Failed\n' - if poll == 2: - msg = text + '\n\n' + 'Input File was Not Encrypted - No Output File Needed\n' - self.showCmdOutput(msg) - self.p2 = None - self.sbotton.configure(state='normal') - return - text = self.p2.readerr() - text += self.p2.read() - self.showCmdOutput(text) - # make sure we get invoked again by event loop after interval - self.stext.after(self.interval,self.processPipe) - return - - # post output from subprocess in scrolled text widget - def showCmdOutput(self, msg): - if msg and msg !='': - if sys.platform.startswith('win'): - msg = msg.replace('\r\n','\n') - self.stext.insert(Tkconstants.END,msg) - self.stext.yview_pickplace(Tkconstants.END) - return - - # run as a subprocess via pipes and collect stdout - def mobirdr(self, infile, outfile, altinfopath, pidnums, sernums): - # os.putenv('PYTHONUNBUFFERED', '1') - tool = 'k4mobidedrm.py' - pidoption = '' - if pidnums and pidnums != '': - pidoption = ' -p "' + pidnums + '" ' - seroption = '' - if sernums and sernums != '': - seroption = ' -s "' + sernums + '" ' - infooption = '' - if altinfopath and altinfopath != '': - infooption = ' -k "' + altinfopath + '" ' - pengine = sys.executable - if pengine is None or pengine == '': - pengine = "python" - pengine = os.path.normpath(pengine) - cmdline = pengine + ' ./lib/' + tool + ' ' + pidoption + seroption + infooption + '"' + infile + '" "' + outfile + '"' - if sys.platform.startswith('win'): - cmdline = pengine + ' lib\\' + tool + ' ' + pidoption + seroption + infooption + '"' + infile + '" "' + outfile + '"' - print cmdline - cmdline = cmdline.encode(sys.getfilesystemencoding()) - p2 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False) - return p2 - - - def get_mobipath(self): - cpath = self.mobipath.get() - mobipath = tkFileDialog.askopenfilename( - initialdir = cpath, - parent=None, title='Select Kindle/Mobi/Topaz eBook File', - defaultextension='.prc', filetypes=[('Mobi eBook File', '.prc'), ('Mobi eBook File', '.azw'),('Mobi eBook File', '.mobi'),('Mobi eBook File', '.tpz'),('Mobi eBook File', '.azw1'),('Mobi azw4 eBook File', '.azw4'),('All Files', '.*')]) - if mobipath: - mobipath = os.path.normpath(mobipath) - self.mobipath.delete(0, Tkconstants.END) - self.mobipath.insert(0, mobipath) - return - - def get_outpath(self): - cwd = os.getcwdu() - cwd = cwd.encode('utf-8') - outpath = tkFileDialog.askdirectory( - parent=None, title='Directory to Store Unencrypted file(s) into', - initialdir=cwd, initialfile=None) - if outpath: - outpath = os.path.normpath(outpath) - self.outpath.delete(0, Tkconstants.END) - self.outpath.insert(0, outpath) - return - - def get_altinfopath(self): - cwd = os.getcwdu() - cwd = cwd.encode('utf-8') - altinfopath = tkFileDialog.askopenfilename( - parent=None, title='Select Alternative kindle.info File', - defaultextension='.prc', filetypes=[('Kindle Info', '.info'), - ('All Files', '.*')], - initialdir=cwd) - if altinfopath: - altinfopath = os.path.normpath(altinfopath) - self.altinfopath.delete(0, Tkconstants.END) - self.altinfopath.insert(0, altinfopath) - return - - def quitting(self): - # kill any still running subprocess - if self.p2 != None: - if (self.p2.wait('nowait') == None): - self.p2.terminate() - self.root.destroy() - - # actually ready to run the subprocess and get its output - def convertit(self): - self.status['text'] = '' - # now disable the button to prevent multiple launches - self.sbotton.configure(state='disabled') - mobipath = self.mobipath.get() - outpath = self.outpath.get() - altinfopath = self.altinfopath.get() - pidnums = self.pidinfo.get() - sernums = self.serinfo.get() - - if not mobipath or not os.path.exists(mobipath) or not os.path.isfile(mobipath): - self.status['text'] = 'Specified Kindle Mobi eBook file does not exist' - self.sbotton.configure(state='normal') - return - - tpz = False - # Identify any Topaz Files - f = file(mobipath, 'rb') - raw = f.read(3) - if raw.startswith('TPZ'): - tpz = True - f.close() - if not outpath: - self.status['text'] = 'No output directory specified' - self.sbotton.configure(state='normal') - return - if not os.path.isdir(outpath): - self.status['text'] = 'Error specified output directory does not exist' - self.sbotton.configure(state='normal') - return - if altinfopath and not os.path.exists(altinfopath): - self.status['text'] = 'Specified kindle.info file does not exist' - self.sbotton.configure(state='normal') - return - - log = 'Command = "python k4mobidedrm.py"\n' - if not tpz: - log += 'Kindle/Mobi Path = "'+ mobipath + '"\n' - else: - log += 'Topaz Path = "'+ mobipath + '"\n' - log += 'Output Directory = "' + outpath + '"\n' - log += 'Kindle.info file = "' + altinfopath + '"\n' - log += 'PID list = "' + pidnums + '"\n' - log += 'Serial Number list = "' + sernums + '"\n' - log += '\n\n' - log += 'Please Wait ...\n\n' - log = log.encode('utf-8') - self.stext.insert(Tkconstants.END,log) - self.p2 = self.mobirdr(mobipath, outpath, altinfopath, pidnums, sernums) - - # python does not seem to allow you to create - # your own eventloop which every other gui does - strange - # so need to use the widget "after" command to force - # event loop to run non-gui events every interval - self.stext.after(self.interval,self.processPipe) - return - - -def main(argv=None): - root = Tkinter.Tk() - root.title('Kindle/Mobi/Topaz eBook Encryption Removal') - root.resizable(True, False) - root.minsize(300, 0) - MainDialog(root).pack(fill=Tkconstants.X, expand=1) - root.mainloop() - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/Other_Tools/KindleBooks/README_KindleBooks.txt b/Other_Tools/KindleBooks/README_KindleBooks.txt deleted file mode 100644 index 59fe31d..0000000 --- a/Other_Tools/KindleBooks/README_KindleBooks.txt +++ /dev/null @@ -1,58 +0,0 @@ -KindleBooks (Originally called K4MobiDeDRM and Topaz_Tools) - -Most users will be better off using the DeDRM applications or the calibre plugin. This script is provided more for historical interest than anything else. - - -This tools combines functionality of MobiDeDRM with that of K4PCDeDRM, K4MDeDRM, and K4DeDRM. Effectively, it provides one-stop shopping for all your Mobipocket, Kindle for iPhone/iPad/iPodTouch, Kindle for PC, and Kindle for Mac needs and should work for both Mobi and Topaz ebooks. - -Preliminary Steps: - -1. Make sure you have Python 2.5, 2.6 or 2.7 installed (32 bit) and properly set as part of your SYSTEM PATH environment variable (On Windows I recommend ActiveState's ActivePython. See their web pages for instructions on how to install and how to properly set your PATH). On Mac OSX 10.5 and later everything you need is already installed. - - -Instructions: - -1. double-click on KindleBooks.pyw - -2. In the window that opens: -hit the first '...' button to locate your DRM Kindle-style ebook - -3. Then hit the second '...' button to select an output directory for the unlocked file - -4. If you have multiple Kindle.Info files and would like to use one specific one, please hit the third "...' button to select it. Note, if you only have one Kindle.Info file (like most users) this can and should be left blank. - -5. Then add in any PIDs you need from KindleV1, Kindle for iPhone/iPad/iPodTouch, or other single PID devices to the provided box as a comma separated list of 10 digit PID numbers. If this is a Kindle for Mac or a Kindle for PC book then you can leave this box blank - - -6. If you have standalone Kindles, add in any 16 digit Serial Numbers as a comma separated list. If this is a Kindle for Mac or a Kindle for PC book then you can leave this box blank - -7. hit the 'Start' button - -After a short delay, you should see progress in the Conversion Log window indicating is the unlocking was a success or failure. - - - -If your book was a normal Mobi style ebook: - If successful, you should see a "_nodrm" named version Mobi ebook. - If not please examine the Conversion Log window for any errors. - - - -If your book was actually a Topaz book: - -Please note that Topaz is most similar to a poor man's image only PDF in style. It has glyphs and x,y positions, ocrText used just for searching, that describe the image each page all encoded into a binary xml-like set of files. - -If successful, you will have 3 zip archives created. - -1. The first is BOOKNAME_nodrm.zip. - You can import this into calibre as is or unzip it and edit the book.html file you find inside. To create the book.html, Amazon's ocrText is combined with other information to recreate as closely as possible what the original book looked like. Unfortunately most bolding, italics is lost. Also, Amazon's ocrText can be absolutely horrible at times. Much work will be needed to clean up and correct Topaz books. - -2. The second is BOOKNAME_SVG.zip - You can also import this into calibre or unzip it and open the indexsvg.xhtml file in any good Browser (Safari, Firefox, etc). This zip contains a set of svg images (one for each pages is created) and it shows the page exactly how it appeared. This zip can be used to create an image only pdf file via post conversion. - -3. The third is BOOKNAME_XML.zip - This is a zip archive of the decrypted and translated xml-like descriptions of each page and can be archived/saved in case later code can do a better job converting these files. These are exactly what a Topaz books guts are. You should take a look at them in any text editor to see what they look like. - -If the Topaz book conversion is not successful, a large _DEBUG.zip archive of all of the pieces is created and this can examined along with the Conversion Log window contents to determine the cause of the error and hopefully get it fixed in the next release. - - diff --git a/Other_Tools/KindleBooks/lib/aescbc.py b/Other_Tools/KindleBooks/lib/aescbc.py deleted file mode 100644 index 5667511..0000000 --- a/Other_Tools/KindleBooks/lib/aescbc.py +++ /dev/null @@ -1,568 +0,0 @@ -#! /usr/bin/env python - -""" - Routines for doing AES CBC in one file - - Modified by some_updates to extract - and combine only those parts needed for AES CBC - into one simple to add python file - - Original Version - Copyright (c) 2002 by Paul A. Lambert - Under: - CryptoPy Artisitic License Version 1.0 - See the wonderful pure python package cryptopy-1.2.5 - and read its LICENSE.txt for complete license details. -""" - -class CryptoError(Exception): - """ Base class for crypto exceptions """ - def __init__(self,errorMessage='Error!'): - self.message = errorMessage - def __str__(self): - return self.message - -class InitCryptoError(CryptoError): - """ Crypto errors during algorithm initialization """ -class BadKeySizeError(InitCryptoError): - """ Bad key size error """ -class EncryptError(CryptoError): - """ Error in encryption processing """ -class DecryptError(CryptoError): - """ Error in decryption processing """ -class DecryptNotBlockAlignedError(DecryptError): - """ Error in decryption processing """ - -def xorS(a,b): - """ XOR two strings """ - assert len(a)==len(b) - x = [] - for i in range(len(a)): - x.append( chr(ord(a[i])^ord(b[i]))) - return ''.join(x) - -def xor(a,b): - """ XOR two strings """ - x = [] - for i in range(min(len(a),len(b))): - x.append( chr(ord(a[i])^ord(b[i]))) - return ''.join(x) - -""" - Base 'BlockCipher' and Pad classes for cipher instances. - BlockCipher supports automatic padding and type conversion. The BlockCipher - class was written to make the actual algorithm code more readable and - not for performance. -""" - -class BlockCipher: - """ Block ciphers """ - def __init__(self): - self.reset() - - def reset(self): - self.resetEncrypt() - self.resetDecrypt() - def resetEncrypt(self): - self.encryptBlockCount = 0 - self.bytesToEncrypt = '' - def resetDecrypt(self): - self.decryptBlockCount = 0 - self.bytesToDecrypt = '' - - def encrypt(self, plainText, more = None): - """ Encrypt a string and return a binary string """ - self.bytesToEncrypt += plainText # append plainText to any bytes from prior encrypt - numBlocks, numExtraBytes = divmod(len(self.bytesToEncrypt), self.blockSize) - cipherText = '' - for i in range(numBlocks): - bStart = i*self.blockSize - ctBlock = self.encryptBlock(self.bytesToEncrypt[bStart:bStart+self.blockSize]) - self.encryptBlockCount += 1 - cipherText += ctBlock - if numExtraBytes > 0: # save any bytes that are not block aligned - self.bytesToEncrypt = self.bytesToEncrypt[-numExtraBytes:] - else: - self.bytesToEncrypt = '' - - if more == None: # no more data expected from caller - finalBytes = self.padding.addPad(self.bytesToEncrypt,self.blockSize) - if len(finalBytes) > 0: - ctBlock = self.encryptBlock(finalBytes) - self.encryptBlockCount += 1 - cipherText += ctBlock - self.resetEncrypt() - return cipherText - - def decrypt(self, cipherText, more = None): - """ Decrypt a string and return a string """ - self.bytesToDecrypt += cipherText # append to any bytes from prior decrypt - - numBlocks, numExtraBytes = divmod(len(self.bytesToDecrypt), self.blockSize) - if more == None: # no more calls to decrypt, should have all the data - if numExtraBytes != 0: - raise DecryptNotBlockAlignedError, 'Data not block aligned on decrypt' - - # hold back some bytes in case last decrypt has zero len - if (more != None) and (numExtraBytes == 0) and (numBlocks >0) : - numBlocks -= 1 - numExtraBytes = self.blockSize - - plainText = '' - for i in range(numBlocks): - bStart = i*self.blockSize - ptBlock = self.decryptBlock(self.bytesToDecrypt[bStart : bStart+self.blockSize]) - self.decryptBlockCount += 1 - plainText += ptBlock - - if numExtraBytes > 0: # save any bytes that are not block aligned - self.bytesToEncrypt = self.bytesToEncrypt[-numExtraBytes:] - else: - self.bytesToEncrypt = '' - - if more == None: # last decrypt remove padding - plainText = self.padding.removePad(plainText, self.blockSize) - self.resetDecrypt() - return plainText - - -class Pad: - def __init__(self): - pass # eventually could put in calculation of min and max size extension - -class padWithPadLen(Pad): - """ Pad a binary string with the length of the padding """ - - def addPad(self, extraBytes, blockSize): - """ Add padding to a binary string to make it an even multiple - of the block size """ - blocks, numExtraBytes = divmod(len(extraBytes), blockSize) - padLength = blockSize - numExtraBytes - return extraBytes + padLength*chr(padLength) - - def removePad(self, paddedBinaryString, blockSize): - """ Remove padding from a binary string """ - if not(0 6 and i%Nk == 4 : - temp = [ Sbox[byte] for byte in temp ] # SubWord(temp) - w.append( [ w[i-Nk][byte]^temp[byte] for byte in range(4) ] ) - return w - -Rcon = (0,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36, # note extra '0' !!! - 0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6, - 0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91) - -#------------------------------------- -def AddRoundKey(algInstance, keyBlock): - """ XOR the algorithm state with a block of key material """ - for column in range(algInstance.Nb): - for row in range(4): - algInstance.state[column][row] ^= keyBlock[column][row] -#------------------------------------- - -def SubBytes(algInstance): - for column in range(algInstance.Nb): - for row in range(4): - algInstance.state[column][row] = Sbox[algInstance.state[column][row]] - -def InvSubBytes(algInstance): - for column in range(algInstance.Nb): - for row in range(4): - algInstance.state[column][row] = InvSbox[algInstance.state[column][row]] - -Sbox = (0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5, - 0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76, - 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0, - 0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0, - 0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc, - 0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15, - 0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a, - 0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75, - 0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0, - 0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84, - 0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b, - 0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf, - 0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85, - 0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8, - 0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5, - 0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2, - 0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17, - 0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73, - 0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88, - 0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb, - 0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c, - 0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79, - 0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9, - 0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08, - 0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6, - 0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a, - 0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e, - 0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e, - 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94, - 0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf, - 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68, - 0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16) - -InvSbox = (0x52,0x09,0x6a,0xd5,0x30,0x36,0xa5,0x38, - 0xbf,0x40,0xa3,0x9e,0x81,0xf3,0xd7,0xfb, - 0x7c,0xe3,0x39,0x82,0x9b,0x2f,0xff,0x87, - 0x34,0x8e,0x43,0x44,0xc4,0xde,0xe9,0xcb, - 0x54,0x7b,0x94,0x32,0xa6,0xc2,0x23,0x3d, - 0xee,0x4c,0x95,0x0b,0x42,0xfa,0xc3,0x4e, - 0x08,0x2e,0xa1,0x66,0x28,0xd9,0x24,0xb2, - 0x76,0x5b,0xa2,0x49,0x6d,0x8b,0xd1,0x25, - 0x72,0xf8,0xf6,0x64,0x86,0x68,0x98,0x16, - 0xd4,0xa4,0x5c,0xcc,0x5d,0x65,0xb6,0x92, - 0x6c,0x70,0x48,0x50,0xfd,0xed,0xb9,0xda, - 0x5e,0x15,0x46,0x57,0xa7,0x8d,0x9d,0x84, - 0x90,0xd8,0xab,0x00,0x8c,0xbc,0xd3,0x0a, - 0xf7,0xe4,0x58,0x05,0xb8,0xb3,0x45,0x06, - 0xd0,0x2c,0x1e,0x8f,0xca,0x3f,0x0f,0x02, - 0xc1,0xaf,0xbd,0x03,0x01,0x13,0x8a,0x6b, - 0x3a,0x91,0x11,0x41,0x4f,0x67,0xdc,0xea, - 0x97,0xf2,0xcf,0xce,0xf0,0xb4,0xe6,0x73, - 0x96,0xac,0x74,0x22,0xe7,0xad,0x35,0x85, - 0xe2,0xf9,0x37,0xe8,0x1c,0x75,0xdf,0x6e, - 0x47,0xf1,0x1a,0x71,0x1d,0x29,0xc5,0x89, - 0x6f,0xb7,0x62,0x0e,0xaa,0x18,0xbe,0x1b, - 0xfc,0x56,0x3e,0x4b,0xc6,0xd2,0x79,0x20, - 0x9a,0xdb,0xc0,0xfe,0x78,0xcd,0x5a,0xf4, - 0x1f,0xdd,0xa8,0x33,0x88,0x07,0xc7,0x31, - 0xb1,0x12,0x10,0x59,0x27,0x80,0xec,0x5f, - 0x60,0x51,0x7f,0xa9,0x19,0xb5,0x4a,0x0d, - 0x2d,0xe5,0x7a,0x9f,0x93,0xc9,0x9c,0xef, - 0xa0,0xe0,0x3b,0x4d,0xae,0x2a,0xf5,0xb0, - 0xc8,0xeb,0xbb,0x3c,0x83,0x53,0x99,0x61, - 0x17,0x2b,0x04,0x7e,0xba,0x77,0xd6,0x26, - 0xe1,0x69,0x14,0x63,0x55,0x21,0x0c,0x7d) - -#------------------------------------- -""" For each block size (Nb), the ShiftRow operation shifts row i - by the amount Ci. Note that row 0 is not shifted. - Nb C1 C2 C3 - ------------------- """ -shiftOffset = { 4 : ( 0, 1, 2, 3), - 5 : ( 0, 1, 2, 3), - 6 : ( 0, 1, 2, 3), - 7 : ( 0, 1, 2, 4), - 8 : ( 0, 1, 3, 4) } -def ShiftRows(algInstance): - tmp = [0]*algInstance.Nb # list of size Nb - for r in range(1,4): # row 0 reamains unchanged and can be skipped - for c in range(algInstance.Nb): - tmp[c] = algInstance.state[(c+shiftOffset[algInstance.Nb][r]) % algInstance.Nb][r] - for c in range(algInstance.Nb): - algInstance.state[c][r] = tmp[c] -def InvShiftRows(algInstance): - tmp = [0]*algInstance.Nb # list of size Nb - for r in range(1,4): # row 0 reamains unchanged and can be skipped - for c in range(algInstance.Nb): - tmp[c] = algInstance.state[(c+algInstance.Nb-shiftOffset[algInstance.Nb][r]) % algInstance.Nb][r] - for c in range(algInstance.Nb): - algInstance.state[c][r] = tmp[c] -#------------------------------------- -def MixColumns(a): - Sprime = [0,0,0,0] - for j in range(a.Nb): # for each column - Sprime[0] = mul(2,a.state[j][0])^mul(3,a.state[j][1])^mul(1,a.state[j][2])^mul(1,a.state[j][3]) - Sprime[1] = mul(1,a.state[j][0])^mul(2,a.state[j][1])^mul(3,a.state[j][2])^mul(1,a.state[j][3]) - Sprime[2] = mul(1,a.state[j][0])^mul(1,a.state[j][1])^mul(2,a.state[j][2])^mul(3,a.state[j][3]) - Sprime[3] = mul(3,a.state[j][0])^mul(1,a.state[j][1])^mul(1,a.state[j][2])^mul(2,a.state[j][3]) - for i in range(4): - a.state[j][i] = Sprime[i] - -def InvMixColumns(a): - """ Mix the four bytes of every column in a linear way - This is the opposite operation of Mixcolumn """ - Sprime = [0,0,0,0] - for j in range(a.Nb): # for each column - Sprime[0] = mul(0x0E,a.state[j][0])^mul(0x0B,a.state[j][1])^mul(0x0D,a.state[j][2])^mul(0x09,a.state[j][3]) - Sprime[1] = mul(0x09,a.state[j][0])^mul(0x0E,a.state[j][1])^mul(0x0B,a.state[j][2])^mul(0x0D,a.state[j][3]) - Sprime[2] = mul(0x0D,a.state[j][0])^mul(0x09,a.state[j][1])^mul(0x0E,a.state[j][2])^mul(0x0B,a.state[j][3]) - Sprime[3] = mul(0x0B,a.state[j][0])^mul(0x0D,a.state[j][1])^mul(0x09,a.state[j][2])^mul(0x0E,a.state[j][3]) - for i in range(4): - a.state[j][i] = Sprime[i] - -#------------------------------------- -def mul(a, b): - """ Multiply two elements of GF(2^m) - needed for MixColumn and InvMixColumn """ - if (a !=0 and b!=0): - return Alogtable[(Logtable[a] + Logtable[b])%255] - else: - return 0 - -Logtable = ( 0, 0, 25, 1, 50, 2, 26, 198, 75, 199, 27, 104, 51, 238, 223, 3, - 100, 4, 224, 14, 52, 141, 129, 239, 76, 113, 8, 200, 248, 105, 28, 193, - 125, 194, 29, 181, 249, 185, 39, 106, 77, 228, 166, 114, 154, 201, 9, 120, - 101, 47, 138, 5, 33, 15, 225, 36, 18, 240, 130, 69, 53, 147, 218, 142, - 150, 143, 219, 189, 54, 208, 206, 148, 19, 92, 210, 241, 64, 70, 131, 56, - 102, 221, 253, 48, 191, 6, 139, 98, 179, 37, 226, 152, 34, 136, 145, 16, - 126, 110, 72, 195, 163, 182, 30, 66, 58, 107, 40, 84, 250, 133, 61, 186, - 43, 121, 10, 21, 155, 159, 94, 202, 78, 212, 172, 229, 243, 115, 167, 87, - 175, 88, 168, 80, 244, 234, 214, 116, 79, 174, 233, 213, 231, 230, 173, 232, - 44, 215, 117, 122, 235, 22, 11, 245, 89, 203, 95, 176, 156, 169, 81, 160, - 127, 12, 246, 111, 23, 196, 73, 236, 216, 67, 31, 45, 164, 118, 123, 183, - 204, 187, 62, 90, 251, 96, 177, 134, 59, 82, 161, 108, 170, 85, 41, 157, - 151, 178, 135, 144, 97, 190, 220, 252, 188, 149, 207, 205, 55, 63, 91, 209, - 83, 57, 132, 60, 65, 162, 109, 71, 20, 42, 158, 93, 86, 242, 211, 171, - 68, 17, 146, 217, 35, 32, 46, 137, 180, 124, 184, 38, 119, 153, 227, 165, - 103, 74, 237, 222, 197, 49, 254, 24, 13, 99, 140, 128, 192, 247, 112, 7) - -Alogtable= ( 1, 3, 5, 15, 17, 51, 85, 255, 26, 46, 114, 150, 161, 248, 19, 53, - 95, 225, 56, 72, 216, 115, 149, 164, 247, 2, 6, 10, 30, 34, 102, 170, - 229, 52, 92, 228, 55, 89, 235, 38, 106, 190, 217, 112, 144, 171, 230, 49, - 83, 245, 4, 12, 20, 60, 68, 204, 79, 209, 104, 184, 211, 110, 178, 205, - 76, 212, 103, 169, 224, 59, 77, 215, 98, 166, 241, 8, 24, 40, 120, 136, - 131, 158, 185, 208, 107, 189, 220, 127, 129, 152, 179, 206, 73, 219, 118, 154, - 181, 196, 87, 249, 16, 48, 80, 240, 11, 29, 39, 105, 187, 214, 97, 163, - 254, 25, 43, 125, 135, 146, 173, 236, 47, 113, 147, 174, 233, 32, 96, 160, - 251, 22, 58, 78, 210, 109, 183, 194, 93, 231, 50, 86, 250, 21, 63, 65, - 195, 94, 226, 61, 71, 201, 64, 192, 91, 237, 44, 116, 156, 191, 218, 117, - 159, 186, 213, 100, 172, 239, 42, 126, 130, 157, 188, 223, 122, 142, 137, 128, - 155, 182, 193, 88, 232, 35, 101, 175, 234, 37, 111, 177, 200, 67, 197, 84, - 252, 31, 33, 99, 165, 244, 7, 9, 27, 45, 119, 153, 176, 203, 70, 202, - 69, 207, 74, 222, 121, 139, 134, 145, 168, 227, 62, 66, 198, 81, 243, 14, - 18, 54, 90, 238, 41, 123, 141, 140, 143, 138, 133, 148, 167, 242, 13, 23, - 57, 75, 221, 124, 132, 151, 162, 253, 28, 36, 108, 180, 199, 82, 246, 1) - - - - -""" - AES Encryption Algorithm - The AES algorithm is just Rijndael algorithm restricted to the default - blockSize of 128 bits. -""" - -class AES(Rijndael): - """ The AES algorithm is the Rijndael block cipher restricted to block - sizes of 128 bits and key sizes of 128, 192 or 256 bits - """ - def __init__(self, key = None, padding = padWithPadLen(), keySize=16): - """ Initialize AES, keySize is in bytes """ - if not (keySize == 16 or keySize == 24 or keySize == 32) : - raise BadKeySizeError, 'Illegal AES key size, must be 16, 24, or 32 bytes' - - Rijndael.__init__( self, key, padding=padding, keySize=keySize, blockSize=16 ) - - self.name = 'AES' - - -""" - CBC mode of encryption for block ciphers. - This algorithm mode wraps any BlockCipher to make a - Cipher Block Chaining mode. -""" -from random import Random # should change to crypto.random!!! - - -class CBC(BlockCipher): - """ The CBC class wraps block ciphers to make cipher block chaining (CBC) mode - algorithms. The initialization (IV) is automatic if set to None. Padding - is also automatic based on the Pad class used to initialize the algorithm - """ - def __init__(self, blockCipherInstance, padding = padWithPadLen()): - """ CBC algorithms are created by initializing with a BlockCipher instance """ - self.baseCipher = blockCipherInstance - self.name = self.baseCipher.name + '_CBC' - self.blockSize = self.baseCipher.blockSize - self.keySize = self.baseCipher.keySize - self.padding = padding - self.baseCipher.padding = noPadding() # baseCipher should NOT pad!! - self.r = Random() # for IV generation, currently uses - # mediocre standard distro version <---------------- - import time - newSeed = time.ctime()+str(self.r) # seed with instance location - self.r.seed(newSeed) # to make unique - self.reset() - - def setKey(self, key): - self.baseCipher.setKey(key) - - # Overload to reset both CBC state and the wrapped baseCipher - def resetEncrypt(self): - BlockCipher.resetEncrypt(self) # reset CBC encrypt state (super class) - self.baseCipher.resetEncrypt() # reset base cipher encrypt state - - def resetDecrypt(self): - BlockCipher.resetDecrypt(self) # reset CBC state (super class) - self.baseCipher.resetDecrypt() # reset base cipher decrypt state - - def encrypt(self, plainText, iv=None, more=None): - """ CBC encryption - overloads baseCipher to allow optional explicit IV - when iv=None, iv is auto generated! - """ - if self.encryptBlockCount == 0: - self.iv = iv - else: - assert(iv==None), 'IV used only on first call to encrypt' - - return BlockCipher.encrypt(self,plainText, more=more) - - def decrypt(self, cipherText, iv=None, more=None): - """ CBC decryption - overloads baseCipher to allow optional explicit IV - when iv=None, iv is auto generated! - """ - if self.decryptBlockCount == 0: - self.iv = iv - else: - assert(iv==None), 'IV used only on first call to decrypt' - - return BlockCipher.decrypt(self, cipherText, more=more) - - def encryptBlock(self, plainTextBlock): - """ CBC block encryption, IV is set with 'encrypt' """ - auto_IV = '' - if self.encryptBlockCount == 0: - if self.iv == None: - # generate IV and use - self.iv = ''.join([chr(self.r.randrange(256)) for i in range(self.blockSize)]) - self.prior_encr_CT_block = self.iv - auto_IV = self.prior_encr_CT_block # prepend IV if it's automatic - else: # application provided IV - assert(len(self.iv) == self.blockSize ),'IV must be same length as block' - self.prior_encr_CT_block = self.iv - """ encrypt the prior CT XORed with the PT """ - ct = self.baseCipher.encryptBlock( xor(self.prior_encr_CT_block, plainTextBlock) ) - self.prior_encr_CT_block = ct - return auto_IV+ct - - def decryptBlock(self, encryptedBlock): - """ Decrypt a single block """ - - if self.decryptBlockCount == 0: # first call, process IV - if self.iv == None: # auto decrypt IV? - self.prior_CT_block = encryptedBlock - return '' - else: - assert(len(self.iv)==self.blockSize),"Bad IV size on CBC decryption" - self.prior_CT_block = self.iv - - dct = self.baseCipher.decryptBlock(encryptedBlock) - """ XOR the prior decrypted CT with the prior CT """ - dct_XOR_priorCT = xor( self.prior_CT_block, dct ) - - self.prior_CT_block = encryptedBlock - - return dct_XOR_priorCT - - -""" - AES_CBC Encryption Algorithm -""" - -class AES_CBC(CBC): - """ AES encryption in CBC feedback mode """ - def __init__(self, key=None, padding=padWithPadLen(), keySize=16): - CBC.__init__( self, AES(key, noPadding(), keySize), padding) - self.name = 'AES_CBC' diff --git a/Other_Tools/KindleBooks/lib/alfcrypto.dll b/Other_Tools/KindleBooks/lib/alfcrypto.dll deleted file mode 100644 index 26d740ddb0ed3be82128b3fbe0e45471b0e04f4b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 70144 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 G!2bbGSZYrI diff --git a/Other_Tools/KindleBooks/lib/alfcrypto.py b/Other_Tools/KindleBooks/lib/alfcrypto.py deleted file mode 100644 index e25a0c8..0000000 --- a/Other_Tools/KindleBooks/lib/alfcrypto.py +++ /dev/null @@ -1,290 +0,0 @@ -#! /usr/bin/env python - -import sys, os -import hmac -from struct import pack -import hashlib - - -# interface to needed routines libalfcrypto -def _load_libalfcrypto(): - import ctypes - from ctypes import CDLL, byref, POINTER, c_void_p, c_char_p, c_int, c_long, \ - Structure, c_ulong, create_string_buffer, addressof, string_at, cast, sizeof - - pointer_size = ctypes.sizeof(ctypes.c_voidp) - name_of_lib = None - if sys.platform.startswith('darwin'): - name_of_lib = 'libalfcrypto.dylib' - elif sys.platform.startswith('win'): - if pointer_size == 4: - name_of_lib = 'alfcrypto.dll' - else: - name_of_lib = 'alfcrypto64.dll' - else: - if pointer_size == 4: - name_of_lib = 'libalfcrypto32.so' - else: - name_of_lib = 'libalfcrypto64.so' - - libalfcrypto = sys.path[0] + os.sep + name_of_lib - - if not os.path.isfile(libalfcrypto): - raise Exception('libalfcrypto not found') - - libalfcrypto = CDLL(libalfcrypto) - - c_char_pp = POINTER(c_char_p) - c_int_p = POINTER(c_int) - - - def F(restype, name, argtypes): - func = getattr(libalfcrypto, name) - func.restype = restype - func.argtypes = argtypes - return func - - # aes cbc decryption - # - # struct aes_key_st { - # unsigned long rd_key[4 *(AES_MAXNR + 1)]; - # int rounds; - # }; - # - # typedef struct aes_key_st AES_KEY; - # - # int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key); - # - # - # void AES_cbc_encrypt(const unsigned char *in, unsigned char *out, - # const unsigned long length, const AES_KEY *key, - # unsigned char *ivec, const int enc); - - AES_MAXNR = 14 - - class AES_KEY(Structure): - _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)] - - AES_KEY_p = POINTER(AES_KEY) - AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p, c_int]) - AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',[c_char_p, c_int, AES_KEY_p]) - - - - # Pukall 1 Cipher - # unsigned char *PC1(const unsigned char *key, unsigned int klen, const unsigned char *src, - # unsigned char *dest, unsigned int len, int decryption); - - PC1 = F(c_char_p, 'PC1', [c_char_p, c_ulong, c_char_p, c_char_p, c_ulong, c_ulong]) - - # Topaz Encryption - # typedef struct _TpzCtx { - # unsigned int v[2]; - # } TpzCtx; - # - # void topazCryptoInit(TpzCtx *ctx, const unsigned char *key, int klen); - # void topazCryptoDecrypt(const TpzCtx *ctx, const unsigned char *in, unsigned char *out, int len); - - class TPZ_CTX(Structure): - _fields_ = [('v', c_long * 2)] - - TPZ_CTX_p = POINTER(TPZ_CTX) - topazCryptoInit = F(None, 'topazCryptoInit', [TPZ_CTX_p, c_char_p, c_ulong]) - topazCryptoDecrypt = F(None, 'topazCryptoDecrypt', [TPZ_CTX_p, c_char_p, c_char_p, c_ulong]) - - - class AES_CBC(object): - def __init__(self): - self._blocksize = 0 - self._keyctx = None - self._iv = 0 - - def set_decrypt_key(self, userkey, iv): - self._blocksize = len(userkey) - if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : - raise Exception('AES CBC improper key used') - return - keyctx = self._keyctx = AES_KEY() - self._iv = iv - rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx) - if rv < 0: - raise Exception('Failed to initialize AES CBC key') - - def decrypt(self, data): - out = create_string_buffer(len(data)) - mutable_iv = create_string_buffer(self._iv, len(self._iv)) - rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, mutable_iv, 0) - if rv == 0: - raise Exception('AES CBC decryption failed') - return out.raw - - class Pukall_Cipher(object): - def __init__(self): - self.key = None - - def PC1(self, key, src, decryption=True): - self.key = key - out = create_string_buffer(len(src)) - de = 0 - if decryption: - de = 1 - rv = PC1(key, len(key), src, out, len(src), de) - return out.raw - - class Topaz_Cipher(object): - def __init__(self): - self._ctx = None - - def ctx_init(self, key): - tpz_ctx = self._ctx = TPZ_CTX() - topazCryptoInit(tpz_ctx, key, len(key)) - return tpz_ctx - - def decrypt(self, data, ctx=None): - if ctx == None: - ctx = self._ctx - out = create_string_buffer(len(data)) - topazCryptoDecrypt(ctx, data, out, len(data)) - return out.raw - - print "Using Library AlfCrypto DLL/DYLIB/SO" - return (AES_CBC, Pukall_Cipher, Topaz_Cipher) - - -def _load_python_alfcrypto(): - - import aescbc - - class Pukall_Cipher(object): - def __init__(self): - self.key = None - - def PC1(self, key, src, decryption=True): - sum1 = 0; - sum2 = 0; - keyXorVal = 0; - if len(key)!=16: - print "Bad key length!" - return None - wkey = [] - for i in xrange(8): - wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1])) - dst = "" - for i in xrange(len(src)): - temp1 = 0; - byteXorVal = 0; - for j in xrange(8): - temp1 ^= wkey[j] - sum2 = (sum2+j)*20021 + sum1 - sum1 = (temp1*346)&0xFFFF - sum2 = (sum2+sum1)&0xFFFF - temp1 = (temp1*20021+1)&0xFFFF - byteXorVal ^= temp1 ^ sum2 - curByte = ord(src[i]) - if not decryption: - keyXorVal = curByte * 257; - curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF - if decryption: - keyXorVal = curByte * 257; - for j in xrange(8): - wkey[j] ^= keyXorVal; - dst+=chr(curByte) - return dst - - class Topaz_Cipher(object): - def __init__(self): - self._ctx = None - - def ctx_init(self, key): - ctx1 = 0x0CAFFE19E - for keyChar in key: - keyByte = ord(keyChar) - ctx2 = ctx1 - ctx1 = ((((ctx1 >>2) * (ctx1 >>7))&0xFFFFFFFF) ^ (keyByte * keyByte * 0x0F902007)& 0xFFFFFFFF ) - self._ctx = [ctx1, ctx2] - return [ctx1,ctx2] - - def decrypt(self, data, ctx=None): - if ctx == None: - ctx = self._ctx - ctx1 = ctx[0] - ctx2 = ctx[1] - plainText = "" - for dataChar in data: - dataByte = ord(dataChar) - m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF - ctx2 = ctx1 - ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF) - plainText += chr(m) - return plainText - - class AES_CBC(object): - def __init__(self): - self._key = None - self._iv = None - self.aes = None - - def set_decrypt_key(self, userkey, iv): - self._key = userkey - self._iv = iv - self.aes = aescbc.AES_CBC(userkey, aescbc.noPadding(), len(userkey)) - - def decrypt(self, data): - iv = self._iv - cleartext = self.aes.decrypt(iv + data) - return cleartext - - return (AES_CBC, Pukall_Cipher, Topaz_Cipher) - - -def _load_crypto(): - AES_CBC = Pukall_Cipher = Topaz_Cipher = None - cryptolist = (_load_libalfcrypto, _load_python_alfcrypto) - for loader in cryptolist: - try: - AES_CBC, Pukall_Cipher, Topaz_Cipher = loader() - break - except (ImportError, Exception): - pass - return AES_CBC, Pukall_Cipher, Topaz_Cipher - -AES_CBC, Pukall_Cipher, Topaz_Cipher = _load_crypto() - - -class KeyIVGen(object): - # this only exists in openssl so we will use pure python implementation instead - # PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1', - # [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p]) - def pbkdf2(self, passwd, salt, iter, keylen): - - def xorstr( a, b ): - if len(a) != len(b): - raise Exception("xorstr(): lengths differ") - return ''.join((chr(ord(x)^ord(y)) for x, y in zip(a, b))) - - def prf( h, data ): - hm = h.copy() - hm.update( data ) - return hm.digest() - - def pbkdf2_F( h, salt, itercount, blocknum ): - U = prf( h, salt + pack('>i',blocknum ) ) - T = U - for i in range(2, itercount+1): - U = prf( h, U ) - T = xorstr( T, U ) - return T - - sha = hashlib.sha1 - digest_size = sha().digest_size - # l - number of output blocks to produce - l = keylen / digest_size - if keylen % digest_size != 0: - l += 1 - h = hmac.new( passwd, None, sha ) - T = "" - for i in range(1, l+1): - T += pbkdf2_F( h, salt, iter, i ) - return T[0: keylen] - - diff --git a/Other_Tools/KindleBooks/lib/alfcrypto64.dll b/Other_Tools/KindleBooks/lib/alfcrypto64.dll deleted file mode 100644 index 7bef68eac0d0a751b0c7090f5b5427a1d5ab1a59..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 52224 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 NdK>>N@IQ70{vUg3cj*8C diff --git a/Other_Tools/KindleBooks/lib/alfcrypto_src.zip b/Other_Tools/KindleBooks/lib/alfcrypto_src.zip deleted file mode 100644 index 269810cfa24541f8be10050e7192fb6211a45a98..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17393 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 VE;*= 0x80: - datax = (data & 0x7F) - while data >= 0x80 : - data = ord(bookFile.read(1)) - datax = (datax <<7) + (data & 0x7F) - data = datax - - if flag: - data = -data - return data - -# -# Encode a number in 7 bit format -# - -def encodeNumber(number): - result = "" - negative = False - flag = 0 - - if number < 0 : - number = -number + 1 - negative = True - - while True: - byte = number & 0x7F - number = number >> 7 - byte += flag - result += chr(byte) - flag = 0x80 - if number == 0 : - if (byte == 0xFF and negative == False) : - result += chr(0x80) - break - - if negative: - result += chr(0xFF) - - return result[::-1] - -# -# Get a length prefixed string from the file -# - -def bookReadString(): - stringLength = bookReadEncodedNumber() - return unpack(str(stringLength)+"s",bookFile.read(stringLength))[0] - -# -# Returns a length prefixed string -# - -def lengthPrefixString(data): - return encodeNumber(len(data))+data - - -# -# Read and return the data of one header record at the current book file position [[offset,compressedLength,decompressedLength],...] -# - -def bookReadHeaderRecordData(): - nbValues = bookReadEncodedNumber() - values = [] - for i in range (0,nbValues): - values.append([bookReadEncodedNumber(),bookReadEncodedNumber(),bookReadEncodedNumber()]) - return values - -# -# Read and parse one header record at the current book file position and return the associated data [[offset,compressedLength,decompressedLength],...] -# - -def parseTopazHeaderRecord(): - if ord(bookFile.read(1)) != 0x63: - raise CMBDTCFatal("Parse Error : Invalid Header") - - tag = bookReadString() - record = bookReadHeaderRecordData() - return [tag,record] - -# -# Parse the header of a Topaz file, get all the header records and the offset for the payload -# - -def parseTopazHeader(): - global bookHeaderRecords - global bookPayloadOffset - magic = unpack("4s",bookFile.read(4))[0] - - if magic != 'TPZ0': - raise CMBDTCFatal("Parse Error : Invalid Header, not a Topaz file") - - nbRecords = bookReadEncodedNumber() - bookHeaderRecords = {} - - for i in range (0,nbRecords): - result = parseTopazHeaderRecord() - bookHeaderRecords[result[0]] = result[1] - - if ord(bookFile.read(1)) != 0x64 : - raise CMBDTCFatal("Parse Error : Invalid Header") - - bookPayloadOffset = bookFile.tell() - -# -# Get a record in the book payload, given its name and index. If necessary the record is decrypted. The record is not decompressed -# - -def getBookPayloadRecord(name, index): - encrypted = False - - try: - recordOffset = bookHeaderRecords[name][index][0] - except: - raise CMBDTCFatal("Parse Error : Invalid Record, record not found") - - bookFile.seek(bookPayloadOffset + recordOffset) - - tag = bookReadString() - if tag != name : - raise CMBDTCFatal("Parse Error : Invalid Record, record name doesn't match") - - recordIndex = bookReadEncodedNumber() - - if recordIndex < 0 : - encrypted = True - recordIndex = -recordIndex -1 - - if recordIndex != index : - raise CMBDTCFatal("Parse Error : Invalid Record, index doesn't match") - - if bookHeaderRecords[name][index][2] != 0 : - record = bookFile.read(bookHeaderRecords[name][index][2]) - else: - record = bookFile.read(bookHeaderRecords[name][index][1]) - - if encrypted: - ctx = topazCryptoInit(bookKey) - record = topazCryptoDecrypt(record,ctx) - - return record - -# -# Extract, decrypt and decompress a book record indicated by name and index and print it or save it in "filename" -# - -def extractBookPayloadRecord(name, index, filename): - compressed = False - - try: - compressed = bookHeaderRecords[name][index][2] != 0 - record = getBookPayloadRecord(name,index) - except: - print("Could not find record") - - if compressed: - try: - record = zlib.decompress(record) - except: - raise CMBDTCFatal("Could not decompress record") - - if filename != "": - try: - file = open(filename,"wb") - file.write(record) - file.close() - except: - raise CMBDTCFatal("Could not write to destination file") - else: - print(record) - -# -# return next record [key,value] from the book metadata from the current book position -# - -def readMetadataRecord(): - return [bookReadString(),bookReadString()] - -# -# Parse the metadata record from the book payload and return a list of [key,values] -# - -def parseMetadata(): - global bookHeaderRecords - global bookPayloadAddress - global bookMetadata - bookMetadata = {} - bookFile.seek(bookPayloadOffset + bookHeaderRecords["metadata"][0][0]) - tag = bookReadString() - if tag != "metadata" : - raise CMBDTCFatal("Parse Error : Record Names Don't Match") - - flags = ord(bookFile.read(1)) - nbRecords = ord(bookFile.read(1)) - - for i in range (0,nbRecords) : - record =readMetadataRecord() - bookMetadata[record[0]] = record[1] - -# -# Returns two bit at offset from a bit field -# - -def getTwoBitsFromBitField(bitField,offset): - byteNumber = offset // 4 - bitPosition = 6 - 2*(offset % 4) - - return ord(bitField[byteNumber]) >> bitPosition & 3 - -# -# Returns the six bits at offset from a bit field -# - -def getSixBitsFromBitField(bitField,offset): - offset *= 3 - value = (getTwoBitsFromBitField(bitField,offset) <<4) + (getTwoBitsFromBitField(bitField,offset+1) << 2) +getTwoBitsFromBitField(bitField,offset+2) - return value - -# -# 8 bits to six bits encoding from hash to generate PID string -# - -def encodePID(hash): - global charMap3 - PID = "" - for position in range (0,8): - PID += charMap3[getSixBitsFromBitField(hash,position)] - return PID - -# -# Context initialisation for the Topaz Crypto -# - -def topazCryptoInit(key): - ctx1 = 0x0CAFFE19E - - for keyChar in key: - keyByte = ord(keyChar) - ctx2 = ctx1 - ctx1 = ((((ctx1 >>2) * (ctx1 >>7))&0xFFFFFFFF) ^ (keyByte * keyByte * 0x0F902007)& 0xFFFFFFFF ) - return [ctx1,ctx2] - -# -# decrypt data with the context prepared by topazCryptoInit() -# - -def topazCryptoDecrypt(data, ctx): - ctx1 = ctx[0] - ctx2 = ctx[1] - - plainText = "" - - for dataChar in data: - dataByte = ord(dataChar) - m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF - ctx2 = ctx1 - ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF) - plainText += chr(m) - - return plainText - -# -# Decrypt a payload record with the PID -# - -def decryptRecord(data,PID): - ctx = topazCryptoInit(PID) - return topazCryptoDecrypt(data, ctx) - -# -# Try to decrypt a dkey record (contains the book PID) -# - -def decryptDkeyRecord(data,PID): - record = decryptRecord(data,PID) - fields = unpack("3sB8sB8s3s",record) - - if fields[0] != "PID" or fields[5] != "pid" : - raise CMBDTCError("Didn't find PID magic numbers in record") - elif fields[1] != 8 or fields[3] != 8 : - raise CMBDTCError("Record didn't contain correct length fields") - elif fields[2] != PID : - raise CMBDTCError("Record didn't contain PID") - - return fields[4] - -# -# Decrypt all the book's dkey records (contain the book PID) -# - -def decryptDkeyRecords(data,PID): - nbKeyRecords = ord(data[0]) - records = [] - data = data[1:] - for i in range (0,nbKeyRecords): - length = ord(data[0]) - try: - key = decryptDkeyRecord(data[1:length+1],PID) - records.append(key) - except CMBDTCError: - pass - data = data[1+length:] - - return records - -# -# Encryption table used to generate the device PID -# - -def generatePidEncryptionTable() : - table = [] - for counter1 in range (0,0x100): - value = counter1 - for counter2 in range (0,8): - if (value & 1 == 0) : - value = value >> 1 - else : - value = value >> 1 - value = value ^ 0xEDB88320 - table.append(value) - return table - -# -# Seed value used to generate the device PID -# - -def generatePidSeed(table,dsn) : - value = 0 - for counter in range (0,4) : - index = (ord(dsn[counter]) ^ value) &0xFF - value = (value >> 8) ^ table[index] - return value - -# -# Generate the device PID -# - -def generateDevicePID(table,dsn,nbRoll): - seed = generatePidSeed(table,dsn) - pidAscii = "" - pid = [(seed >>24) &0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF,(seed>>24) & 0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF] - index = 0 - - for counter in range (0,nbRoll): - pid[index] = pid[index] ^ ord(dsn[counter]) - index = (index+1) %8 - - for counter in range (0,8): - index = ((((pid[counter] >>5) & 3) ^ pid[counter]) & 0x1f) + (pid[counter] >> 7) - pidAscii += charMap4[index] - return pidAscii - -# -# Create decrypted book payload -# - -def createDecryptedPayload(payload): - - # store data to be able to create the header later - headerData= [] - currentOffset = 0 - - # Add social DRM to decrypted files - - try: - data = getKindleInfoValueForKey("kindle.name.info")+":"+ getKindleInfoValueForKey("login") - if payload!= None: - payload.write(lengthPrefixString("sdrm")) - payload.write(encodeNumber(0)) - payload.write(data) - else: - currentOffset += len(lengthPrefixString("sdrm")) - currentOffset += len(encodeNumber(0)) - currentOffset += len(data) - except: - pass - - for headerRecord in bookHeaderRecords: - name = headerRecord - newRecord = [] - - if name != "dkey" : - - for index in range (0,len(bookHeaderRecords[name])) : - offset = currentOffset - - if payload != None: - # write tag - payload.write(lengthPrefixString(name)) - # write data - payload.write(encodeNumber(index)) - payload.write(getBookPayloadRecord(name, index)) - - else : - currentOffset += len(lengthPrefixString(name)) - currentOffset += len(encodeNumber(index)) - currentOffset += len(getBookPayloadRecord(name, index)) - newRecord.append([offset,bookHeaderRecords[name][index][1],bookHeaderRecords[name][index][2]]) - - headerData.append([name,newRecord]) - - - - return headerData - -# -# Create decrypted book -# - -def createDecryptedBook(outputFile): - outputFile = open(outputFile,"wb") - # Write the payload in a temporary file - headerData = createDecryptedPayload(None) - outputFile.write("TPZ0") - outputFile.write(encodeNumber(len(headerData))) - - for header in headerData : - outputFile.write(chr(0x63)) - outputFile.write(lengthPrefixString(header[0])) - outputFile.write(encodeNumber(len(header[1]))) - for numbers in header[1] : - outputFile.write(encodeNumber(numbers[0])) - outputFile.write(encodeNumber(numbers[1])) - outputFile.write(encodeNumber(numbers[2])) - - outputFile.write(chr(0x64)) - createDecryptedPayload(outputFile) - outputFile.close() - -# -# Set the command to execute by the programm according to cmdLine parameters -# - -def setCommand(name) : - global command - if command != "" : - raise CMBDTCFatal("Invalid command line parameters") - else : - command = name - -# -# Program usage -# - -def usage(): - print("\nUsage:") - print("\nCMBDTC.py [options] bookFileName\n") - print("-p Adds a PID to the list of PIDs that are tried to decrypt the book key (can be used several times)") - print("-d Saves a decrypted copy of the book") - print("-r Prints or writes to disk a record indicated in the form name:index (e.g \"img:0\")") - print("-o Output file name to write records and decrypted books") - print("-v Verbose (can be used several times)") - print("-i Prints kindle.info database") - -# -# Main -# - -def main(argv=sys.argv): - global kindleDatabase - global bookMetadata - global bookKey - global bookFile - global command - - progname = os.path.basename(argv[0]) - - verbose = 0 - recordName = "" - recordIndex = 0 - outputFile = "" - PIDs = [] - kindleDatabase = None - command = "" - - - try: - opts, args = getopt.getopt(sys.argv[1:], "vdir:o:p:") - except getopt.GetoptError, err: - # print help information and exit: - print str(err) # will print something like "option -a not recognized" - usage() - sys.exit(2) - - if len(opts) == 0 and len(args) == 0 : - usage() - sys.exit(2) - - for o, a in opts: - if o == "-v": - verbose+=1 - if o == "-i": - setCommand("printInfo") - if o =="-o": - if a == None : - raise CMBDTCFatal("Invalid parameter for -o") - outputFile = a - if o =="-r": - setCommand("printRecord") - try: - recordName,recordIndex = a.split(':') - except: - raise CMBDTCFatal("Invalid parameter for -r") - if o =="-p": - PIDs.append(a) - if o =="-d": - setCommand("doit") - - if command == "" : - raise CMBDTCFatal("No action supplied on command line") - - # - # Read the encrypted database - # - - try: - kindleDatabase = parseKindleInfo() - except Exception, message: - if verbose>0: - print(message) - - if kindleDatabase != None : - if command == "printInfo" : - printKindleInfo() - - # - # Compute the DSN - # - - # Get the Mazama Random number - MazamaRandomNumber = getKindleInfoValueForKey("MazamaRandomNumber") - - # Get the HDD serial - encodedSystemVolumeSerialNumber = encodeHash(str(GetVolumeSerialNumber(GetSystemDirectory().split('\\')[0] + '\\')),charMap1) - - # Get the current user name - encodedUsername = encodeHash(GetUserName(),charMap1) - - # concat, hash and encode - DSN = encode(SHA1(MazamaRandomNumber+encodedSystemVolumeSerialNumber+encodedUsername),charMap1) - - if verbose >1: - print("DSN: " + DSN) - - # - # Compute the device PID - # - - table = generatePidEncryptionTable() - devicePID = generateDevicePID(table,DSN,4) - PIDs.append(devicePID) - - if verbose > 0: - print("Device PID: " + devicePID) - - # - # Open book and parse metadata - # - - if len(args) == 1: - - bookFile = openBook(args[0]) - parseTopazHeader() - parseMetadata() - - # - # Compute book PID - # - - # Get the account token - - if kindleDatabase != None: - kindleAccountToken = getKindleInfoValueForKey("kindle.account.tokens") - - if verbose >1: - print("Account Token: " + kindleAccountToken) - - keysRecord = bookMetadata["keys"] - keysRecordRecord = bookMetadata[keysRecord] - - pidHash = SHA1(DSN+kindleAccountToken+keysRecord+keysRecordRecord) - - bookPID = encodePID(pidHash) - PIDs.append(bookPID) - - if verbose > 0: - print ("Book PID: " + bookPID ) - - # - # Decrypt book key - # - - dkey = getBookPayloadRecord('dkey', 0) - - bookKeys = [] - for PID in PIDs : - bookKeys+=decryptDkeyRecords(dkey,PID) - - if len(bookKeys) == 0 : - if verbose > 0 : - print ("Book key could not be found. Maybe this book is not registered with this device.") - else : - bookKey = bookKeys[0] - if verbose > 0: - print("Book key: " + bookKey.encode('hex')) - - - - if command == "printRecord" : - extractBookPayloadRecord(recordName,int(recordIndex),outputFile) - if outputFile != "" and verbose>0 : - print("Wrote record to file: "+outputFile) - elif command == "doit" : - if outputFile!="" : - createDecryptedBook(outputFile) - if verbose >0 : - print ("Decrypted book saved. Don't pirate!") - elif verbose > 0: - print("Output file name was not supplied.") - - return 0 - -if __name__ == '__main__': - sys.exit(main()) - diff --git a/Other_Tools/KindleBooks/lib/config.py b/Other_Tools/KindleBooks/lib/config.py deleted file mode 100644 index 9825878..0000000 --- a/Other_Tools/KindleBooks/lib/config.py +++ /dev/null @@ -1,59 +0,0 @@ -from PyQt4.Qt import QWidget, QVBoxLayout, QLabel, QLineEdit - -from calibre.utils.config import JSONConfig - -# This is where all preferences for this plugin will be stored -# You should always prefix your config file name with plugins/, -# so as to ensure you dont accidentally clobber a calibre config file -prefs = JSONConfig('plugins/K4MobiDeDRM') - -# Set defaults -prefs.defaults['pids'] = "" -prefs.defaults['serials'] = "" -prefs.defaults['WINEPREFIX'] = None - - -class ConfigWidget(QWidget): - - def __init__(self): - QWidget.__init__(self) - self.l = QVBoxLayout() - self.setLayout(self.l) - - self.serialLabel = QLabel('eInk Kindle Serial numbers (First character B, 16 characters, use commas if more than one)') - self.l.addWidget(self.serialLabel) - - self.serials = QLineEdit(self) - self.serials.setText(prefs['serials']) - self.l.addWidget(self.serials) - self.serialLabel.setBuddy(self.serials) - - self.pidLabel = QLabel('Mobipocket PIDs (8 or 10 characters, use commas if more than one)') - self.l.addWidget(self.pidLabel) - - self.pids = QLineEdit(self) - self.pids.setText(prefs['pids']) - self.l.addWidget(self.pids) - self.pidLabel.setBuddy(self.serials) - - self.wpLabel = QLabel('For Linux only: WINEPREFIX (enter absolute path)') - self.l.addWidget(self.wpLabel) - - self.wineprefix = QLineEdit(self) - wineprefix = prefs['WINEPREFIX'] - if wineprefix is not None: - self.wineprefix.setText(wineprefix) - else: - self.wineprefix.setText('') - - self.l.addWidget(self.wineprefix) - self.wpLabel.setBuddy(self.wineprefix) - - def save_settings(self): - prefs['pids'] = str(self.pids.text()).replace(" ","") - prefs['serials'] = str(self.serials.text()).replace(" ","") - winepref=str(self.wineprefix.text()) - if winepref.strip() != '': - prefs['WINEPREFIX'] = winepref - else: - prefs['WINEPREFIX'] = None diff --git a/Other_Tools/KindleBooks/lib/convert2xml.py b/Other_Tools/KindleBooks/lib/convert2xml.py deleted file mode 100644 index c412d7b..0000000 --- a/Other_Tools/KindleBooks/lib/convert2xml.py +++ /dev/null @@ -1,846 +0,0 @@ -#! /usr/bin/python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab -# For use with Topaz Scripts Version 2.6 - -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 csv -import os -import getopt -from struct import pack -from struct import unpack - -class TpzDRMError(Exception): - pass - -# Get a 7 bit encoded number from string. The most -# significant byte comes first and has the high bit (8th) set - -def readEncodedNumber(file): - flag = False - c = file.read(1) - if (len(c) == 0): - return None - data = ord(c) - - if data == 0xFF: - flag = True - c = file.read(1) - if (len(c) == 0): - return None - data = ord(c) - - if data >= 0x80: - datax = (data & 0x7F) - while data >= 0x80 : - c = file.read(1) - if (len(c) == 0): - return None - data = ord(c) - datax = (datax <<7) + (data & 0x7F) - data = datax - - if flag: - data = -data - return data - - -# returns a binary string that encodes a number into 7 bits -# most significant byte first which has the high bit set - -def encodeNumber(number): - result = "" - negative = False - flag = 0 - - if number < 0 : - number = -number + 1 - negative = True - - while True: - byte = number & 0x7F - number = number >> 7 - byte += flag - result += chr(byte) - flag = 0x80 - if number == 0 : - if (byte == 0xFF and negative == False) : - result += chr(0x80) - break - - if negative: - result += chr(0xFF) - - return result[::-1] - - - -# create / read a length prefixed string from the file - -def lengthPrefixString(data): - return encodeNumber(len(data))+data - -def readString(file): - stringLength = readEncodedNumber(file) - if (stringLength == None): - return "" - sv = file.read(stringLength) - if (len(sv) != stringLength): - return "" - return unpack(str(stringLength)+"s",sv)[0] - - -# convert a binary string generated by encodeNumber (7 bit encoded number) -# to the value you would find inside the page*.dat files to be processed - -def convert(i): - result = '' - val = encodeNumber(i) - for j in xrange(len(val)): - c = ord(val[j:j+1]) - result += '%02x' % c - return result - - - -# the complete string table used to store all book text content -# as well as the xml tokens and values that make sense out of it - -class Dictionary(object): - def __init__(self, dictFile): - self.filename = dictFile - self.size = 0 - self.fo = file(dictFile,'rb') - self.stable = [] - self.size = readEncodedNumber(self.fo) - for i in xrange(self.size): - self.stable.append(self.escapestr(readString(self.fo))) - self.pos = 0 - - def escapestr(self, str): - str = str.replace('&','&') - str = str.replace('<','<') - str = str.replace('>','>') - str = str.replace('=','=') - return str - - def lookup(self,val): - if ((val >= 0) and (val < self.size)) : - self.pos = val - return self.stable[self.pos] - else: - print "Error - %d outside of string table limits" % val - raise TpzDRMError('outside of string table limits') - # sys.exit(-1) - - def getSize(self): - return self.size - - def getPos(self): - return self.pos - - def dumpDict(self): - for i in xrange(self.size): - print "%d %s %s" % (i, convert(i), self.stable[i]) - return - -# parses the xml snippets that are represented by each page*.dat file. -# also parses the other0.dat file - the main stylesheet -# and information used to inject the xml snippets into page*.dat files - -class PageParser(object): - def __init__(self, filename, dict, debug, flat_xml): - self.fo = file(filename,'rb') - self.id = os.path.basename(filename).replace('.dat','') - self.dict = dict - self.debug = debug - self.flat_xml = flat_xml - self.tagpath = [] - self.doc = [] - self.snippetList = [] - - - # hash table used to enable the decoding process - # This has all been developed by trial and error so it may still have omissions or - # contain errors - # Format: - # tag : (number of arguments, argument type, subtags present, special case of subtags presents when escaped) - - token_tags = { - 'x' : (1, 'scalar_number', 0, 0), - 'y' : (1, 'scalar_number', 0, 0), - 'h' : (1, 'scalar_number', 0, 0), - 'w' : (1, 'scalar_number', 0, 0), - 'firstWord' : (1, 'scalar_number', 0, 0), - 'lastWord' : (1, 'scalar_number', 0, 0), - 'rootID' : (1, 'scalar_number', 0, 0), - 'stemID' : (1, 'scalar_number', 0, 0), - 'type' : (1, 'scalar_text', 0, 0), - - 'info' : (0, 'number', 1, 0), - - 'info.word' : (0, 'number', 1, 1), - 'info.word.ocrText' : (1, 'text', 0, 0), - 'info.word.firstGlyph' : (1, 'raw', 0, 0), - 'info.word.lastGlyph' : (1, 'raw', 0, 0), - 'info.word.bl' : (1, 'raw', 0, 0), - 'info.word.link_id' : (1, 'number', 0, 0), - - 'glyph' : (0, 'number', 1, 1), - 'glyph.x' : (1, 'number', 0, 0), - 'glyph.y' : (1, 'number', 0, 0), - 'glyph.glyphID' : (1, 'number', 0, 0), - - 'dehyphen' : (0, 'number', 1, 1), - 'dehyphen.rootID' : (1, 'number', 0, 0), - 'dehyphen.stemID' : (1, 'number', 0, 0), - 'dehyphen.stemPage' : (1, 'number', 0, 0), - 'dehyphen.sh' : (1, 'number', 0, 0), - - 'links' : (0, 'number', 1, 1), - 'links.page' : (1, 'number', 0, 0), - 'links.rel' : (1, 'number', 0, 0), - 'links.row' : (1, 'number', 0, 0), - 'links.title' : (1, 'text', 0, 0), - 'links.href' : (1, 'text', 0, 0), - 'links.type' : (1, 'text', 0, 0), - 'links.id' : (1, 'number', 0, 0), - - 'paraCont' : (0, 'number', 1, 1), - 'paraCont.rootID' : (1, 'number', 0, 0), - 'paraCont.stemID' : (1, 'number', 0, 0), - 'paraCont.stemPage' : (1, 'number', 0, 0), - - 'paraStems' : (0, 'number', 1, 1), - 'paraStems.stemID' : (1, 'number', 0, 0), - - 'wordStems' : (0, 'number', 1, 1), - 'wordStems.stemID' : (1, 'number', 0, 0), - - 'empty' : (1, 'snippets', 1, 0), - - 'page' : (1, 'snippets', 1, 0), - 'page.pageid' : (1, 'scalar_text', 0, 0), - 'page.pagelabel' : (1, 'scalar_text', 0, 0), - 'page.type' : (1, 'scalar_text', 0, 0), - 'page.h' : (1, 'scalar_number', 0, 0), - 'page.w' : (1, 'scalar_number', 0, 0), - 'page.startID' : (1, 'scalar_number', 0, 0), - - 'group' : (1, 'snippets', 1, 0), - 'group.type' : (1, 'scalar_text', 0, 0), - 'group._tag' : (1, 'scalar_text', 0, 0), - 'group.orientation': (1, 'scalar_text', 0, 0), - - 'region' : (1, 'snippets', 1, 0), - 'region.type' : (1, 'scalar_text', 0, 0), - 'region.x' : (1, 'scalar_number', 0, 0), - 'region.y' : (1, 'scalar_number', 0, 0), - 'region.h' : (1, 'scalar_number', 0, 0), - 'region.w' : (1, 'scalar_number', 0, 0), - 'region.orientation' : (1, 'scalar_text', 0, 0), - - '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), - - 'paragraph' : (1, 'snippets', 1, 0), - 'paragraph.class' : (1, 'scalar_text', 0, 0), - 'paragraph.firstWord' : (1, 'scalar_number', 0, 0), - 'paragraph.lastWord' : (1, 'scalar_number', 0, 0), - 'paragraph.lastWord' : (1, 'scalar_number', 0, 0), - 'paragraph.gridSize' : (1, 'scalar_number', 0, 0), - 'paragraph.gridBottomCenter' : (1, 'scalar_number', 0, 0), - 'paragraph.gridTopCenter' : (1, 'scalar_number', 0, 0), - 'paragraph.gridBeginCenter' : (1, 'scalar_number', 0, 0), - 'paragraph.gridEndCenter' : (1, 'scalar_number', 0, 0), - - - 'word_semantic' : (1, 'snippets', 1, 1), - 'word_semantic.type' : (1, 'scalar_text', 0, 0), - 'word_semantic.firstWord' : (1, 'scalar_number', 0, 0), - 'word_semantic.lastWord' : (1, 'scalar_number', 0, 0), - - 'word' : (1, 'snippets', 1, 0), - 'word.type' : (1, 'scalar_text', 0, 0), - 'word.class' : (1, 'scalar_text', 0, 0), - 'word.firstGlyph' : (1, 'scalar_number', 0, 0), - 'word.lastGlyph' : (1, 'scalar_number', 0, 0), - - '_span' : (1, 'snippets', 1, 0), - '_span.firstWord' : (1, 'scalar_number', 0, 0), - '_span.lastWord' : (1, 'scalar_number', 0, 0), - '_span.gridSize' : (1, 'scalar_number', 0, 0), - '_span.gridBottomCenter' : (1, 'scalar_number', 0, 0), - '_span.gridTopCenter' : (1, 'scalar_number', 0, 0), - '_span.gridBeginCenter' : (1, 'scalar_number', 0, 0), - '_span.gridEndCenter' : (1, 'scalar_number', 0, 0), - - 'span' : (1, 'snippets', 1, 0), - 'span.firstWord' : (1, 'scalar_number', 0, 0), - 'span.lastWord' : (1, 'scalar_number', 0, 0), - 'span.gridSize' : (1, 'scalar_number', 0, 0), - 'span.gridBottomCenter' : (1, 'scalar_number', 0, 0), - 'span.gridTopCenter' : (1, 'scalar_number', 0, 0), - 'span.gridBeginCenter' : (1, 'scalar_number', 0, 0), - 'span.gridEndCenter' : (1, 'scalar_number', 0, 0), - - 'extratokens' : (1, 'snippets', 1, 0), - 'extratokens.type' : (1, 'scalar_text', 0, 0), - 'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0), - 'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0), - - 'glyph.h' : (1, 'number', 0, 0), - 'glyph.w' : (1, 'number', 0, 0), - 'glyph.use' : (1, 'number', 0, 0), - 'glyph.vtx' : (1, 'number', 0, 1), - 'glyph.len' : (1, 'number', 0, 1), - 'glyph.dpi' : (1, 'number', 0, 0), - 'vtx' : (0, 'number', 1, 1), - 'vtx.x' : (1, 'number', 0, 0), - 'vtx.y' : (1, 'number', 0, 0), - 'len' : (0, 'number', 1, 1), - 'len.n' : (1, 'number', 0, 0), - - 'book' : (1, 'snippets', 1, 0), - 'version' : (1, 'snippets', 1, 0), - 'version.FlowEdit_1_id' : (1, 'scalar_text', 0, 0), - 'version.FlowEdit_1_version' : (1, 'scalar_text', 0, 0), - 'version.Schema_id' : (1, 'scalar_text', 0, 0), - 'version.Schema_version' : (1, 'scalar_text', 0, 0), - 'version.Topaz_version' : (1, 'scalar_text', 0, 0), - 'version.WordDetailEdit_1_id' : (1, 'scalar_text', 0, 0), - 'version.WordDetailEdit_1_version' : (1, 'scalar_text', 0, 0), - 'version.ZoneEdit_1_id' : (1, 'scalar_text', 0, 0), - 'version.ZoneEdit_1_version' : (1, 'scalar_text', 0, 0), - 'version.chapterheaders' : (1, 'scalar_text', 0, 0), - 'version.creation_date' : (1, 'scalar_text', 0, 0), - 'version.header_footer' : (1, 'scalar_text', 0, 0), - 'version.init_from_ocr' : (1, 'scalar_text', 0, 0), - 'version.letter_insertion' : (1, 'scalar_text', 0, 0), - 'version.xmlinj_convert' : (1, 'scalar_text', 0, 0), - 'version.xmlinj_reflow' : (1, 'scalar_text', 0, 0), - 'version.xmlinj_transform' : (1, 'scalar_text', 0, 0), - 'version.findlists' : (1, 'scalar_text', 0, 0), - 'version.page_num' : (1, 'scalar_text', 0, 0), - 'version.page_type' : (1, 'scalar_text', 0, 0), - 'version.bad_text' : (1, 'scalar_text', 0, 0), - 'version.glyph_mismatch' : (1, 'scalar_text', 0, 0), - 'version.margins' : (1, 'scalar_text', 0, 0), - 'version.staggered_lines' : (1, 'scalar_text', 0, 0), - 'version.paragraph_continuation' : (1, 'scalar_text', 0, 0), - 'version.toc' : (1, 'scalar_text', 0, 0), - - 'stylesheet' : (1, 'snippets', 1, 0), - 'style' : (1, 'snippets', 1, 0), - 'style._tag' : (1, 'scalar_text', 0, 0), - 'style.type' : (1, 'scalar_text', 0, 0), - 'style._parent_type' : (1, 'scalar_text', 0, 0), - 'style.class' : (1, 'scalar_text', 0, 0), - 'style._after_class' : (1, 'scalar_text', 0, 0), - 'rule' : (1, 'snippets', 1, 0), - 'rule.attr' : (1, 'scalar_text', 0, 0), - 'rule.value' : (1, 'scalar_text', 0, 0), - - 'original' : (0, 'number', 1, 1), - 'original.pnum' : (1, 'number', 0, 0), - 'original.pid' : (1, 'text', 0, 0), - 'pages' : (0, 'number', 1, 1), - 'pages.ref' : (1, 'number', 0, 0), - 'pages.id' : (1, 'number', 0, 0), - 'startID' : (0, 'number', 1, 1), - 'startID.page' : (1, 'number', 0, 0), - 'startID.id' : (1, 'number', 0, 0), - - } - - - # full tag path record keeping routines - def tag_push(self, token): - self.tagpath.append(token) - def tag_pop(self): - if len(self.tagpath) > 0 : - self.tagpath.pop() - def tagpath_len(self): - return len(self.tagpath) - def get_tagpath(self, i): - cnt = len(self.tagpath) - if i < cnt : result = self.tagpath[i] - for j in xrange(i+1, cnt) : - result += '.' + self.tagpath[j] - return result - - - # list of absolute command byte values values that indicate - # various types of loop meachanisms typically used to generate vectors - - cmd_list = (0x76, 0x76) - - # peek at and return 1 byte that is ahead by i bytes - def peek(self, aheadi): - c = self.fo.read(aheadi) - if (len(c) == 0): - return None - self.fo.seek(-aheadi,1) - c = c[-1:] - return ord(c) - - - # get the next value from the file being processed - def getNext(self): - nbyte = self.peek(1); - if (nbyte == None): - return None - val = readEncodedNumber(self.fo) - return val - - - # format an arg by argtype - def formatArg(self, arg, argtype): - if (argtype == 'text') or (argtype == 'scalar_text') : - result = self.dict.lookup(arg) - elif (argtype == 'raw') or (argtype == 'number') or (argtype == 'scalar_number') : - result = arg - elif (argtype == 'snippets') : - result = arg - else : - print "Error Unknown argtype %s" % argtype - sys.exit(-2) - return result - - - # process the next tag token, recursively handling subtags, - # arguments, and commands - def procToken(self, token): - - known_token = False - self.tag_push(token) - - if self.debug : print 'Processing: ', self.get_tagpath(0) - cnt = self.tagpath_len() - for j in xrange(cnt): - tkn = self.get_tagpath(j) - if tkn in self.token_tags : - num_args = self.token_tags[tkn][0] - argtype = self.token_tags[tkn][1] - subtags = self.token_tags[tkn][2] - splcase = self.token_tags[tkn][3] - ntags = -1 - known_token = True - break - - if known_token : - - # handle subtags if present - subtagres = [] - if (splcase == 1): - # this type of tag uses of escape marker 0x74 indicate subtag count - if self.peek(1) == 0x74: - skip = readEncodedNumber(self.fo) - subtags = 1 - num_args = 0 - - if (subtags == 1): - ntags = readEncodedNumber(self.fo) - if self.debug : print 'subtags: ' + token + ' has ' + str(ntags) - for j in xrange(ntags): - val = readEncodedNumber(self.fo) - subtagres.append(self.procToken(self.dict.lookup(val))) - - # arguments can be scalars or vectors of text or numbers - argres = [] - if num_args > 0 : - firstarg = self.peek(1) - if (firstarg in self.cmd_list) and (argtype != 'scalar_number') and (argtype != 'scalar_text'): - # single argument is a variable length vector of data - arg = readEncodedNumber(self.fo) - argres = self.decodeCMD(arg,argtype) - else : - # num_arg scalar arguments - for i in xrange(num_args): - argres.append(self.formatArg(readEncodedNumber(self.fo), argtype)) - - # build the return tag - result = [] - tkn = self.get_tagpath(0) - result.append(tkn) - result.append(subtagres) - result.append(argtype) - result.append(argres) - self.tag_pop() - return result - - # all tokens that need to be processed should be in the hash - # table if it may indicate a problem, either new token - # or an out of sync condition - else: - result = [] - if (self.debug): - print 'Unknown Token:', token - self.tag_pop() - return result - - - # special loop used to process code snippets - # it is NEVER used to format arguments. - # builds the snippetList - def doLoop72(self, argtype): - cnt = readEncodedNumber(self.fo) - if self.debug : - result = 'Set of '+ str(cnt) + ' xml snippets. The overall structure \n' - result += 'of the document is indicated by snippet number sets at the\n' - result += 'end of each snippet. \n' - print result - for i in xrange(cnt): - if self.debug: print 'Snippet:',str(i) - snippet = [] - snippet.append(i) - val = readEncodedNumber(self.fo) - snippet.append(self.procToken(self.dict.lookup(val))) - self.snippetList.append(snippet) - return - - - - # general loop code gracisouly submitted by "skindle" - thank you! - def doLoop76Mode(self, argtype, cnt, mode): - result = [] - adj = 0 - if mode & 1: - adj = readEncodedNumber(self.fo) - mode = mode >> 1 - x = [] - for i in xrange(cnt): - x.append(readEncodedNumber(self.fo) - adj) - for i in xrange(mode): - for j in xrange(1, cnt): - x[j] = x[j] + x[j - 1] - for i in xrange(cnt): - result.append(self.formatArg(x[i],argtype)) - return result - - - # dispatches loop commands bytes with various modes - # The 0x76 style loops are used to build vectors - - # This was all derived by trial and error and - # new loop types may exist that are not handled here - # since they did not appear in the test cases - - def decodeCMD(self, cmd, argtype): - if (cmd == 0x76): - - # loop with cnt, and mode to control loop styles - cnt = readEncodedNumber(self.fo) - mode = readEncodedNumber(self.fo) - - if self.debug : print 'Loop for', cnt, 'with mode', mode, ': ' - return self.doLoop76Mode(argtype, cnt, mode) - - if self.dbug: print "Unknown command", cmd - result = [] - return result - - - - # add full tag path to injected snippets - def updateName(self, tag, prefix): - name = tag[0] - subtagList = tag[1] - argtype = tag[2] - argList = tag[3] - nname = prefix + '.' + name - nsubtaglist = [] - for j in subtagList: - nsubtaglist.append(self.updateName(j,prefix)) - ntag = [] - ntag.append(nname) - ntag.append(nsubtaglist) - ntag.append(argtype) - ntag.append(argList) - return ntag - - - - # perform depth first injection of specified snippets into this one - def injectSnippets(self, snippet): - snipno, tag = snippet - name = tag[0] - subtagList = tag[1] - argtype = tag[2] - argList = tag[3] - nsubtagList = [] - if len(argList) > 0 : - for j in argList: - asnip = self.snippetList[j] - aso, atag = self.injectSnippets(asnip) - atag = self.updateName(atag, name) - nsubtagList.append(atag) - argtype='number' - argList=[] - if len(nsubtagList) > 0 : - subtagList.extend(nsubtagList) - tag = [] - tag.append(name) - tag.append(subtagList) - tag.append(argtype) - tag.append(argList) - snippet = [] - snippet.append(snipno) - snippet.append(tag) - return snippet - - - - # format the tag for output - def formatTag(self, node): - name = node[0] - subtagList = node[1] - argtype = node[2] - argList = node[3] - fullpathname = name.split('.') - nodename = fullpathname.pop() - ilvl = len(fullpathname) - indent = ' ' * (3 * ilvl) - rlst = [] - rlst.append(indent + '<' + nodename + '>') - if len(argList) > 0: - alst = [] - for j in argList: - if (argtype == 'text') or (argtype == 'scalar_text') : - alst.append(j + '|') - else : - alst.append(str(j) + ',') - argres = "".join(alst) - argres = argres[0:-1] - if argtype == 'snippets' : - rlst.append('snippets:' + argres) - else : - rlst.append(argres) - if len(subtagList) > 0 : - rlst.append('\n') - for j in subtagList: - if len(j) > 0 : - rlst.append(self.formatTag(j)) - rlst.append(indent + '\n') - else: - rlst.append('\n') - return "".join(rlst) - - - # flatten tag - def flattenTag(self, node): - name = node[0] - subtagList = node[1] - argtype = node[2] - argList = node[3] - rlst = [] - rlst.append(name) - if (len(argList) > 0): - alst = [] - for j in argList: - if (argtype == 'text') or (argtype == 'scalar_text') : - alst.append(j + '|') - else : - alst.append(str(j) + '|') - argres = "".join(alst) - argres = argres[0:-1] - if argtype == 'snippets' : - rlst.append('.snippets=' + argres) - else : - rlst.append('=' + argres) - rlst.append('\n') - for j in subtagList: - if len(j) > 0 : - rlst.append(self.flattenTag(j)) - return "".join(rlst) - - - # reduce create xml output - def formatDoc(self, flat_xml): - rlst = [] - for j in self.doc : - if len(j) > 0: - if flat_xml: - rlst.append(self.flattenTag(j)) - else: - rlst.append(self.formatTag(j)) - result = "".join(rlst) - if self.debug : print result - return result - - - - # main loop - parse the page.dat files - # to create structured document and snippets - - # FIXME: value at end of magic appears to be a subtags count - # but for what? For now, inject an 'info" tag as it is in - # every dictionary and seems close to what is meant - # The alternative is to special case the last _ "0x5f" to mean something - - def process(self): - - # peek at the first bytes to see what type of file it is - magic = self.fo.read(9) - if (magic[0:1] == 'p') and (magic[2:9] == 'marker_'): - first_token = 'info' - elif (magic[0:1] == 'p') and (magic[2:9] == '__PAGE_'): - skip = self.fo.read(2) - first_token = 'info' - elif (magic[0:1] == 'p') and (magic[2:8] == '_PAGE_'): - first_token = 'info' - elif (magic[0:1] == 'g') and (magic[2:9] == '__GLYPH'): - skip = self.fo.read(3) - first_token = 'info' - else : - # other0.dat file - first_token = None - self.fo.seek(-9,1) - - - # main loop to read and build the document tree - while True: - - if first_token != None : - # use "inserted" first token 'info' for page and glyph files - tag = self.procToken(first_token) - if len(tag) > 0 : - self.doc.append(tag) - first_token = None - - v = self.getNext() - if (v == None): - break - - if (v == 0x72): - self.doLoop72('number') - elif (v > 0) and (v < self.dict.getSize()) : - tag = self.procToken(self.dict.lookup(v)) - if len(tag) > 0 : - self.doc.append(tag) - else: - if self.debug: - print "Main Loop: Unknown value: %x" % v - if (v == 0): - if (self.peek(1) == 0x5f): - skip = self.fo.read(1) - first_token = 'info' - - # now do snippet injection - if len(self.snippetList) > 0 : - if self.debug : print 'Injecting Snippets:' - snippet = self.injectSnippets(self.snippetList[0]) - snipno = snippet[0] - tag_add = snippet[1] - if self.debug : print self.formatTag(tag_add) - if len(tag_add) > 0: - self.doc.append(tag_add) - - # handle generation of xml output - xmlpage = self.formatDoc(self.flat_xml) - - return xmlpage - - -def fromData(dict, fname): - flat_xml = True - debug = False - pp = PageParser(fname, dict, debug, flat_xml) - xmlpage = pp.process() - return xmlpage - -def getXML(dict, fname): - flat_xml = False - debug = False - pp = PageParser(fname, dict, debug, flat_xml) - xmlpage = pp.process() - return xmlpage - -def usage(): - print 'Usage: ' - print ' convert2xml.py dict0000.dat infile.dat ' - print ' ' - print ' Options:' - print ' -h print this usage help message ' - print ' -d turn on debug output to check for potential errors ' - print ' --flat-xml output the flattened xml page description only ' - print ' ' - print ' This program will attempt to convert a page*.dat file or ' - print ' glyphs*.dat file, using the dict0000.dat file, to its xml description. ' - print ' ' - print ' Use "cmbtc_dump.py" first to unencrypt, uncompress, and dump ' - print ' the *.dat files from a Topaz format e-book.' - -# -# Main -# - -def main(argv): - dictFile = "" - pageFile = "" - debug = False - flat_xml = False - printOutput = False - if len(argv) == 0: - printOutput = True - argv = sys.argv - - try: - opts, args = getopt.getopt(argv[1:], "hd", ["flat-xml"]) - - except getopt.GetoptError, err: - - # print help information and exit: - print str(err) # will print something like "option -a not recognized" - usage() - sys.exit(2) - - if len(opts) == 0 and len(args) == 0 : - usage() - sys.exit(2) - - for o, a in opts: - if o =="-d": - debug=True - if o =="-h": - usage() - sys.exit(0) - if o =="--flat-xml": - flat_xml = True - - dictFile, pageFile = args[0], args[1] - - # read in the string table dictionary - dict = Dictionary(dictFile) - # dict.dumpDict() - - # create a page parser - pp = PageParser(pageFile, dict, debug, flat_xml) - - xmlpage = pp.process() - - if printOutput: - print xmlpage - return 0 - - return xmlpage - -if __name__ == '__main__': - sys.exit(main('')) diff --git a/Other_Tools/KindleBooks/lib/flatxml2html.py b/Other_Tools/KindleBooks/lib/flatxml2html.py deleted file mode 100644 index e5647f4..0000000 --- a/Other_Tools/KindleBooks/lib/flatxml2html.py +++ /dev/null @@ -1,793 +0,0 @@ -#! /usr/bin/python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab -# For use with Topaz Scripts Version 2.6 - -import sys -import csv -import os -import math -import getopt -from struct import pack -from struct import unpack - - -class DocParser(object): - def __init__(self, flatxml, classlst, fileid, bookDir, gdict, fixedimage): - self.id = os.path.basename(fileid).replace('.dat','') - self.svgcount = 0 - self.docList = flatxml.split('\n') - self.docSize = len(self.docList) - self.classList = {} - self.bookDir = bookDir - self.gdict = gdict - tmpList = classlst.split('\n') - for pclass in tmpList: - if pclass != '': - # remove the leading period from the css name - cname = pclass[1:] - self.classList[cname] = True - self.fixedimage = fixedimage - self.ocrtext = [] - self.link_id = [] - self.link_title = [] - self.link_page = [] - self.link_href = [] - self.link_type = [] - self.dehyphen_rootid = [] - self.paracont_stemid = [] - self.parastems_stemid = [] - - - def getGlyph(self, gid): - result = '' - id='id="gl%d"' % gid - return self.gdict.lookup(id) - - def glyphs_to_image(self, glyphList): - - def extract(path, key): - b = path.find(key) + len(key) - e = path.find(' ',b) - return int(path[b:e]) - - svgDir = os.path.join(self.bookDir,'svg') - - imgDir = os.path.join(self.bookDir,'img') - imgname = self.id + '_%04d.svg' % self.svgcount - imgfile = os.path.join(imgDir,imgname) - - # get glyph information - gxList = self.getData('info.glyph.x',0,-1) - gyList = self.getData('info.glyph.y',0,-1) - gidList = self.getData('info.glyph.glyphID',0,-1) - - gids = [] - maxws = [] - maxhs = [] - xs = [] - ys = [] - gdefs = [] - - # get path defintions, positions, dimensions for each glyph - # that makes up the image, and find min x and min y to reposition origin - minx = -1 - miny = -1 - for j in glyphList: - gid = gidList[j] - gids.append(gid) - - xs.append(gxList[j]) - if minx == -1: minx = gxList[j] - else : minx = min(minx, gxList[j]) - - ys.append(gyList[j]) - if miny == -1: miny = gyList[j] - else : miny = min(miny, gyList[j]) - - path = self.getGlyph(gid) - gdefs.append(path) - - maxws.append(extract(path,'width=')) - maxhs.append(extract(path,'height=')) - - - # change the origin to minx, miny and calc max height and width - maxw = maxws[0] + xs[0] - minx - maxh = maxhs[0] + ys[0] - miny - for j in xrange(0, len(xs)): - xs[j] = xs[j] - minx - ys[j] = ys[j] - miny - maxw = max( maxw, (maxws[j] + xs[j]) ) - maxh = max( maxh, (maxhs[j] + ys[j]) ) - - # open the image file for output - ifile = open(imgfile,'w') - ifile.write('\n') - ifile.write('\n') - ifile.write('\n' % (math.floor(maxw/10), math.floor(maxh/10), maxw, maxh)) - ifile.write('\n') - for j in xrange(0,len(gdefs)): - ifile.write(gdefs[j]) - ifile.write('\n') - for j in xrange(0,len(gids)): - ifile.write('\n' % (gids[j], xs[j], ys[j])) - ifile.write('') - ifile.close() - - return 0 - - - - # return tag at line pos in document - def lineinDoc(self, pos) : - if (pos >= 0) and (pos < self.docSize) : - item = self.docList[pos] - if item.find('=') >= 0: - (name, argres) = item.split('=',1) - else : - name = item - argres = '' - return name, argres - - - # find tag in doc if within pos to end inclusive - def findinDoc(self, tagpath, pos, end) : - result = None - if end == -1 : - end = self.docSize - else: - end = min(self.docSize, end) - foundat = -1 - for j in xrange(pos, end): - item = self.docList[j] - if item.find('=') >= 0: - (name, argres) = item.split('=',1) - else : - name = item - argres = '' - if name.endswith(tagpath) : - result = argres - foundat = j - break - return foundat, result - - - # return list of start positions for the tagpath - def posinDoc(self, tagpath): - startpos = [] - pos = 0 - res = "" - while res != None : - (foundpos, res) = self.findinDoc(tagpath, pos, -1) - if res != None : - startpos.append(foundpos) - pos = foundpos + 1 - return startpos - - - # returns a vector of integers for the tagpath - def getData(self, tagpath, pos, end): - argres=[] - (foundat, argt) = self.findinDoc(tagpath, pos, end) - if (argt != None) and (len(argt) > 0) : - argList = argt.split('|') - argres = [ int(strval) for strval in argList] - return argres - - - # get the class - def getClass(self, pclass): - nclass = pclass - - # class names are an issue given topaz may start them with numerals (not allowed), - # use a mix of cases (which cause some browsers problems), and actually - # attach numbers after "_reclustered*" to the end to deal classeses that inherit - # from a base class (but then not actually provide all of these _reclustereed - # classes in the stylesheet! - - # so we clean this up by lowercasing, prepend 'cl-', and getting any baseclass - # that exists in the stylesheet first, and then adding this specific class - # after - - # also some class names have spaces in them so need to convert to dashes - if nclass != None : - nclass = nclass.replace(' ','-') - classres = '' - nclass = nclass.lower() - nclass = 'cl-' + nclass - baseclass = '' - # graphic is the base class for captions - if nclass.find('cl-cap-') >=0 : - classres = 'graphic' + ' ' - else : - # strip to find baseclass - p = nclass.find('_') - if p > 0 : - baseclass = nclass[0:p] - if baseclass in self.classList: - classres += baseclass + ' ' - classres += nclass - nclass = classres - return nclass - - - # develop a sorted description of the starting positions of - # groups and regions on the page, as well as the page type - def PageDescription(self): - - def compare(x, y): - (xtype, xval) = x - (ytype, yval) = y - if xval > yval: - return 1 - if xval == yval: - return 0 - return -1 - - result = [] - (pos, pagetype) = self.findinDoc('page.type',0,-1) - - groupList = self.posinDoc('page.group') - groupregionList = self.posinDoc('page.group.region') - pageregionList = self.posinDoc('page.region') - # integrate into one list - for j in groupList: - result.append(('grpbeg',j)) - for j in groupregionList: - result.append(('gregion',j)) - for j in pageregionList: - result.append(('pregion',j)) - result.sort(compare) - - # insert group end and page end indicators - inGroup = False - j = 0 - while True: - if j == len(result): break - rtype = result[j][0] - rval = result[j][1] - if not inGroup and (rtype == 'grpbeg') : - inGroup = True - j = j + 1 - elif inGroup and (rtype in ('grpbeg', 'pregion')): - result.insert(j,('grpend',rval)) - inGroup = False - else: - j = j + 1 - if inGroup: - result.append(('grpend',-1)) - result.append(('pageend', -1)) - return pagetype, result - - - - # build a description of the paragraph - def getParaDescription(self, start, end, regtype): - - result = [] - - # paragraph - (pos, pclass) = self.findinDoc('paragraph.class',start,end) - - pclass = self.getClass(pclass) - - # if paragraph uses extratokens (extra glyphs) then make it fixed - (pos, extraglyphs) = self.findinDoc('paragraph.extratokens',start,end) - - # build up a description of the paragraph in result and return it - # first check for the basic - all words paragraph - (pos, sfirst) = self.findinDoc('paragraph.firstWord',start,end) - (pos, slast) = self.findinDoc('paragraph.lastWord',start,end) - if (sfirst != None) and (slast != None) : - first = int(sfirst) - last = int(slast) - - makeImage = (regtype == 'vertical') or (regtype == 'table') - makeImage = makeImage or (extraglyphs != None) - if self.fixedimage: - makeImage = makeImage or (regtype == 'fixed') - - if (pclass != None): - makeImage = makeImage or (pclass.find('.inverted') >= 0) - if self.fixedimage : - makeImage = makeImage or (pclass.find('cl-f-') >= 0) - - # before creating an image make sure glyph info exists - gidList = self.getData('info.glyph.glyphID',0,-1) - - makeImage = makeImage & (len(gidList) > 0) - - if not makeImage : - # standard all word paragraph - for wordnum in xrange(first, last): - result.append(('ocr', wordnum)) - return pclass, result - - # convert paragraph to svg image - # translate first and last word into first and last glyphs - # and generate inline image and include it - glyphList = [] - firstglyphList = self.getData('word.firstGlyph',0,-1) - gidList = self.getData('info.glyph.glyphID',0,-1) - firstGlyph = firstglyphList[first] - if last < len(firstglyphList): - lastGlyph = firstglyphList[last] - else : - lastGlyph = len(gidList) - - # handle case of white sapce paragraphs with no actual glyphs in them - # by reverting to text based paragraph - if firstGlyph >= lastGlyph: - # revert to standard text based paragraph - for wordnum in xrange(first, last): - result.append(('ocr', wordnum)) - return pclass, result - - for glyphnum in xrange(firstGlyph, lastGlyph): - glyphList.append(glyphnum) - # include any extratokens if they exist - (pos, sfg) = self.findinDoc('extratokens.firstGlyph',start,end) - (pos, slg) = self.findinDoc('extratokens.lastGlyph',start,end) - if (sfg != None) and (slg != None): - for glyphnum in xrange(int(sfg), int(slg)): - glyphList.append(glyphnum) - num = self.svgcount - self.glyphs_to_image(glyphList) - self.svgcount += 1 - result.append(('svg', num)) - return pclass, result - - # this type of paragraph may be made up of multiple spans, inline - # word monograms (images), and words with semantic meaning, - # plus glyphs used to form starting letter of first word - - # need to parse this type line by line - line = start + 1 - word_class = '' - - # if end is -1 then we must search to end of document - if end == -1 : - end = self.docSize - - # seems some xml has last* coming before first* so we have to - # handle any order - sp_first = -1 - sp_last = -1 - - gl_first = -1 - gl_last = -1 - - ws_first = -1 - ws_last = -1 - - word_class = '' - - word_semantic_type = '' - - while (line < end) : - - (name, argres) = self.lineinDoc(line) - - if name.endswith('span.firstWord') : - sp_first = int(argres) - - elif name.endswith('span.lastWord') : - sp_last = int(argres) - - elif name.endswith('word.firstGlyph') : - gl_first = int(argres) - - elif name.endswith('word.lastGlyph') : - gl_last = int(argres) - - elif name.endswith('word_semantic.firstWord'): - ws_first = int(argres) - - elif name.endswith('word_semantic.lastWord'): - 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' - - elif name.endswith('word.img.src'): - result.append(('img' + word_class, int(argres))) - word_class = '' - - elif name.endswith('region.img.src'): - result.append(('img' + word_class, int(argres))) - - if (sp_first != -1) and (sp_last != -1): - for wordnum in xrange(sp_first, sp_last): - result.append(('ocr', wordnum)) - sp_first = -1 - sp_last = -1 - - if (gl_first != -1) and (gl_last != -1): - glyphList = [] - for glyphnum in xrange(gl_first, gl_last): - glyphList.append(glyphnum) - num = self.svgcount - self.glyphs_to_image(glyphList) - self.svgcount += 1 - result.append(('svg', num)) - gl_first = -1 - gl_last = -1 - - if (ws_first != -1) and (ws_last != -1): - for wordnum in xrange(ws_first, ws_last): - result.append(('ocr', wordnum)) - ws_first = -1 - ws_last = -1 - - line += 1 - - return pclass, result - - - def buildParagraph(self, pclass, pdesc, type, regtype) : - parares = '' - sep ='' - - classres = '' - if pclass : - classres = ' class="' + pclass + '"' - - br_lb = (regtype == 'fixed') or (regtype == 'chapterheading') or (regtype == 'vertical') - - handle_links = len(self.link_id) > 0 - - if (type == 'full') or (type == 'begin') : - parares += '' - - if (type == 'end'): - parares += ' ' - - lstart = len(parares) - - cnt = len(pdesc) - - for j in xrange( 0, cnt) : - - (wtype, num) = pdesc[j] - - if wtype == 'ocr' : - word = self.ocrtext[num] - sep = ' ' - - if handle_links: - link = self.link_id[num] - if (link > 0): - linktype = self.link_type[link-1] - title = self.link_title[link-1] - if (title == "") or (parares.rfind(title) < 0): - title=parares[lstart:] - if linktype == 'external' : - linkhref = self.link_href[link-1] - linkhtml = '' % linkhref - else : - if len(self.link_page) >= link : - ptarget = self.link_page[link-1] - 1 - linkhtml = '' % ptarget - else : - # just link to the current page - linkhtml = '' - linkhtml += title + '' - pos = parares.rfind(title) - if pos >= 0: - parares = parares[0:pos] + linkhtml + parares[pos+len(title):] - else : - parares += linkhtml - lstart = len(parares) - if word == '_link_' : word = '' - elif (link < 0) : - if word == '_link_' : word = '' - - if word == '_lb_': - if ((num-1) in self.dehyphen_rootid ) or handle_links: - word = '' - sep = '' - elif br_lb : - word = '
\n' - sep = '' - else : - word = '\n' - sep = '' - - if num in self.dehyphen_rootid : - word = word[0:-1] - sep = '' - - parares += word + sep - - elif wtype == 'img' : - sep = '' - parares += '' % num - parares += sep - - elif wtype == 'imgsa' : - sep = ' ' - parares += '' % num - parares += sep - - elif wtype == 'svg' : - sep = '' - parares += '' % num - parares += sep - - if len(sep) > 0 : parares = parares[0:-1] - if (type == 'full') or (type == 'end') : - parares += '

' - return parares - - - def buildTOCEntry(self, pdesc) : - parares = '' - sep ='' - tocentry = '' - handle_links = len(self.link_id) > 0 - - lstart = 0 - - cnt = len(pdesc) - for j in xrange( 0, cnt) : - - (wtype, num) = pdesc[j] - - if wtype == 'ocr' : - word = self.ocrtext[num] - sep = ' ' - - if handle_links: - link = self.link_id[num] - if (link > 0): - linktype = self.link_type[link-1] - title = self.link_title[link-1] - title = title.rstrip('. ') - alt_title = parares[lstart:] - alt_title = alt_title.strip() - # now strip off the actual printed page number - alt_title = alt_title.rstrip('01234567890ivxldIVXLD-.') - alt_title = alt_title.rstrip('. ') - # skip over any external links - can't have them in a books toc - if linktype == 'external' : - title = '' - alt_title = '' - linkpage = '' - else : - if len(self.link_page) >= link : - ptarget = self.link_page[link-1] - 1 - linkpage = '%04d' % ptarget - else : - # just link to the current page - linkpage = self.id[4:] - if len(alt_title) >= len(title): - title = alt_title - if title != '' and linkpage != '': - tocentry += title + '|' + linkpage + '\n' - lstart = len(parares) - if word == '_link_' : word = '' - elif (link < 0) : - if word == '_link_' : word = '' - - if word == '_lb_': - word = '' - sep = '' - - if num in self.dehyphen_rootid : - word = word[0:-1] - sep = '' - - parares += word + sep - - else : - continue - - return tocentry - - - - - # walk the document tree collecting the information needed - # to build an html page using the ocrText - - def process(self): - - tocinfo = '' - hlst = [] - - # get the ocr text - (pos, argres) = self.findinDoc('info.word.ocrText',0,-1) - if argres : self.ocrtext = argres.split('|') - - # get information to dehyphenate the text - self.dehyphen_rootid = self.getData('info.dehyphen.rootID',0,-1) - - # determine if first paragraph is continued from previous page - (pos, self.parastems_stemid) = self.findinDoc('info.paraStems.stemID',0,-1) - first_para_continued = (self.parastems_stemid != None) - - # determine if last paragraph is continued onto the next page - (pos, self.paracont_stemid) = self.findinDoc('info.paraCont.stemID',0,-1) - last_para_continued = (self.paracont_stemid != None) - - # collect link ids - self.link_id = self.getData('info.word.link_id',0,-1) - - # collect link destination page numbers - self.link_page = self.getData('info.links.page',0,-1) - - # collect link types (container versus external) - (pos, argres) = self.findinDoc('info.links.type',0,-1) - if argres : self.link_type = argres.split('|') - - # collect link destinations - (pos, argres) = self.findinDoc('info.links.href',0,-1) - if argres : self.link_href = argres.split('|') - - # collect link titles - (pos, argres) = self.findinDoc('info.links.title',0,-1) - if argres : - self.link_title = argres.split('|') - else: - self.link_title.append('') - - # get a descriptions of the starting points of the regions - # and groups on the page - (pagetype, pageDesc) = self.PageDescription() - regcnt = len(pageDesc) - 1 - - anchorSet = False - breakSet = False - inGroup = False - - # process each region on the page and convert what you can to html - - for j in xrange(regcnt): - - (etype, start) = pageDesc[j] - (ntype, end) = pageDesc[j+1] - - - # set anchor for link target on this page - if not anchorSet and not first_para_continued: - hlst.append('\n') - anchorSet = True - - # handle groups of graphics with text captions - if (etype == 'grpbeg'): - (pos, grptype) = self.findinDoc('group.type', start, end) - if grptype != None: - if grptype == 'graphic': - gcstr = ' class="' + grptype + '"' - hlst.append('') - inGroup = True - - elif (etype == 'grpend'): - if inGroup: - hlst.append('\n') - inGroup = False - - else: - (pos, regtype) = self.findinDoc('region.type',start,end) - - if regtype == 'graphic' : - (pos, simgsrc) = self.findinDoc('img.src',start,end) - if simgsrc: - if inGroup: - hlst.append('' % int(simgsrc)) - else: - hlst.append('
' % int(simgsrc)) - - elif regtype == 'chapterheading' : - (pclass, pdesc) = self.getParaDescription(start,end, regtype) - if not breakSet: - hlst.append('
 
\n') - breakSet = True - tag = 'h1' - if pclass and (len(pclass) >= 7): - if pclass[3:7] == 'ch1-' : tag = 'h1' - if pclass[3:7] == 'ch2-' : tag = 'h2' - if pclass[3:7] == 'ch3-' : tag = 'h3' - hlst.append('<' + tag + ' class="' + pclass + '">') - else: - hlst.append('<' + tag + '>') - hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype)) - hlst.append('') - - elif (regtype == 'text') or (regtype == 'fixed') or (regtype == 'insert') or (regtype == 'listitem'): - ptype = 'full' - # check to see if this is a continution from the previous page - if first_para_continued : - ptype = 'end' - first_para_continued = False - (pclass, pdesc) = self.getParaDescription(start,end, regtype) - if pclass and (len(pclass) >= 6) and (ptype == 'full'): - tag = 'p' - if pclass[3:6] == 'h1-' : tag = 'h4' - if pclass[3:6] == 'h2-' : tag = 'h5' - if pclass[3:6] == 'h3-' : tag = 'h6' - hlst.append('<' + tag + ' class="' + pclass + '">') - hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype)) - hlst.append('') - else : - hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype)) - - elif (regtype == 'tocentry') : - ptype = 'full' - if first_para_continued : - ptype = 'end' - first_para_continued = False - (pclass, pdesc) = self.getParaDescription(start,end, regtype) - tocinfo += self.buildTOCEntry(pdesc) - hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype)) - - elif (regtype == 'vertical') or (regtype == 'table') : - ptype = 'full' - if inGroup: - ptype = 'middle' - if first_para_continued : - ptype = 'end' - first_para_continued = False - (pclass, pdesc) = self.getParaDescription(start, end, regtype) - hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype)) - - - elif (regtype == 'synth_fcvr.center'): - (pos, simgsrc) = self.findinDoc('img.src',start,end) - if simgsrc: - hlst.append('
' % int(simgsrc)) - - else : - print ' Making region type', regtype, - (pos, temp) = self.findinDoc('paragraph',start,end) - (pos2, temp) = self.findinDoc('span',start,end) - if pos != -1 or pos2 != -1: - print ' a "text" region' - orig_regtype = regtype - regtype = 'fixed' - ptype = 'full' - # check to see if this is a continution from the previous page - if first_para_continued : - ptype = 'end' - first_para_continued = False - (pclass, pdesc) = self.getParaDescription(start,end, regtype) - if not pclass: - if orig_regtype.endswith('.right') : pclass = 'cl-right' - elif orig_regtype.endswith('.center') : pclass = 'cl-center' - elif orig_regtype.endswith('.left') : pclass = 'cl-left' - elif orig_regtype.endswith('.justify') : pclass = 'cl-justify' - if pclass and (ptype == 'full') and (len(pclass) >= 6): - tag = 'p' - if pclass[3:6] == 'h1-' : tag = 'h4' - if pclass[3:6] == 'h2-' : tag = 'h5' - if pclass[3:6] == 'h3-' : tag = 'h6' - hlst.append('<' + tag + ' class="' + pclass + '">') - hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype)) - hlst.append('') - else : - hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype)) - else : - print ' a "graphic" region' - (pos, simgsrc) = self.findinDoc('img.src',start,end) - if simgsrc: - hlst.append('
' % int(simgsrc)) - - - htmlpage = "".join(hlst) - if last_para_continued : - if htmlpage[-4:] == '

': - htmlpage = htmlpage[0:-4] - last_para_continued = False - - return htmlpage, tocinfo - - -def convert2HTML(flatxml, classlst, fileid, bookDir, gdict, fixedimage): - # create a document parser - dp = DocParser(flatxml, classlst, fileid, bookDir, gdict, fixedimage) - htmlpage, tocinfo = dp.process() - return htmlpage, tocinfo diff --git a/Other_Tools/KindleBooks/lib/flatxml2svg.py b/Other_Tools/KindleBooks/lib/flatxml2svg.py deleted file mode 100644 index 4dfd6c7..0000000 --- a/Other_Tools/KindleBooks/lib/flatxml2svg.py +++ /dev/null @@ -1,249 +0,0 @@ -#! /usr/bin/python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab - -import sys -import csv -import os -import getopt -from struct import pack -from struct import unpack - - -class PParser(object): - def __init__(self, gd, flatxml, meta_array): - self.gd = gd - self.flatdoc = flatxml.split('\n') - self.docSize = len(self.flatdoc) - self.temp = [] - - self.ph = -1 - self.pw = -1 - startpos = self.posinDoc('page.h') or self.posinDoc('book.h') - for p in startpos: - (name, argres) = self.lineinDoc(p) - self.ph = max(self.ph, int(argres)) - startpos = self.posinDoc('page.w') or self.posinDoc('book.w') - for p in startpos: - (name, argres) = self.lineinDoc(p) - self.pw = max(self.pw, int(argres)) - - if self.ph <= 0: - self.ph = int(meta_array.get('pageHeight', '11000')) - if self.pw <= 0: - self.pw = int(meta_array.get('pageWidth', '8500')) - - res = [] - startpos = self.posinDoc('info.glyph.x') - for p in startpos: - argres = self.getDataatPos('info.glyph.x', p) - res.extend(argres) - self.gx = res - - res = [] - startpos = self.posinDoc('info.glyph.y') - for p in startpos: - argres = self.getDataatPos('info.glyph.y', p) - res.extend(argres) - self.gy = res - - res = [] - startpos = self.posinDoc('info.glyph.glyphID') - for p in startpos: - argres = self.getDataatPos('info.glyph.glyphID', p) - res.extend(argres) - self.gid = res - - - # return tag at line pos in document - def lineinDoc(self, pos) : - if (pos >= 0) and (pos < self.docSize) : - item = self.flatdoc[pos] - if item.find('=') >= 0: - (name, argres) = item.split('=',1) - else : - name = item - argres = '' - return name, argres - - # find tag in doc if within pos to end inclusive - def findinDoc(self, tagpath, pos, end) : - result = None - if end == -1 : - end = self.docSize - else: - end = min(self.docSize, end) - foundat = -1 - for j in xrange(pos, end): - item = self.flatdoc[j] - if item.find('=') >= 0: - (name, argres) = item.split('=',1) - else : - name = item - argres = '' - if name.endswith(tagpath) : - result = argres - foundat = j - break - return foundat, result - - # return list of start positions for the tagpath - def posinDoc(self, tagpath): - startpos = [] - pos = 0 - res = "" - while res != None : - (foundpos, res) = self.findinDoc(tagpath, pos, -1) - if res != None : - startpos.append(foundpos) - pos = foundpos + 1 - return startpos - - def getData(self, path): - result = None - cnt = len(self.flatdoc) - for j in xrange(cnt): - item = self.flatdoc[j] - if item.find('=') >= 0: - (name, argt) = item.split('=') - argres = argt.split('|') - else: - name = item - argres = [] - if (name.endswith(path)): - result = argres - break - if (len(argres) > 0) : - for j in xrange(0,len(argres)): - argres[j] = int(argres[j]) - return result - - def getDataatPos(self, path, pos): - result = None - item = self.flatdoc[pos] - if item.find('=') >= 0: - (name, argt) = item.split('=') - argres = argt.split('|') - else: - name = item - argres = [] - if (len(argres) > 0) : - for j in xrange(0,len(argres)): - argres[j] = int(argres[j]) - if (name.endswith(path)): - result = argres - return result - - def getDataTemp(self, path): - result = None - cnt = len(self.temp) - for j in xrange(cnt): - item = self.temp[j] - if item.find('=') >= 0: - (name, argt) = item.split('=') - argres = argt.split('|') - else: - name = item - argres = [] - if (name.endswith(path)): - result = argres - self.temp.pop(j) - break - if (len(argres) > 0) : - for j in xrange(0,len(argres)): - argres[j] = int(argres[j]) - return result - - def getImages(self): - result = [] - self.temp = self.flatdoc - while (self.getDataTemp('img') != None): - h = self.getDataTemp('img.h')[0] - w = self.getDataTemp('img.w')[0] - x = self.getDataTemp('img.x')[0] - y = self.getDataTemp('img.y')[0] - src = self.getDataTemp('img.src')[0] - result.append('\n' % (src, x, y, w, h)) - return result - - def getGlyphs(self): - result = [] - if (self.gid != None) and (len(self.gid) > 0): - glyphs = [] - for j in set(self.gid): - glyphs.append(j) - glyphs.sort() - for gid in glyphs: - id='id="gl%d"' % gid - path = self.gd.lookup(id) - if path: - result.append(id + ' ' + path) - return result - - -def convert2SVG(gdict, flat_xml, pageid, previd, nextid, svgDir, raw, meta_array, scaledpi): - mlst = [] - pp = PParser(gdict, flat_xml, meta_array) - mlst.append('\n') - if (raw): - mlst.append('\n') - mlst.append('\n' % (pp.pw / scaledpi, pp.ph / scaledpi, pp.pw -1, pp.ph -1)) - mlst.append('Page %d - %s by %s\n' % (pageid, meta_array['Title'],meta_array['Authors'])) - else: - mlst.append('\n') - mlst.append('\n') - mlst.append('Page %d - %s by %s\n' % (pageid, meta_array['Title'],meta_array['Authors'])) - mlst.append('\n') - mlst.append('\n') - mlst.append('\n') - mlst.append('\n') - mlst.append('\n') - mlst.append('\n') - mlst.append('\n') - return "".join(mlst) diff --git a/Other_Tools/KindleBooks/lib/genbook.py b/Other_Tools/KindleBooks/lib/genbook.py deleted file mode 100644 index 9733887..0000000 --- a/Other_Tools/KindleBooks/lib/genbook.py +++ /dev/null @@ -1,721 +0,0 @@ -#! /usr/bin/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 csv -import os -import getopt -from struct import pack -from struct import unpack - -class TpzDRMError(Exception): - pass - -# local support routines -if 'calibre' in sys.modules: - inCalibre = True -else: - inCalibre = False - -if inCalibre : - from calibre_plugins.k4mobidedrm import convert2xml - from calibre_plugins.k4mobidedrm import flatxml2html - from calibre_plugins.k4mobidedrm import flatxml2svg - from calibre_plugins.k4mobidedrm import stylexml2css -else : - import convert2xml - import flatxml2html - import flatxml2svg - import stylexml2css - -# global switch -buildXML = False - -# Get a 7 bit encoded number from a file -def readEncodedNumber(file): - flag = False - c = file.read(1) - if (len(c) == 0): - return None - data = ord(c) - if data == 0xFF: - flag = True - c = file.read(1) - if (len(c) == 0): - return None - data = ord(c) - if data >= 0x80: - datax = (data & 0x7F) - while data >= 0x80 : - c = file.read(1) - if (len(c) == 0): - return None - data = ord(c) - datax = (datax <<7) + (data & 0x7F) - data = datax - if flag: - data = -data - return data - -# Get a length prefixed string from the file -def lengthPrefixString(data): - return encodeNumber(len(data))+data - -def readString(file): - stringLength = readEncodedNumber(file) - if (stringLength == None): - return None - sv = file.read(stringLength) - if (len(sv) != stringLength): - return "" - return unpack(str(stringLength)+"s",sv)[0] - -def getMetaArray(metaFile): - # parse the meta file - result = {} - fo = file(metaFile,'rb') - size = readEncodedNumber(fo) - for i in xrange(size): - tag = readString(fo) - value = readString(fo) - result[tag] = value - # print tag, value - fo.close() - return result - - -# dictionary of all text strings by index value -class Dictionary(object): - def __init__(self, dictFile): - self.filename = dictFile - self.size = 0 - self.fo = file(dictFile,'rb') - self.stable = [] - self.size = readEncodedNumber(self.fo) - for i in xrange(self.size): - self.stable.append(self.escapestr(readString(self.fo))) - self.pos = 0 - def escapestr(self, str): - str = str.replace('&','&') - str = str.replace('<','<') - str = str.replace('>','>') - str = str.replace('=','=') - return str - def lookup(self,val): - if ((val >= 0) and (val < self.size)) : - self.pos = val - return self.stable[self.pos] - else: - print "Error - %d outside of string table limits" % val - raise TpzDRMError('outside or string table limits') - # sys.exit(-1) - def getSize(self): - return self.size - def getPos(self): - return self.pos - - -class PageDimParser(object): - def __init__(self, flatxml): - self.flatdoc = flatxml.split('\n') - # find tag if within pos to end inclusive - def findinDoc(self, tagpath, pos, end) : - result = None - docList = self.flatdoc - cnt = len(docList) - if end == -1 : - end = cnt - else: - end = min(cnt,end) - foundat = -1 - for j in xrange(pos, end): - item = docList[j] - if item.find('=') >= 0: - (name, argres) = item.split('=') - else : - name = item - argres = '' - if name.endswith(tagpath) : - result = argres - foundat = j - break - return foundat, result - def process(self): - (pos, sph) = self.findinDoc('page.h',0,-1) - (pos, spw) = self.findinDoc('page.w',0,-1) - if (sph == None): sph = '-1' - if (spw == None): spw = '-1' - return sph, spw - -def getPageDim(flatxml): - # create a document parser - dp = PageDimParser(flatxml) - (ph, pw) = dp.process() - return ph, pw - -class GParser(object): - def __init__(self, flatxml): - self.flatdoc = flatxml.split('\n') - self.dpi = 1440 - self.gh = self.getData('info.glyph.h') - self.gw = self.getData('info.glyph.w') - self.guse = self.getData('info.glyph.use') - if self.guse : - self.count = len(self.guse) - else : - self.count = 0 - self.gvtx = self.getData('info.glyph.vtx') - self.glen = self.getData('info.glyph.len') - self.gdpi = self.getData('info.glyph.dpi') - self.vx = self.getData('info.vtx.x') - self.vy = self.getData('info.vtx.y') - self.vlen = self.getData('info.len.n') - if self.vlen : - self.glen.append(len(self.vlen)) - elif self.glen: - self.glen.append(0) - if self.vx : - self.gvtx.append(len(self.vx)) - elif self.gvtx : - self.gvtx.append(0) - def getData(self, path): - result = None - cnt = len(self.flatdoc) - for j in xrange(cnt): - item = self.flatdoc[j] - if item.find('=') >= 0: - (name, argt) = item.split('=') - argres = argt.split('|') - else: - name = item - argres = [] - if (name == path): - result = argres - break - if (len(argres) > 0) : - for j in xrange(0,len(argres)): - argres[j] = int(argres[j]) - return result - def getGlyphDim(self, gly): - if self.gdpi[gly] == 0: - return 0, 0 - maxh = (self.gh[gly] * self.dpi) / self.gdpi[gly] - maxw = (self.gw[gly] * self.dpi) / self.gdpi[gly] - return maxh, maxw - def getPath(self, gly): - path = '' - if (gly < 0) or (gly >= self.count): - return path - tx = self.vx[self.gvtx[gly]:self.gvtx[gly+1]] - ty = self.vy[self.gvtx[gly]:self.gvtx[gly+1]] - p = 0 - for k in xrange(self.glen[gly], self.glen[gly+1]): - if (p == 0): - zx = tx[0:self.vlen[k]+1] - zy = ty[0:self.vlen[k]+1] - else: - zx = tx[self.vlen[k-1]+1:self.vlen[k]+1] - zy = ty[self.vlen[k-1]+1:self.vlen[k]+1] - p += 1 - j = 0 - while ( j < len(zx) ): - if (j == 0): - # Start Position. - path += 'M %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly]) - elif (j <= len(zx)-3): - # Cubic Bezier Curve - path += 'C %d %d %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[j+1] * self.dpi / self.gdpi[gly], zy[j+1] * self.dpi / self.gdpi[gly], zx[j+2] * self.dpi / self.gdpi[gly], zy[j+2] * self.dpi / self.gdpi[gly]) - j += 2 - elif (j == len(zx)-2): - # Cubic Bezier Curve to Start Position - path += 'C %d %d %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[j+1] * self.dpi / self.gdpi[gly], zy[j+1] * self.dpi / self.gdpi[gly], zx[0] * self.dpi / self.gdpi[gly], zy[0] * self.dpi / self.gdpi[gly]) - j += 1 - elif (j == len(zx)-1): - # Quadratic Bezier Curve to Start Position - path += 'Q %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[0] * self.dpi / self.gdpi[gly], zy[0] * self.dpi / self.gdpi[gly]) - - j += 1 - path += 'z' - return path - - - -# dictionary of all text strings by index value -class GlyphDict(object): - def __init__(self): - self.gdict = {} - def lookup(self, id): - # id='id="gl%d"' % val - if id in self.gdict: - return self.gdict[id] - return None - def addGlyph(self, val, path): - id='id="gl%d"' % val - self.gdict[id] = path - - -def generateBook(bookDir, raw, fixedimage): - # sanity check Topaz file extraction - if not os.path.exists(bookDir) : - print "Can not find directory with unencrypted book" - return 1 - - dictFile = os.path.join(bookDir,'dict0000.dat') - if not os.path.exists(dictFile) : - print "Can not find dict0000.dat file" - return 1 - - pageDir = os.path.join(bookDir,'page') - if not os.path.exists(pageDir) : - print "Can not find page directory in unencrypted book" - return 1 - - imgDir = os.path.join(bookDir,'img') - if not os.path.exists(imgDir) : - print "Can not find image directory in unencrypted book" - return 1 - - glyphsDir = os.path.join(bookDir,'glyphs') - if not os.path.exists(glyphsDir) : - print "Can not find glyphs directory in unencrypted book" - return 1 - - metaFile = os.path.join(bookDir,'metadata0000.dat') - if not os.path.exists(metaFile) : - print "Can not find metadata0000.dat in unencrypted book" - return 1 - - svgDir = os.path.join(bookDir,'svg') - if not os.path.exists(svgDir) : - os.makedirs(svgDir) - - if buildXML: - xmlDir = os.path.join(bookDir,'xml') - if not os.path.exists(xmlDir) : - os.makedirs(xmlDir) - - otherFile = os.path.join(bookDir,'other0000.dat') - if not os.path.exists(otherFile) : - print "Can not find other0000.dat in unencrypted book" - return 1 - - print "Updating to color images if available" - spath = os.path.join(bookDir,'color_img') - dpath = os.path.join(bookDir,'img') - filenames = os.listdir(spath) - filenames = sorted(filenames) - for filename in filenames: - imgname = filename.replace('color','img') - sfile = os.path.join(spath,filename) - dfile = os.path.join(dpath,imgname) - imgdata = file(sfile,'rb').read() - file(dfile,'wb').write(imgdata) - - print "Creating cover.jpg" - isCover = False - cpath = os.path.join(bookDir,'img') - cpath = os.path.join(cpath,'img0000.jpg') - if os.path.isfile(cpath): - cover = file(cpath, 'rb').read() - cpath = os.path.join(bookDir,'cover.jpg') - file(cpath, 'wb').write(cover) - isCover = True - - - print 'Processing Dictionary' - dict = Dictionary(dictFile) - - print 'Processing Meta Data and creating OPF' - meta_array = getMetaArray(metaFile) - - # replace special chars in title and authors like & < > - title = meta_array.get('Title','No Title Provided') - title = title.replace('&','&') - title = title.replace('<','<') - title = title.replace('>','>') - meta_array['Title'] = title - authors = meta_array.get('Authors','No Authors Provided') - authors = authors.replace('&','&') - authors = authors.replace('<','<') - authors = authors.replace('>','>') - meta_array['Authors'] = authors - - if buildXML: - xname = os.path.join(xmlDir, 'metadata.xml') - mlst = [] - for key in meta_array: - mlst.append('\n') - metastr = "".join(mlst) - mlst = None - file(xname, 'wb').write(metastr) - - print 'Processing StyleSheet' - - # get some scaling info from metadata to use while processing styles - # and first page info - - fontsize = '135' - if 'fontSize' in meta_array: - fontsize = meta_array['fontSize'] - - # also get the size of a normal text page - # get the total number of pages unpacked as a safety check - filenames = os.listdir(pageDir) - numfiles = len(filenames) - - spage = '1' - if 'firstTextPage' in meta_array: - spage = meta_array['firstTextPage'] - pnum = int(spage) - if pnum >= numfiles or pnum < 0: - # metadata is wrong so just select a page near the front - # 10% of the book to get a normal text page - pnum = int(0.10 * numfiles) - # print "first normal text page is", spage - - # get page height and width from first text page for use in stylesheet scaling - pname = 'page%04d.dat' % (pnum + 1) - fname = os.path.join(pageDir,pname) - flat_xml = convert2xml.fromData(dict, fname) - - (ph, pw) = getPageDim(flat_xml) - if (ph == '-1') or (ph == '0') : ph = '11000' - if (pw == '-1') or (pw == '0') : pw = '8500' - meta_array['pageHeight'] = ph - meta_array['pageWidth'] = pw - if 'fontSize' not in meta_array.keys(): - meta_array['fontSize'] = fontsize - - # process other.dat for css info and for map of page files to svg images - # this map is needed because some pages actually are made up of multiple - # pageXXXX.xml files - xname = os.path.join(bookDir, 'style.css') - flat_xml = convert2xml.fromData(dict, otherFile) - - # extract info.original.pid to get original page information - pageIDMap = {} - pageidnums = stylexml2css.getpageIDMap(flat_xml) - if len(pageidnums) == 0: - filenames = os.listdir(pageDir) - numfiles = len(filenames) - for k in range(numfiles): - pageidnums.append(k) - # create a map from page ids to list of page file nums to process for that page - for i in range(len(pageidnums)): - id = pageidnums[i] - if id in pageIDMap.keys(): - pageIDMap[id].append(i) - else: - pageIDMap[id] = [i] - - # now get the css info - cssstr , classlst = stylexml2css.convert2CSS(flat_xml, fontsize, ph, pw) - file(xname, 'wb').write(cssstr) - if buildXML: - xname = os.path.join(xmlDir, 'other0000.xml') - file(xname, 'wb').write(convert2xml.getXML(dict, otherFile)) - - print 'Processing Glyphs' - gd = GlyphDict() - filenames = os.listdir(glyphsDir) - filenames = sorted(filenames) - glyfname = os.path.join(svgDir,'glyphs.svg') - glyfile = open(glyfname, 'w') - glyfile.write('\n') - glyfile.write('\n') - glyfile.write('\n') - glyfile.write('Glyphs for %s\n' % meta_array['Title']) - glyfile.write('\n') - counter = 0 - for filename in filenames: - # print ' ', filename - print '.', - fname = os.path.join(glyphsDir,filename) - flat_xml = convert2xml.fromData(dict, fname) - - if buildXML: - xname = os.path.join(xmlDir, filename.replace('.dat','.xml')) - file(xname, 'wb').write(convert2xml.getXML(dict, fname)) - - gp = GParser(flat_xml) - for i in xrange(0, gp.count): - path = gp.getPath(i) - maxh, maxw = gp.getGlyphDim(i) - fullpath = '\n' % (counter * 256 + i, path, maxw, maxh) - glyfile.write(fullpath) - gd.addGlyph(counter * 256 + i, fullpath) - counter += 1 - glyfile.write('\n') - glyfile.write('\n') - glyfile.close() - print " " - - - # start up the html - # also build up tocentries while processing html - htmlFileName = "book.html" - hlst = [] - hlst.append('\n') - hlst.append('\n') - hlst.append('\n') - hlst.append('\n') - hlst.append('\n') - hlst.append('' + meta_array['Title'] + ' by ' + meta_array['Authors'] + '\n') - hlst.append('\n') - hlst.append('\n') - if 'ASIN' in meta_array: - hlst.append('\n') - if 'GUID' in meta_array: - hlst.append('\n') - hlst.append('\n') - hlst.append('\n\n') - - print 'Processing Pages' - # Books are at 1440 DPI. This is rendering at twice that size for - # readability when rendering to the screen. - scaledpi = 1440.0 - - filenames = os.listdir(pageDir) - filenames = sorted(filenames) - numfiles = len(filenames) - - xmllst = [] - elst = [] - - for filename in filenames: - # print ' ', filename - print ".", - fname = os.path.join(pageDir,filename) - flat_xml = convert2xml.fromData(dict, fname) - - # keep flat_xml for later svg processing - xmllst.append(flat_xml) - - if buildXML: - xname = os.path.join(xmlDir, filename.replace('.dat','.xml')) - file(xname, 'wb').write(convert2xml.getXML(dict, fname)) - - # first get the html - pagehtml, tocinfo = flatxml2html.convert2HTML(flat_xml, classlst, fname, bookDir, gd, fixedimage) - elst.append(tocinfo) - hlst.append(pagehtml) - - # finish up the html string and output it - hlst.append('\n\n') - htmlstr = "".join(hlst) - hlst = None - file(os.path.join(bookDir, htmlFileName), 'wb').write(htmlstr) - - print " " - print 'Extracting Table of Contents from Amazon OCR' - - # first create a table of contents file for the svg images - tlst = [] - tlst.append('\n') - tlst.append('\n') - tlst.append('') - tlst.append('\n') - tlst.append('' + meta_array['Title'] + '\n') - tlst.append('\n') - tlst.append('\n') - if 'ASIN' in meta_array: - tlst.append('\n') - if 'GUID' in meta_array: - tlst.append('\n') - tlst.append('\n') - tlst.append('\n') - - tlst.append('

Table of Contents

\n') - start = pageidnums[0] - if (raw): - startname = 'page%04d.svg' % start - else: - startname = 'page%04d.xhtml' % start - - tlst.append('

Start of Book

\n') - # build up a table of contents for the svg xhtml output - tocentries = "".join(elst) - elst = None - toclst = tocentries.split('\n') - toclst.pop() - for entry in toclst: - print entry - title, pagenum = entry.split('|') - id = pageidnums[int(pagenum)] - if (raw): - fname = 'page%04d.svg' % id - else: - fname = 'page%04d.xhtml' % id - tlst.append('

' + title + '

\n') - tlst.append('\n') - tlst.append('\n') - tochtml = "".join(tlst) - file(os.path.join(svgDir, 'toc.xhtml'), 'wb').write(tochtml) - - - # now create index_svg.xhtml that points to all required files - slst = [] - slst.append('\n') - slst.append('\n') - slst.append('') - slst.append('\n') - slst.append('' + meta_array['Title'] + '\n') - slst.append('\n') - slst.append('\n') - if 'ASIN' in meta_array: - slst.append('\n') - if 'GUID' in meta_array: - slst.append('\n') - slst.append('\n') - slst.append('\n') - - print "Building svg images of each book page" - slst.append('

List of Pages

\n') - slst.append('
\n') - idlst = sorted(pageIDMap.keys()) - numids = len(idlst) - cnt = len(idlst) - previd = None - for j in range(cnt): - pageid = idlst[j] - if j < cnt - 1: - nextid = idlst[j+1] - else: - nextid = None - print '.', - pagelst = pageIDMap[pageid] - flst = [] - for page in pagelst: - flst.append(xmllst[page]) - flat_svg = "".join(flst) - flst=None - svgxml = flatxml2svg.convert2SVG(gd, flat_svg, pageid, previd, nextid, svgDir, raw, meta_array, scaledpi) - if (raw) : - pfile = open(os.path.join(svgDir,'page%04d.svg' % pageid),'w') - slst.append('Page %d\n' % (pageid, pageid)) - else : - pfile = open(os.path.join(svgDir,'page%04d.xhtml' % pageid), 'w') - slst.append('Page %d\n' % (pageid, pageid)) - previd = pageid - pfile.write(svgxml) - pfile.close() - counter += 1 - slst.append('
\n') - slst.append('

Table of Contents

\n') - slst.append('\n\n') - svgindex = "".join(slst) - slst = None - file(os.path.join(bookDir, 'index_svg.xhtml'), 'wb').write(svgindex) - - print " " - - # build the opf file - opfname = os.path.join(bookDir, 'book.opf') - olst = [] - olst.append('\n') - olst.append('\n') - # adding metadata - olst.append(' \n') - if 'GUID' in meta_array: - olst.append(' ' + meta_array['GUID'] + '\n') - if 'ASIN' in meta_array: - olst.append(' ' + meta_array['ASIN'] + '\n') - if 'oASIN' in meta_array: - olst.append(' ' + meta_array['oASIN'] + '\n') - olst.append(' ' + meta_array['Title'] + '\n') - olst.append(' ' + meta_array['Authors'] + '\n') - olst.append(' en\n') - olst.append(' ' + meta_array['UpdateTime'] + '\n') - if isCover: - olst.append(' \n') - olst.append(' \n') - olst.append('\n') - olst.append(' \n') - olst.append(' \n') - # adding image files to manifest - filenames = os.listdir(imgDir) - filenames = sorted(filenames) - for filename in filenames: - imgname, imgext = os.path.splitext(filename) - if imgext == '.jpg': - imgext = 'jpeg' - if imgext == '.svg': - imgext = 'svg+xml' - olst.append(' \n') - if isCover: - olst.append(' \n') - olst.append('\n') - # adding spine - olst.append('\n \n\n') - if isCover: - olst.append(' \n') - olst.append(' \n') - olst.append(' \n') - olst.append('\n') - opfstr = "".join(olst) - olst = None - file(opfname, 'wb').write(opfstr) - - print 'Processing Complete' - - return 0 - -def usage(): - print "genbook.py generates a book from the extract Topaz Files" - print "Usage:" - print " genbook.py [-r] [-h [--fixed-image] " - print " " - print "Options:" - print " -h : help - print this usage message" - print " -r : generate raw svg files (not wrapped in xhtml)" - print " --fixed-image : genearate any Fixed Area as an svg image in the html" - print " " - - -def main(argv): - bookDir = '' - if len(argv) == 0: - argv = sys.argv - - try: - opts, args = getopt.getopt(argv[1:], "rh:",["fixed-image"]) - - except getopt.GetoptError, err: - print str(err) - usage() - return 1 - - if len(opts) == 0 and len(args) == 0 : - usage() - return 1 - - raw = 0 - fixedimage = True - for o, a in opts: - if o =="-h": - usage() - return 0 - if o =="-r": - raw = 1 - if o =="--fixed-image": - fixedimage = True - - bookDir = args[0] - - rv = generateBook(bookDir, raw, fixedimage) - return rv - - -if __name__ == '__main__': - sys.exit(main('')) diff --git a/Other_Tools/KindleBooks/lib/genxml.py b/Other_Tools/KindleBooks/lib/genxml.py deleted file mode 100644 index be542f0..0000000 --- a/Other_Tools/KindleBooks/lib/genxml.py +++ /dev/null @@ -1,145 +0,0 @@ -#! /usr/bin/python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab -# For use with Topaz Scripts Version 2.6 - -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, getopt - -# local routines -import convert2xml -import flatxml2html -import decode_meta - - -def usage(): - print 'Usage: ' - print ' ' - print ' genxml.py dict0000.dat unencryptedBookDir' - print ' ' - - - -def main(argv): - bookDir = '' - - if len(argv) == 0: - argv = sys.argv - - try: - opts, args = getopt.getopt(argv[1:], "h:") - - except getopt.GetoptError, err: - print str(err) - usage() - sys.exit(1) - - if len(opts) == 0 and len(args) == 0 : - usage() - sys.exit(1) - - for o, a in opts: - if o =="-h": - usage() - sys.exit(0) - - bookDir = args[0] - - if not os.path.exists(bookDir) : - print "Can not find directory with unencrypted book" - sys.exit(1) - - dictFile = os.path.join(bookDir,'dict0000.dat') - if not os.path.exists(dictFile) : - print "Can not find dict0000.dat file" - sys.exit(1) - - pageDir = os.path.join(bookDir,'page') - if not os.path.exists(pageDir) : - print "Can not find page directory in unencrypted book" - sys.exit(1) - - glyphsDir = os.path.join(bookDir,'glyphs') - if not os.path.exists(glyphsDir) : - print "Can not find glyphs directory in unencrypted book" - sys.exit(1) - - otherFile = os.path.join(bookDir,'other0000.dat') - if not os.path.exists(otherFile) : - print "Can not find other0000.dat in unencrypted book" - sys.exit(1) - - metaFile = os.path.join(bookDir,'metadata0000.dat') - if not os.path.exists(metaFile) : - print "Can not find metadata0000.dat in unencrypted book" - sys.exit(1) - - xmlDir = os.path.join(bookDir,'xml') - if not os.path.exists(xmlDir): - os.makedirs(xmlDir) - - - print 'Processing ... ' - - print ' ', 'metadata0000.dat' - fname = os.path.join(bookDir,'metadata0000.dat') - xname = os.path.join(xmlDir, 'metadata.txt') - metastr = decode_meta.getMetaData(fname) - file(xname, 'wb').write(metastr) - - print ' ', 'other0000.dat' - fname = os.path.join(bookDir,'other0000.dat') - xname = os.path.join(xmlDir, 'stylesheet.xml') - pargv=[] - pargv.append('convert2xml.py') - pargv.append(dictFile) - pargv.append(fname) - xmlstr = convert2xml.main(pargv) - file(xname, 'wb').write(xmlstr) - - filenames = os.listdir(pageDir) - filenames = sorted(filenames) - - for filename in filenames: - print ' ', filename - fname = os.path.join(pageDir,filename) - xname = os.path.join(xmlDir, filename.replace('.dat','.xml')) - pargv=[] - pargv.append('convert2xml.py') - pargv.append(dictFile) - pargv.append(fname) - xmlstr = convert2xml.main(pargv) - file(xname, 'wb').write(xmlstr) - - filenames = os.listdir(glyphsDir) - filenames = sorted(filenames) - - for filename in filenames: - print ' ', filename - fname = os.path.join(glyphsDir,filename) - xname = os.path.join(xmlDir, filename.replace('.dat','.xml')) - pargv=[] - pargv.append('convert2xml.py') - pargv.append(dictFile) - pargv.append(fname) - xmlstr = convert2xml.main(pargv) - file(xname, 'wb').write(xmlstr) - - - print 'Processing Complete' - - return 0 - -if __name__ == '__main__': - sys.exit(main('')) diff --git a/Other_Tools/KindleBooks/lib/getk4pcpids.py b/Other_Tools/KindleBooks/lib/getk4pcpids.py deleted file mode 100644 index cc8bcd4..0000000 --- a/Other_Tools/KindleBooks/lib/getk4pcpids.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/python -# -# This is a python script. You need a Python interpreter to run it. -# For example, ActiveState Python, which exists for windows. -# -# Changelog -# 1.00 - Initial version -# 1.01 - getPidList interface change - -__version__ = '1.01' - -import sys - -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) -sys.stdout=Unbuffered(sys.stdout) - -import os -import struct -import binascii -import kgenpids -import topazextract -import mobidedrm -from alfcrypto import Pukall_Cipher - -class DrmException(Exception): - pass - -def getK4PCpids(path_to_ebook): - # Return Kindle4PC PIDs. Assumes that the caller checked that we are not on Linux, which will raise an exception - - mobi = True - magic3 = file(path_to_ebook,'rb').read(3) - if magic3 == 'TPZ': - mobi = False - - if mobi: - mb = mobidedrm.MobiBook(path_to_ebook,False) - else: - mb = topazextract.TopazBook(path_to_ebook) - - md1, md2 = mb.getPIDMetaInfo() - - return kgenpids.getPidList(md1, md2) - - -def main(argv=sys.argv): - print ('getk4pcpids.py v%(__version__)s. ' - 'Copyright 2012 Apprentice Alf' % globals()) - - if len(argv)<2 or len(argv)>3: - print "Gets the possible book-specific PIDs from K4PC for a particular book" - print "Usage:" - print " %s []" % sys.argv[0] - return 1 - else: - infile = argv[1] - try: - pidlist = getK4PCpids(infile) - except DrmException, e: - print "Error: %s" % e - return 1 - pidstring = ','.join(pidlist) - print "Possible PIDs are: ", pidstring - if len(argv) is 3: - outfile = argv[2] - file(outfile, 'w').write(pidstring) - - return 0 - -if __name__ == "__main__": - sys.exit(main()) diff --git a/Other_Tools/KindleBooks/lib/k4mdumpkinfo.py b/Other_Tools/KindleBooks/lib/k4mdumpkinfo.py deleted file mode 100644 index da200ee..0000000 --- a/Other_Tools/KindleBooks/lib/k4mdumpkinfo.py +++ /dev/null @@ -1,333 +0,0 @@ -# engine to remove drm from Kindle for Mac books -# for personal use for archiving and converting your ebooks -# PLEASE DO NOT PIRATE! -# We want all authors and Publishers, and eBook stores to live long and prosperous lives -# -# it borrows heavily from works by CMBDTC, IHeartCabbages, skindle, -# unswindle, DiapDealer, some_updates and many many others - -from __future__ import with_statement - -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, csv, getopt -from struct import pack -from struct import unpack -import zlib - -# for handling sub processes -import subprocess -from subprocess import Popen, PIPE, STDOUT -import subasyncio -from subasyncio import Process - - -#Exception Handling -class K4MDEDRMError(Exception): - pass -class K4MDEDRMFatal(Exception): - pass - -# -# crypto routines -# -import hashlib - -def MD5(message): - ctx = hashlib.md5() - ctx.update(message) - return ctx.digest() - -def SHA1(message): - ctx = hashlib.sha1() - ctx.update(message) - return ctx.digest() - -def SHA256(message): - ctx = hashlib.sha256() - ctx.update(message) - return ctx.digest() - -# interface to needed routines in openssl's libcrypto -def _load_crypto_libcrypto(): - from ctypes import CDLL, byref, POINTER, c_void_p, c_char_p, c_int, c_long, \ - Structure, c_ulong, create_string_buffer, addressof, string_at, cast - from ctypes.util import find_library - - libcrypto = find_library('crypto') - if libcrypto is None: - raise K4MDEDRMError('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_cbc_encrypt = F(None, 'AES_cbc_encrypt',[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,c_int]) - - AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',[c_char_p, c_int, AES_KEY_p]) - - PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1', - [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p]) - - class LibCrypto(object): - def __init__(self): - self._blocksize = 0 - self._keyctx = None - self.iv = 0 - def set_decrypt_key(self, userkey, iv): - self._blocksize = len(userkey) - if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : - raise K4MDEDRMError('AES improper key used') - return - keyctx = self._keyctx = AES_KEY() - self.iv = iv - rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx) - if rv < 0: - raise K4MDEDRMError('Failed to initialize AES key') - def decrypt(self, data): - out = create_string_buffer(len(data)) - rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self.iv, 0) - if rv == 0: - raise K4MDEDRMError('AES decryption failed') - return out.raw - def keyivgen(self, passwd): - salt = '16743' - saltlen = 5 - passlen = len(passwd) - iter = 0x3e8 - keylen = 80 - out = create_string_buffer(keylen) - rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out) - return out.raw - return LibCrypto - -def _load_crypto(): - LibCrypto = None - try: - LibCrypto = _load_crypto_libcrypto() - except (ImportError, K4MDEDRMError): - pass - return LibCrypto - -LibCrypto = _load_crypto() - -# -# Utility Routines -# - -# uses a sub process to get the Hard Drive Serial Number using ioreg -# returns with the first found serial number in that class -def GetVolumeSerialNumber(): - sernum = os.getenv('MYSERIALNUMBER') - if sernum != None: - return sernum - cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver' - cmdline = cmdline.encode(sys.getfilesystemencoding()) - p = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False) - poll = p.wait('wait') - results = p.read() - reslst = results.split('\n') - cnt = len(reslst) - bsdname = None - sernum = None - foundIt = False - for j in xrange(cnt): - resline = reslst[j] - pp = resline.find('"Serial Number" = "') - if pp >= 0: - sernum = resline[pp+19:-1] - sernum = sernum.strip() - bb = resline.find('"BSD Name" = "') - if bb >= 0: - bsdname = resline[bb+14:-1] - bsdname = bsdname.strip() - if (bsdname == 'disk0') and (sernum != None): - foundIt = True - break - if not foundIt: - sernum = '9999999999' - return sernum - -# uses unix env to get username instead of using sysctlbyname -def GetUserName(): - username = os.getenv('USER') - return username - -MAX_PATH = 255 - -# -# start of Kindle specific routines -# - -global kindleDatabase - -# Various character maps used to decrypt books. Probably supposed to act as obfuscation -charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" -charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM" -charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" -charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" - -# Encode the bytes in data with the characters in map -def encode(data, map): - result = "" - for char in data: - value = ord(char) - Q = (value ^ 0x80) // len(map) - R = value % len(map) - result += map[Q] - result += map[R] - return result - -# Hash the bytes in data and then encode the digest with the characters in map -def encodeHash(data,map): - return encode(MD5(data),map) - -# Decode the string in data with the characters in map. Returns the decoded bytes -def decode(data,map): - result = "" - for i in range (0,len(data)-1,2): - high = map.find(data[i]) - low = map.find(data[i+1]) - if (high == -1) or (low == -1) : - break - value = (((high * len(map)) ^ 0x80) & 0xFF) + low - result += pack("B",value) - return result - -# implements an Pseudo Mac Version of Windows built-in Crypto routine -def CryptUnprotectData(encryptedData): - sp = GetVolumeSerialNumber() + '!@#' + GetUserName() - passwdData = encode(SHA256(sp),charMap1) - crp = LibCrypto() - key_iv = crp.keyivgen(passwdData) - key = key_iv[0:32] - iv = key_iv[32:48] - crp.set_decrypt_key(key,iv) - cleartext = crp.decrypt(encryptedData) - return cleartext - -# Locate and open the .kindle-info file -def openKindleInfo(): - home = os.getenv('HOME') - kinfopath = home + '/Library/Application Support/Amazon/Kindle/storage/.kindle-info' - if not os.path.exists(kinfopath): - kinfopath = home + '/Library/Application Support/Amazon/Kindle for Mac/storage/.kindle-info' - if not os.path.exists(kinfopath): - raise K4MDEDRMError('Error: .kindle-info file can not be found') - return open(kinfopath,'r') - -# Parse the Kindle.info file and return the records as a list of key-values -def parseKindleInfo(): - DB = {} - infoReader = openKindleInfo() - infoReader.read(1) - data = infoReader.read() - items = data.split('[') - for item in items: - splito = item.split(':') - DB[splito[0]] =splito[1] - return DB - -# Get a record from the Kindle.info file for the key "hashedKey" (already hashed and encoded). Return the decoded and decrypted record -def getKindleInfoValueForHash(hashedKey): - global kindleDatabase - encryptedValue = decode(kindleDatabase[hashedKey],charMap2) - cleartext = CryptUnprotectData(encryptedValue) - return decode(cleartext, charMap1) - -# Get a record from the Kindle.info file for the string in "key" (plaintext). Return the decoded and decrypted record -def getKindleInfoValueForKey(key): - return getKindleInfoValueForHash(encodeHash(key,charMap2)) - -# Find if the original string for a hashed/encoded string is known. If so return the original string othwise return an empty string. -def findNameForHash(hash): - names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"] - result = "" - for name in names: - if hash == encodeHash(name, charMap2): - result = name - break - return result - -# Print all the records from the kindle.info file (option -i) -def printKindleInfo(): - for record in kindleDatabase: - name = findNameForHash(record) - if name != "" : - print (name) - print ("--------------------------") - else : - print ("Unknown Record") - print getKindleInfoValueForHash(record) - print "\n" - -# -# PID generation routines -# - -# Returns two bit at offset from a bit field -def getTwoBitsFromBitField(bitField,offset): - byteNumber = offset // 4 - bitPosition = 6 - 2*(offset % 4) - return ord(bitField[byteNumber]) >> bitPosition & 3 - -# Returns the six bits at offset from a bit field -def getSixBitsFromBitField(bitField,offset): - offset *= 3 - value = (getTwoBitsFromBitField(bitField,offset) <<4) + (getTwoBitsFromBitField(bitField,offset+1) << 2) +getTwoBitsFromBitField(bitField,offset+2) - return value - -# 8 bits to six bits encoding from hash to generate PID string -def encodePID(hash): - global charMap3 - PID = "" - for position in range (0,8): - PID += charMap3[getSixBitsFromBitField(hash,position)] - return PID - - -# -# Main -# - -def main(argv=sys.argv): - global kindleDatabase - - kindleDatabase = None - - # - # Read the encrypted database - # - - try: - kindleDatabase = parseKindleInfo() - except Exception, message: - print(message) - - if kindleDatabase != None : - printKindleInfo() - - return 0 - -if __name__ == '__main__': - sys.exit(main()) diff --git a/Other_Tools/KindleBooks/lib/k4mobidedrm.py b/Other_Tools/KindleBooks/lib/k4mobidedrm.py deleted file mode 100644 index 717b0d0..0000000 --- a/Other_Tools/KindleBooks/lib/k4mobidedrm.py +++ /dev/null @@ -1,238 +0,0 @@ -#!/usr/bin/env python - -from __future__ import with_statement - -# engine to remove drm from Kindle for Mac and Kindle for PC books -# for personal use for archiving and converting your ebooks - -# PLEASE DO NOT PIRATE EBOOKS! - -# We want all authors and publishers, and eBook stores to live -# long and prosperous lives but at the same time we just want to -# be able to read OUR books on whatever device we want and to keep -# readable for a long, long time - -# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle, -# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates -# and many many others - - -__version__ = '4.4' - -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 -import os, csv, getopt -import string -import re -import traceback -import time - -buildXML = False - -class DrmException(Exception): - pass - -if 'calibre' in sys.modules: - inCalibre = True -else: - inCalibre = False - -if inCalibre: - from calibre_plugins.k4mobidedrm import mobidedrm - from calibre_plugins.k4mobidedrm import topazextract - from calibre_plugins.k4mobidedrm import kgenpids -else: - import mobidedrm - import topazextract - import kgenpids - - -# cleanup bytestring filenames -# borrowed from calibre from calibre/src/calibre/__init__.py -# added in removal of non-printing chars -# and removal of . at start -# convert underscores to spaces (we're OK with spaces in file names) -def cleanup_name(name): - _filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+/]') - substitute='_' - one = ''.join(char for char in name if char in string.printable) - one = _filename_sanitize.sub(substitute, one) - one = re.sub(r'\s', ' ', one).strip() - one = re.sub(r'^\.+$', '_', one) - one = one.replace('..', substitute) - # Windows doesn't like path components that end with a period - if one.endswith('.'): - one = one[:-1]+substitute - # Mac and Unix don't like file names that begin with a full stop - if len(one) > 0 and one[0] == '.': - one = substitute+one[1:] - one = one.replace('_',' ') - return one - -def decryptBook(infile, outdir, k4, kInfoFiles, serials, pids): - global buildXML - - - # handle the obvious cases at the beginning - if not os.path.isfile(infile): - print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: Input file does not exist" - return 1 - - starttime = time.time() - print "Starting decryptBook routine." - - - mobi = True - magic3 = file(infile,'rb').read(3) - if magic3 == 'TPZ': - mobi = False - - bookname = os.path.splitext(os.path.basename(infile))[0] - - if mobi: - mb = mobidedrm.MobiBook(infile) - else: - mb = topazextract.TopazBook(infile) - - title = mb.getBookTitle() - print "Processing Book: ", title - filenametitle = cleanup_name(title) - outfilename = cleanup_name(bookname) - - # generate 'sensible' filename, that will sort with the original name, - # but is close to the name from the file. - outlength = len(outfilename) - comparelength = min(8,min(outlength,len(filenametitle))) - copylength = min(max(outfilename.find(' '),8),len(outfilename)) - if outlength==0: - outfilename = filenametitle - elif comparelength > 0: - if outfilename[:comparelength] == filenametitle[:comparelength]: - outfilename = filenametitle - else: - outfilename = outfilename[:copylength] + " " + filenametitle - - # avoid excessively long file names - if len(outfilename)>150: - outfilename = outfilename[:150] - - # build pid list - md1, md2 = mb.getPIDMetaInfo() - pids.extend(kgenpids.getPidList(md1, md2, k4, serials, kInfoFiles)) - - print "Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(pids)) - - - try: - mb.processBook(pids) - - except mobidedrm.DrmException, e: - print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: " + str(e) + "\nDRM Removal Failed.\n" - print "Failed to decrypted book after {0:.1f} seconds".format(time.time()-starttime) - return 1 - except topazextract.TpzDRMError, e: - print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: " + str(e) + "\nDRM Removal Failed.\n" - print "Failed to decrypted book after {0:.1f} seconds".format(time.time()-starttime) - return 1 - except Exception, e: - print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: " + str(e) + "\nDRM Removal Failed.\n" - print "Failed to decrypted book after {0:.1f} seconds".format(time.time()-starttime) - return 1 - - print "Successfully decrypted book after {0:.1f} seconds".format(time.time()-starttime) - - if mobi: - if mb.getPrintReplica(): - outfile = os.path.join(outdir, outfilename + '_nodrm' + '.azw4') - elif mb.getMobiVersion() >= 8: - outfile = os.path.join(outdir, outfilename + '_nodrm' + '.azw3') - else: - outfile = os.path.join(outdir, outfilename + '_nodrm' + '.mobi') - mb.getMobiFile(outfile) - print "Saved decrypted book {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename + '_nodrm') - return 0 - - # topaz: - print " Creating NoDRM HTMLZ Archive" - zipname = os.path.join(outdir, outfilename + '_nodrm' + '.htmlz') - mb.getHTMLZip(zipname) - - print " Creating SVG ZIP Archive" - zipname = os.path.join(outdir, outfilename + '_SVG' + '.zip') - mb.getSVGZip(zipname) - - if buildXML: - print " Creating XML ZIP Archive" - zipname = os.path.join(outdir, outfilename + '_XML' + '.zip') - mb.getXMLZip(zipname) - - # remove internal temporary directory of Topaz pieces - mb.cleanup() - print "Saved decrypted Topaz book parts after {0:.1f} seconds".format(time.time()-starttime) - return 0 - - -def usage(progname): - print "Removes DRM protection from K4PC/M, Kindle, Mobi and Topaz ebooks" - print "Usage:" - print " %s [-k ] [-p ] [-s ] " % progname - -# -# Main -# -def main(argv=sys.argv): - progname = os.path.basename(argv[0]) - - k4 = False - kInfoFiles = [] - serials = [] - pids = [] - - print ('K4MobiDeDrm v%(__version__)s ' - 'provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc .' % globals()) - - try: - opts, args = getopt.getopt(sys.argv[1:], "k:p:s:") - except getopt.GetoptError, err: - print str(err) - usage(progname) - sys.exit(2) - if len(args)<2: - usage(progname) - sys.exit(2) - - for o, a in opts: - if o == "-k": - if a == None : - raise DrmException("Invalid parameter for -k") - kInfoFiles.append(a) - if o == "-p": - if a == None : - raise DrmException("Invalid parameter for -p") - pids = a.split(',') - if o == "-s": - if a == None : - raise DrmException("Invalid parameter for -s") - serials = a.split(',') - - # try with built in Kindle Info files - k4 = True - if sys.platform.startswith('linux'): - k4 = False - kInfoFiles = None - infile = args[0] - outdir = args[1] - return decryptBook(infile, outdir, k4, kInfoFiles, serials, pids) - - -if __name__ == '__main__': - sys.stdout=Unbuffered(sys.stdout) - sys.exit(main()) diff --git a/Other_Tools/KindleBooks/lib/k4mutils.py b/Other_Tools/KindleBooks/lib/k4mutils.py deleted file mode 100644 index 1fc08cb..0000000 --- a/Other_Tools/KindleBooks/lib/k4mutils.py +++ /dev/null @@ -1,730 +0,0 @@ -# standlone set of Mac OSX specific routines needed for KindleBooks - -from __future__ import with_statement - -import sys -import os -import os.path -import re -import copy -import subprocess -from struct import pack, unpack, unpack_from - -class DrmException(Exception): - pass - - -# interface to needed routines in openssl's libcrypto -def _load_crypto_libcrypto(): - from ctypes import CDLL, byref, POINTER, c_void_p, c_char_p, c_int, c_long, \ - Structure, c_ulong, create_string_buffer, addressof, string_at, cast - from ctypes.util import find_library - - libcrypto = find_library('crypto') - if libcrypto is None: - raise DrmException('libcrypto not found') - libcrypto = CDLL(libcrypto) - - # From OpenSSL's crypto aes header - # - # AES_ENCRYPT 1 - # AES_DECRYPT 0 - # AES_MAXNR 14 (in bytes) - # AES_BLOCK_SIZE 16 (in bytes) - # - # struct aes_key_st { - # unsigned long rd_key[4 *(AES_MAXNR + 1)]; - # int rounds; - # }; - # typedef struct aes_key_st AES_KEY; - # - # int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key); - # - # note: the ivec string, and output buffer are both mutable - # void AES_cbc_encrypt(const unsigned char *in, unsigned char *out, - # const unsigned long length, const AES_KEY *key, unsigned char *ivec, const int enc); - - 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_cbc_encrypt = F(None, 'AES_cbc_encrypt',[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,c_int]) - - AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',[c_char_p, c_int, AES_KEY_p]) - - # From OpenSSL's Crypto evp/p5_crpt2.c - # - # int PKCS5_PBKDF2_HMAC_SHA1(const char *pass, int passlen, - # const unsigned char *salt, int saltlen, int iter, - # int keylen, unsigned char *out); - - PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1', - [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p]) - - class LibCrypto(object): - def __init__(self): - self._blocksize = 0 - self._keyctx = None - self._iv = 0 - - def set_decrypt_key(self, userkey, iv): - self._blocksize = len(userkey) - if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : - raise DrmException('AES improper key used') - return - keyctx = self._keyctx = AES_KEY() - self._iv = iv - self._userkey = userkey - rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx) - if rv < 0: - raise DrmException('Failed to initialize AES key') - - def decrypt(self, data): - out = create_string_buffer(len(data)) - mutable_iv = create_string_buffer(self._iv, len(self._iv)) - keyctx = self._keyctx - rv = AES_cbc_encrypt(data, out, len(data), keyctx, mutable_iv, 0) - if rv == 0: - raise DrmException('AES decryption failed') - return out.raw - - def keyivgen(self, passwd, salt, iter, keylen): - saltlen = len(salt) - passlen = len(passwd) - out = create_string_buffer(keylen) - rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out) - return out.raw - return LibCrypto - -def _load_crypto(): - LibCrypto = None - try: - LibCrypto = _load_crypto_libcrypto() - except (ImportError, DrmException): - pass - return LibCrypto - -LibCrypto = _load_crypto() - -# -# Utility Routines -# - -# crypto digestroutines -import hashlib - -def MD5(message): - ctx = hashlib.md5() - ctx.update(message) - return ctx.digest() - -def SHA1(message): - ctx = hashlib.sha1() - ctx.update(message) - return ctx.digest() - -def SHA256(message): - ctx = hashlib.sha256() - ctx.update(message) - return ctx.digest() - -# Various character maps used to decrypt books. Probably supposed to act as obfuscation -charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" -charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM" - -# For kinf approach of K4Mac 1.6.X or later -# On K4PC charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE" -# For Mac they seem to re-use charMap2 here -charMap5 = charMap2 - -# new in K4M 1.9.X -testMap8 = "YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD" - - -def encode(data, map): - result = "" - for char in data: - value = ord(char) - Q = (value ^ 0x80) // len(map) - R = value % len(map) - result += map[Q] - result += map[R] - return result - -# Hash the bytes in data and then encode the digest with the characters in map -def encodeHash(data,map): - return encode(MD5(data),map) - -# Decode the string in data with the characters in map. Returns the decoded bytes -def decode(data,map): - result = "" - for i in range (0,len(data)-1,2): - high = map.find(data[i]) - low = map.find(data[i+1]) - if (high == -1) or (low == -1) : - break - value = (((high * len(map)) ^ 0x80) & 0xFF) + low - result += pack("B",value) - return result - -# For K4M 1.6.X and later -# generate table of prime number less than or equal to int n -def primes(n): - if n==2: return [2] - elif n<2: return [] - s=range(3,n+1,2) - mroot = n ** 0.5 - half=(n+1)/2-1 - i=0 - m=3 - while m <= mroot: - if s[i]: - j=(m*m-3)/2 - s[j]=0 - while j 7: - print('Using Munged MAC Address for ID: '+mungedmac) - return mungedmac - sernum = GetVolumeSerialNumber() - if len(sernum) > 7: - print('Using Volume Serial Number for ID: '+sernum) - return sernum - diskpart = GetUserHomeAppSupKindleDirParitionName() - uuidnum = GetDiskPartitionUUID(diskpart) - if len(uuidnum) > 7: - print('Using Disk Partition UUID for ID: '+uuidnum) - return uuidnum - mungedmac = GetMACAddressMunged() - if len(mungedmac) > 7: - print('Using Munged MAC Address for ID: '+mungedmac) - return mungedmac - print('Using Fixed constant 9999999999 for ID.') - return '9999999999' - - -# implements an Pseudo Mac Version of Windows built-in Crypto routine -# used by Kindle for Mac versions < 1.6.0 -class CryptUnprotectData(object): - def __init__(self): - sernum = GetVolumeSerialNumber() - if sernum == '': - sernum = '9999999999' - sp = sernum + '!@#' + GetUserName() - passwdData = encode(SHA256(sp),charMap1) - salt = '16743' - self.crp = LibCrypto() - iter = 0x3e8 - keylen = 0x80 - key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen) - self.key = key_iv[0:32] - self.iv = key_iv[32:48] - self.crp.set_decrypt_key(self.key, self.iv) - - def decrypt(self, encryptedData): - cleartext = self.crp.decrypt(encryptedData) - cleartext = decode(cleartext,charMap1) - return cleartext - - -# implements an Pseudo Mac Version of Windows built-in Crypto routine -# used for Kindle for Mac Versions >= 1.6.0 -class CryptUnprotectDataV2(object): - def __init__(self): - sp = GetUserName() + ':&%:' + GetIDString() - passwdData = encode(SHA256(sp),charMap5) - # salt generation as per the code - salt = 0x0512981d * 2 * 1 * 1 - salt = str(salt) + GetUserName() - salt = encode(salt,charMap5) - self.crp = LibCrypto() - iter = 0x800 - keylen = 0x400 - key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen) - self.key = key_iv[0:32] - self.iv = key_iv[32:48] - self.crp.set_decrypt_key(self.key, self.iv) - - def decrypt(self, encryptedData): - cleartext = self.crp.decrypt(encryptedData) - cleartext = decode(cleartext, charMap5) - return cleartext - - -# unprotect the new header blob in .kinf2011 -# used in Kindle for Mac Version >= 1.9.0 -def UnprotectHeaderData(encryptedData): - passwdData = 'header_key_data' - salt = 'HEADER.2011' - iter = 0x80 - keylen = 0x100 - crp = LibCrypto() - key_iv = crp.keyivgen(passwdData, salt, iter, keylen) - key = key_iv[0:32] - iv = key_iv[32:48] - crp.set_decrypt_key(key,iv) - cleartext = crp.decrypt(encryptedData) - return cleartext - - -# implements an Pseudo Mac Version of Windows built-in Crypto routine -# used for Kindle for Mac Versions >= 1.9.0 -class CryptUnprotectDataV3(object): - def __init__(self, entropy): - sp = GetUserName() + '+@#$%+' + GetIDString() - passwdData = encode(SHA256(sp),charMap2) - salt = entropy - self.crp = LibCrypto() - iter = 0x800 - keylen = 0x400 - key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen) - self.key = key_iv[0:32] - self.iv = key_iv[32:48] - self.crp.set_decrypt_key(self.key, self.iv) - - def decrypt(self, encryptedData): - cleartext = self.crp.decrypt(encryptedData) - cleartext = decode(cleartext, charMap2) - return cleartext - - -# Locate the .kindle-info files -def getKindleInfoFiles(): - # file searches can take a long time on some systems, so just look in known specific places. - kInfoFiles=[] - found = False - home = os.getenv('HOME') - # check for .kinf2011 file in new location (App Store Kindle for Mac) - testpath = home + '/Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/storage/.kinf2011' - if os.path.isfile(testpath): - kInfoFiles.append(testpath) - print('Found k4Mac kinf2011 file: ' + testpath) - found = True - # check for .kinf2011 files - testpath = home + '/Library/Application Support/Kindle/storage/.kinf2011' - if os.path.isfile(testpath): - kInfoFiles.append(testpath) - print('Found k4Mac kinf2011 file: ' + testpath) - found = True - # check for .rainier-2.1.1-kinf files - testpath = home + '/Library/Application Support/Kindle/storage/.rainier-2.1.1-kinf' - if os.path.isfile(testpath): - kInfoFiles.append(testpath) - print('Found k4Mac rainier file: ' + testpath) - found = True - # check for .rainier-2.1.1-kinf files - testpath = home + '/Library/Application Support/Kindle/storage/.kindle-info' - if os.path.isfile(testpath): - kInfoFiles.append(testpath) - print('Found k4Mac kindle-info file: ' + testpath) - found = True - if not found: - print('No k4Mac kindle-info/rainier/kinf2011 files have been found.') - return kInfoFiles - -# determine type of kindle info provided and return a -# database of keynames and values -def getDBfromFile(kInfoFile): - names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber", "max_date", "SIGVERIF"] - DB = {} - cnt = 0 - infoReader = open(kInfoFile, 'r') - hdr = infoReader.read(1) - data = infoReader.read() - - if data.find('[') != -1 : - - # older style kindle-info file - cud = CryptUnprotectData() - items = data.split('[') - for item in items: - if item != '': - keyhash, rawdata = item.split(':') - keyname = "unknown" - for name in names: - if encodeHash(name,charMap2) == keyhash: - keyname = name - break - if keyname == "unknown": - keyname = keyhash - encryptedValue = decode(rawdata,charMap2) - cleartext = cud.decrypt(encryptedValue) - DB[keyname] = cleartext - cnt = cnt + 1 - if cnt == 0: - DB = None - return DB - - if hdr == '/': - - # else newer style .kinf file used by K4Mac >= 1.6.0 - # the .kinf file uses "/" to separate it into records - # so remove the trailing "/" to make it easy to use split - data = data[:-1] - items = data.split('/') - cud = CryptUnprotectDataV2() - - # loop through the item records until all are processed - while len(items) > 0: - - # get the first item record - item = items.pop(0) - - # the first 32 chars of the first record of a group - # is the MD5 hash of the key name encoded by charMap5 - keyhash = item[0:32] - keyname = "unknown" - - # the raw keyhash string is also used to create entropy for the actual - # CryptProtectData Blob that represents that keys contents - # "entropy" not used for K4Mac only K4PC - # entropy = SHA1(keyhash) - - # the remainder of the first record when decoded with charMap5 - # has the ':' split char followed by the string representation - # of the number of records that follow - # and make up the contents - srcnt = decode(item[34:],charMap5) - rcnt = int(srcnt) - - # read and store in rcnt records of data - # that make up the contents value - edlst = [] - for i in xrange(rcnt): - item = items.pop(0) - edlst.append(item) - - keyname = "unknown" - for name in names: - if encodeHash(name,charMap5) == keyhash: - keyname = name - break - if keyname == "unknown": - keyname = keyhash - - # the charMap5 encoded contents data has had a length - # of chars (always odd) cut off of the front and moved - # to the end to prevent decoding using charMap5 from - # working properly, and thereby preventing the ensuing - # CryptUnprotectData call from succeeding. - - # The offset into the charMap5 encoded contents seems to be: - # len(contents) - largest prime number less than or equal to int(len(content)/3) - # (in other words split "about" 2/3rds of the way through) - - # move first offsets chars to end to align for decode by charMap5 - encdata = "".join(edlst) - contlen = len(encdata) - - # now properly split and recombine - # by moving noffset chars from the start of the - # string to the end of the string - noffset = contlen - primes(int(contlen/3))[-1] - pfx = encdata[0:noffset] - encdata = encdata[noffset:] - encdata = encdata + pfx - - # decode using charMap5 to get the CryptProtect Data - encryptedValue = decode(encdata,charMap5) - cleartext = cud.decrypt(encryptedValue) - DB[keyname] = cleartext - cnt = cnt + 1 - - if cnt == 0: - DB = None - return DB - - # the latest .kinf2011 version for K4M 1.9.1 - # put back the hdr char, it is needed - data = hdr + data - data = data[:-1] - items = data.split('/') - - # the headerblob is the encrypted information needed to build the entropy string - headerblob = items.pop(0) - encryptedValue = decode(headerblob, charMap1) - cleartext = UnprotectHeaderData(encryptedValue) - - # now extract the pieces in the same way - # this version is different from K4PC it scales the build number by multipying by 735 - pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) - for m in re.finditer(pattern, cleartext): - entropy = str(int(m.group(2)) * 0x2df) + m.group(4) - - cud = CryptUnprotectDataV3(entropy) - - # loop through the item records until all are processed - while len(items) > 0: - - # get the first item record - item = items.pop(0) - - # the first 32 chars of the first record of a group - # is the MD5 hash of the key name encoded by charMap5 - keyhash = item[0:32] - keyname = "unknown" - - # unlike K4PC the keyhash is not used in generating entropy - # entropy = SHA1(keyhash) + added_entropy - # entropy = added_entropy - - # the remainder of the first record when decoded with charMap5 - # has the ':' split char followed by the string representation - # of the number of records that follow - # and make up the contents - srcnt = decode(item[34:],charMap5) - rcnt = int(srcnt) - - # read and store in rcnt records of data - # that make up the contents value - edlst = [] - for i in xrange(rcnt): - item = items.pop(0) - edlst.append(item) - - keyname = "unknown" - for name in names: - if encodeHash(name,testMap8) == keyhash: - keyname = name - break - if keyname == "unknown": - keyname = keyhash - - # the testMap8 encoded contents data has had a length - # of chars (always odd) cut off of the front and moved - # to the end to prevent decoding using testMap8 from - # working properly, and thereby preventing the ensuing - # CryptUnprotectData call from succeeding. - - # The offset into the testMap8 encoded contents seems to be: - # len(contents) - largest prime number less than or equal to int(len(content)/3) - # (in other words split "about" 2/3rds of the way through) - - # move first offsets chars to end to align for decode by testMap8 - encdata = "".join(edlst) - contlen = len(encdata) - - # now properly split and recombine - # by moving noffset chars from the start of the - # string to the end of the string - noffset = contlen - primes(int(contlen/3))[-1] - pfx = encdata[0:noffset] - encdata = encdata[noffset:] - encdata = encdata + pfx - - # decode using testMap8 to get the CryptProtect Data - encryptedValue = decode(encdata,testMap8) - cleartext = cud.decrypt(encryptedValue) - # print keyname - # print cleartext - DB[keyname] = cleartext - cnt = cnt + 1 - - if cnt == 0: - DB = None - return DB diff --git a/Other_Tools/KindleBooks/lib/k4pcutils.py b/Other_Tools/KindleBooks/lib/k4pcutils.py deleted file mode 100644 index 9f9ca07..0000000 --- a/Other_Tools/KindleBooks/lib/k4pcutils.py +++ /dev/null @@ -1,455 +0,0 @@ -#!/usr/bin/env python -# K4PC Windows specific routines - -from __future__ import with_statement - -import sys, os, re -from struct import pack, unpack, unpack_from - -from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \ - create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \ - string_at, Structure, c_void_p, cast - -import _winreg as winreg -MAX_PATH = 255 -kernel32 = windll.kernel32 -advapi32 = windll.advapi32 -crypt32 = windll.crypt32 - -import traceback - -# crypto digestroutines -import hashlib - -def MD5(message): - ctx = hashlib.md5() - ctx.update(message) - return ctx.digest() - -def SHA1(message): - ctx = hashlib.sha1() - ctx.update(message) - return ctx.digest() - -def SHA256(message): - ctx = hashlib.sha256() - ctx.update(message) - return ctx.digest() - -# For K4PC 1.9.X -# use routines in alfcrypto: -# AES_cbc_encrypt -# AES_set_decrypt_key -# PKCS5_PBKDF2_HMAC_SHA1 - -from alfcrypto import AES_CBC, KeyIVGen - -def UnprotectHeaderData(encryptedData): - passwdData = 'header_key_data' - salt = 'HEADER.2011' - iter = 0x80 - keylen = 0x100 - key_iv = KeyIVGen().pbkdf2(passwdData, salt, iter, keylen) - key = key_iv[0:32] - iv = key_iv[32:48] - aes=AES_CBC() - aes.set_decrypt_key(key, iv) - cleartext = aes.decrypt(encryptedData) - return cleartext - - -# simple primes table (<= n) calculator -def primes(n): - if n==2: return [2] - elif n<2: return [] - s=range(3,n+1,2) - mroot = n ** 0.5 - half=(n+1)/2-1 - i=0 - m=3 - while m <= mroot: - if s[i]: - j=(m*m-3)/2 - s[j]=0 - while j 0: - - # get the first item record - item = items.pop(0) - - # the first 32 chars of the first record of a group - # is the MD5 hash of the key name encoded by charMap5 - keyhash = item[0:32] - - # the raw keyhash string is used to create entropy for the actual - # CryptProtectData Blob that represents that keys contents - entropy = SHA1(keyhash) - - # the remainder of the first record when decoded with charMap5 - # has the ':' split char followed by the string representation - # of the number of records that follow - # and make up the contents - srcnt = decode(item[34:],charMap5) - rcnt = int(srcnt) - - # read and store in rcnt records of data - # that make up the contents value - edlst = [] - for i in xrange(rcnt): - item = items.pop(0) - edlst.append(item) - - keyname = "unknown" - for name in names: - if encodeHash(name,charMap5) == keyhash: - keyname = name - break - if keyname == "unknown": - keyname = keyhash - # the charMap5 encoded contents data has had a length - # of chars (always odd) cut off of the front and moved - # to the end to prevent decoding using charMap5 from - # working properly, and thereby preventing the ensuing - # CryptUnprotectData call from succeeding. - - # The offset into the charMap5 encoded contents seems to be: - # len(contents)-largest prime number <= int(len(content)/3) - # (in other words split "about" 2/3rds of the way through) - - # move first offsets chars to end to align for decode by charMap5 - encdata = "".join(edlst) - contlen = len(encdata) - noffset = contlen - primes(int(contlen/3))[-1] - - # now properly split and recombine - # by moving noffset chars from the start of the - # string to the end of the string - pfx = encdata[0:noffset] - encdata = encdata[noffset:] - encdata = encdata + pfx - - # decode using Map5 to get the CryptProtect Data - encryptedValue = decode(encdata,charMap5) - DB[keyname] = CryptUnprotectData(encryptedValue, entropy, 1) - cnt = cnt + 1 - - if cnt == 0: - DB = None - return DB - - # else newest .kinf2011 style .kinf file - # the .kinf file uses "/" to separate it into records - # so remove the trailing "/" to make it easy to use split - # need to put back the first char read because it it part - # of the added entropy blob - data = hdr + data[:-1] - items = data.split('/') - - # starts with and encoded and encrypted header blob - headerblob = items.pop(0) - encryptedValue = decode(headerblob, testMap1) - cleartext = UnprotectHeaderData(encryptedValue) - # now extract the pieces that form the added entropy - pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) - for m in re.finditer(pattern, cleartext): - added_entropy = m.group(2) + m.group(4) - - - # loop through the item records until all are processed - while len(items) > 0: - - # get the first item record - item = items.pop(0) - - # the first 32 chars of the first record of a group - # is the MD5 hash of the key name encoded by charMap5 - keyhash = item[0:32] - - # the sha1 of raw keyhash string is used to create entropy along - # with the added entropy provided above from the headerblob - entropy = SHA1(keyhash) + added_entropy - - # the remainder of the first record when decoded with charMap5 - # has the ':' split char followed by the string representation - # of the number of records that follow - # and make up the contents - srcnt = decode(item[34:],charMap5) - rcnt = int(srcnt) - - # read and store in rcnt records of data - # that make up the contents value - edlst = [] - for i in xrange(rcnt): - item = items.pop(0) - edlst.append(item) - - # key names now use the new testMap8 encoding - keyname = "unknown" - for name in names: - if encodeHash(name,testMap8) == keyhash: - keyname = name - break - - # the testMap8 encoded contents data has had a length - # of chars (always odd) cut off of the front and moved - # to the end to prevent decoding using testMap8 from - # working properly, and thereby preventing the ensuing - # CryptUnprotectData call from succeeding. - - # The offset into the testMap8 encoded contents seems to be: - # len(contents)-largest prime number <= int(len(content)/3) - # (in other words split "about" 2/3rds of the way through) - - # move first offsets chars to end to align for decode by testMap8 - # by moving noffset chars from the start of the - # string to the end of the string - encdata = "".join(edlst) - contlen = len(encdata) - noffset = contlen - primes(int(contlen/3))[-1] - pfx = encdata[0:noffset] - encdata = encdata[noffset:] - encdata = encdata + pfx - - # decode using new testMap8 to get the original CryptProtect Data - encryptedValue = decode(encdata,testMap8) - cleartext = CryptUnprotectData(encryptedValue, entropy, 1) - DB[keyname] = cleartext - cnt = cnt + 1 - - if cnt == 0: - DB = None - return DB diff --git a/Other_Tools/KindleBooks/lib/kgenpids.py b/Other_Tools/KindleBooks/lib/kgenpids.py deleted file mode 100644 index b0fbaa4..0000000 --- a/Other_Tools/KindleBooks/lib/kgenpids.py +++ /dev/null @@ -1,274 +0,0 @@ -#!/usr/bin/env python - -from __future__ import with_statement -import sys -import os, csv -import binascii -import zlib -import re -from struct import pack, unpack, unpack_from - -class DrmException(Exception): - pass - -global charMap1 -global charMap3 -global charMap4 - -if 'calibre' in sys.modules: - inCalibre = True -else: - inCalibre = False - -if inCalibre: - if sys.platform.startswith('win'): - from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString - - if sys.platform.startswith('darwin'): - from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString -else: - if sys.platform.startswith('win'): - from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString - - if sys.platform.startswith('darwin'): - from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString - - -charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" -charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" -charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" - -# crypto digestroutines -import hashlib - -def MD5(message): - ctx = hashlib.md5() - ctx.update(message) - return ctx.digest() - -def SHA1(message): - ctx = hashlib.sha1() - ctx.update(message) - return ctx.digest() - - -# Encode the bytes in data with the characters in map -def encode(data, map): - result = "" - for char in data: - value = ord(char) - Q = (value ^ 0x80) // len(map) - R = value % len(map) - result += map[Q] - result += map[R] - return result - -# Hash the bytes in data and then encode the digest with the characters in map -def encodeHash(data,map): - return encode(MD5(data),map) - -# Decode the string in data with the characters in map. Returns the decoded bytes -def decode(data,map): - result = "" - for i in range (0,len(data)-1,2): - high = map.find(data[i]) - low = map.find(data[i+1]) - if (high == -1) or (low == -1) : - break - value = (((high * len(map)) ^ 0x80) & 0xFF) + low - result += pack("B",value) - return result - -# -# PID generation routines -# - -# Returns two bit at offset from a bit field -def getTwoBitsFromBitField(bitField,offset): - byteNumber = offset // 4 - bitPosition = 6 - 2*(offset % 4) - return ord(bitField[byteNumber]) >> bitPosition & 3 - -# Returns the six bits at offset from a bit field -def getSixBitsFromBitField(bitField,offset): - offset *= 3 - value = (getTwoBitsFromBitField(bitField,offset) <<4) + (getTwoBitsFromBitField(bitField,offset+1) << 2) +getTwoBitsFromBitField(bitField,offset+2) - return value - -# 8 bits to six bits encoding from hash to generate PID string -def encodePID(hash): - global charMap3 - PID = "" - for position in range (0,8): - PID += charMap3[getSixBitsFromBitField(hash,position)] - return PID - -# Encryption table used to generate the device PID -def generatePidEncryptionTable() : - table = [] - for counter1 in range (0,0x100): - value = counter1 - for counter2 in range (0,8): - if (value & 1 == 0) : - value = value >> 1 - else : - value = value >> 1 - value = value ^ 0xEDB88320 - table.append(value) - return table - -# Seed value used to generate the device PID -def generatePidSeed(table,dsn) : - value = 0 - for counter in range (0,4) : - index = (ord(dsn[counter]) ^ value) &0xFF - value = (value >> 8) ^ table[index] - return value - -# Generate the device PID -def generateDevicePID(table,dsn,nbRoll): - global charMap4 - seed = generatePidSeed(table,dsn) - pidAscii = "" - pid = [(seed >>24) &0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF,(seed>>24) & 0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF] - index = 0 - for counter in range (0,nbRoll): - pid[index] = pid[index] ^ ord(dsn[counter]) - index = (index+1) %8 - for counter in range (0,8): - index = ((((pid[counter] >>5) & 3) ^ pid[counter]) & 0x1f) + (pid[counter] >> 7) - pidAscii += charMap4[index] - return pidAscii - -def crc32(s): - return (~binascii.crc32(s,-1))&0xFFFFFFFF - -# convert from 8 digit PID to 10 digit PID with checksum -def checksumPid(s): - global charMap4 - crc = crc32(s) - crc = crc ^ (crc >> 16) - res = s - l = len(charMap4) - for i in (0,1): - b = crc & 0xff - pos = (b // l) ^ (b % l) - res += charMap4[pos%l] - crc >>= 8 - return res - - -# old kindle serial number to fixed pid -def pidFromSerial(s, l): - global charMap4 - crc = crc32(s) - arr1 = [0]*l - for i in xrange(len(s)): - arr1[i%l] ^= ord(s[i]) - crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff] - for i in xrange(l): - arr1[i] ^= crc_bytes[i&3] - pid = "" - for i in xrange(l): - b = arr1[i] & 0xff - pid+=charMap4[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))] - return pid - - -# Parse the EXTH header records and use the Kindle serial number to calculate the book pid. -def getKindlePid(pidlst, rec209, token, serialnum): - # Compute book PID - pidHash = SHA1(serialnum+rec209+token) - bookPID = encodePID(pidHash) - bookPID = checksumPid(bookPID) - pidlst.append(bookPID) - - # compute fixed pid for old pre 2.5 firmware update pid as well - bookPID = pidFromSerial(serialnum, 7) + "*" - bookPID = checksumPid(bookPID) - pidlst.append(bookPID) - - return pidlst - - -# parse the Kindleinfo file to calculate the book pid. - -keynames = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"] - -def getK4Pids(pidlst, rec209, token, kInfoFile): - global charMap1 - kindleDatabase = None - try: - kindleDatabase = getDBfromFile(kInfoFile) - except Exception, message: - print(message) - kindleDatabase = None - pass - - if kindleDatabase == None : - return pidlst - - try: - # Get the Mazama Random number - MazamaRandomNumber = kindleDatabase["MazamaRandomNumber"] - - # Get the kindle account token - kindleAccountToken = kindleDatabase["kindle.account.tokens"] - except KeyError: - print "Keys not found in " + kInfoFile - return pidlst - - # Get the ID string used - encodedIDString = encodeHash(GetIDString(),charMap1) - - # Get the current user name - encodedUsername = encodeHash(GetUserName(),charMap1) - - # concat, hash and encode to calculate the DSN - DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1) - - # Compute the device PID (for which I can tell, is used for nothing). - table = generatePidEncryptionTable() - devicePID = generateDevicePID(table,DSN,4) - devicePID = checksumPid(devicePID) - pidlst.append(devicePID) - - # Compute book PIDs - - # book pid - pidHash = SHA1(DSN+kindleAccountToken+rec209+token) - bookPID = encodePID(pidHash) - bookPID = checksumPid(bookPID) - pidlst.append(bookPID) - - # variant 1 - pidHash = SHA1(kindleAccountToken+rec209+token) - bookPID = encodePID(pidHash) - bookPID = checksumPid(bookPID) - pidlst.append(bookPID) - - # variant 2 - pidHash = SHA1(DSN+rec209+token) - bookPID = encodePID(pidHash) - bookPID = checksumPid(bookPID) - pidlst.append(bookPID) - - return pidlst - -def getPidList(md1, md2, k4 = True, serials=[], kInfoFiles=[]): - pidlst = [] - if kInfoFiles is None: - kInfoFiles = [] - if k4: - kInfoFiles.extend(getKindleInfoFiles()) - for infoFile in kInfoFiles: - try: - pidlst = getK4Pids(pidlst, md1, md2, infoFile) - except Exception, message: - print("Error getting PIDs from " + infoFile + ": " + message) - for serialnum in serials: - try: - pidlst = getKindlePid(pidlst, md1, md2, serialnum) - except Exception, message: - print("Error getting PIDs from " + serialnum + ": " + message) - return pidlst diff --git a/Other_Tools/KindleBooks/lib/libalfcrypto.dylib b/Other_Tools/KindleBooks/lib/libalfcrypto.dylib deleted file mode 100644 index 01c348cc8a638e243754aea2cd2681ad30e629a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 87160 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 diff --git a/Other_Tools/KindleBooks/lib/libalfcrypto32.so b/Other_Tools/KindleBooks/lib/libalfcrypto32.so deleted file mode 100644 index 9a5a442617a2046fb9b7050d339b18bca8993e7b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23859 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 diff --git a/Other_Tools/KindleBooks/lib/libalfcrypto64.so b/Other_Tools/KindleBooks/lib/libalfcrypto64.so deleted file mode 100644 index a08ac28930fdd54e0eb4e8973a398e5e951d3825..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33417 zcmeI5cT^Qe9PclJf{I>@4Lcf4G%69Xmk5G-(QAofiyBlwCDsVU5^GSy6;acBV!G)j zF~pb#c1`RWyRi%QUSgE@`OdwoK+HR@obCpQN_JF_#hv$He5&+a9<)7m>b6!G%1 zSs!oPBQ~Y7eBC+Wo*jLk^lX4|TXUN)e^#|sGG&Y2-Sv;Heev!ri#A)3$)*xWgju{DvM{^^{aSho_#yQ_tdbJSb2nr2f%;kN*D90&8}r(@S%Hs_}C#gZR< zV@m5+yZeNEHei`|k6*IX7ws?3kH%4#_?bE7>J;^|x%`R+c%}IS`DtHie(vLE&gfGg zJ0AM!p?QOR1};1D{N%{?J5Kurq?N1_y=45ZC;L|YA>%^nS|w^{p4@h>^wQY+F<07r z{3zA__2bQvgFN%fD)a*zI#%J2&MT;JCh23eYTRe1=ZaJV?4)Q27!D_4kx->&ai>naHcGg%BG%Y}A<1u}R5;$0f(c+F}z&Cnnos zhXPw{Y}|yw+8jJGaZ-XUw$oFwJraf`CM73~iyS#PDJdbz7SXYMbr2A#qk~l%!j6BVyx*k8l_69vNy&N{&w$ zH;#Qn$Bs)Jojg=~ZOLQC4xSXLE{uuR3FJ#hxw~N-nK&daBxy`YQ(J68{NUulv@j$o z$$jEvjgGf7FF-%*Y<0>5FUx$AA^4%k0O4|bR<>&WJV7~nP#|_Gt@0r@Bp*s2U z^M|5_`SQ!V4{82%q@MQ6d4*>tgPfBd7Dl57FeHN)7$i=qRrtt77^1kWr!oS%|XiAlKma&ZJMzs zXB@3LGOE=Z8f0^3)UFC>uK|0Ub&oi*Z+bgYPkA|7>`Dqc z)~#??i0JK@wKbwwL`<)q_L*NXe2!Up_L(2CG2?cY&1SRDn#35*xILY1himf!`^?wx z?wi?_eQTaMNKR>$&8%V1?B?yrJ`(5%-EGf|^w#3NRIpMNccsdATW?6~RVi4l zV8I&hg4OS|PT*nuJ2g)Z3C-$1Fk(Q&z=+3Jobp<*RCD!C#?S@st5Jf!Hk4b|8p4paP{9lYqWdt>**YdntuY1+nXHFt9Qi^pXzO1}@7MNA?*aNx9AT%=$Ue>E*UYhn34^ zm$?LISbqL%XB^WM(tbx+xpVw%bfkBt+Z!CNvZ}3Cv{$XpyVCsSj4G6C9aBWgKa$I{-PjbtU9Lx+@O^%eu-PnP#zD=rPGOB#*faM>yLM4K zJ;Ye%$eE!Eqo zFYI@?qUe8iZa_D7cMFTUMK5+nXP*mjhII^>aY((0m~p~A?f>W+=5y1LUNzXB`De82 zPg=QF!)7y?MBL!$^r-zrVYQc0bbm!%@mL0N0O@oej#+9QrQQ3}{kJ$mof^0c(Prf_ z8tG9xsL>}VI^F+wM`*Uwl_i~V-Ra)RfEkC?2crHqd**h^k{i04atj{pkpc*i-_1CMm0Rz0Wiv=CrD8`| z)D?|SfO^v@%z2Axxk!Xy%rtj zbJ5Xiaxn4tstKa2>34k%pd)>9aFAvN)4`s(BQ=K`xP#1}krzLh z6(ObiQEBbG*mUZYav0uo>}$u0YVE7ZWtw%kE^@t^ zhXO)3I$i4u3`>B!gC#v3RDX3OMxnTS(~nK=pLaOY2Ly9)pCSxu=xRsYRYzC~Yv#PB zOj@KrTj6O*c2WUpk)_=Eerb`ViJ#4_d&P*@70mq&4Y;NmO@<;|ZVD5@vc)_j<@|gR ztLpjVe;QxsSn+j+6<;S5h_Cb2dj9x2wSB?(I=YF8ufy8>e-dAV?h#*0x%i$T+LdiW z!T&tI+A~+)4YAqw%uOaJmU3|aK+IJOKEYhH<}_$xEKyYRLt*xAh^5{}cfA1W5n8iN zuq{Z(Fz7$M*3T4C<7!#dVs$p(1Vr~64I z%x5p9350~i6;`n17M&*dbqX7B$2N|z*oFm1WqYSEdf7Rw>P_M`k1lJ%qr<8aqSq0$ zFWED%04uoOAh>ef-NBVnN*~F+g6j{S!N#|=+3p0_$#uf%`bmfUSAJk44I+L__sODQ z*LLQ{V|T^IxK-VyDG=ZpwOw>CkYzKX zj;_pFW{`>N<+iz>o^^BSTCTa5){A()Jgq(9{A55{FT%O9{nC2fBb--v&{#&3SBeR3 zUI)usE1aJ_O*l8t7mDXyT&p8`MY}dc#6-Kc_j0&y@yv9I+h<2ScSN=651Nk(ZqEJo ztgH5!&(VOniB4mq3r|!trbs>y)4M1b~49A{sWX$itTx#|TI{>qyO}fy25{tGix@9sfpj>gj+ku7lCpr+uO` zqJq7mvyb{jq#m?)&6r%9<#y1=;kClywThEV35`zojdm4_PVb0)J8+B0K*T%3+{0p@ z^*&$iU{OZ_i$-zNr0$U-27?I|G_mod@bTeK&5bMAP zPx5F?$Nk9Ufi$2~MmZi#8HjH*5KbOstbuU44y7JvVjbeygi*-wFUc@yFlJp%?i!lK zb1~11UX5ak+S1DL#Ux)vwm3iTv?HTjn8WJ=_Z8|-sL4;w_6kpHD=2DbEHR@am{^S4|HZ$IJhD~Op0h%o_+RwYhJj|+fjE@blpvk^olwY zzR5}N!;0(9dF7idIicosnH=Vv?~Zfh3I!&{8Ap16Bb7Dos^CcPPA_`tcWA^MnKAu_KF6} zXfOhsIWq`u(XQ1jDw|j6-u9t;^?e#Mtc&Y#XkO@<_GUgs*uQU-QMHCS)8$1H_cq~c zsi@G^QC@qa5?7l<;(^CFaD;{gXCn#u8K{VBwL2^HK)m`PPv(+b{GFBN8BUW}R?1araAE?yIfXB+lBp#`o<*dF!XP<*9tB zwwxJJ6}(*8p7`kM=wr_;=31Y9(lPtPwo#&+S9Q?KVGSHe?qB=kL5>M1{d z=aEh{xSp4C!F(g5-zAu@-13f_(ePWr`25Rfa6(cnzY+|Ivn7rWdfrS3rr=hid?*nxI`0}IL(=#Drs<`5Vism=PFF*buH(fT{ zZzDeAmegju(EhC&;%9yT`doa$`-u6KZm+)@1}S}Y)Kg!G&pq{fnpoRedgZ+^)L*uX+iE8F73(G*j25HT zuA3)5;aZ*|PMA3FvN+@Jxj>V!f)H=7`TPnfS4I{-WilI!{t2* z;*MX6a(862)fm!vi#X@@M=Qm*db)otys+1$_mpmQNDsOEUiZ}155yLyQ%j1Uo*NV{ z7JKK($HhkjM*l3{m{ao+u}!i4FNT=}FpW_vLoF|U00 zL1OUDS1XCH|8L^(zt3cfaTks*71!?z-6;mlifbo^*LtO_7*uBUOmRwZr&;3lHa~6^ z%QU+2r8w%{x@*MF55HYaywvcUed6gJvrCDak7SP*M^^Z@p7`1iV?Po9a=chZtQDWz zQCvCq??qzew&xd$yXsHrEuLEtR!dydHTqdGdrR}(;_dg=&lQ`N-2SS#X~vb0#5xBi zeJDOK@B2OC2fu#NRy_Jv%lcxe=O=U*-}pWD32{h=sWIa6vKd#!oTa-Kh>u@u5h7ms zYQ;0+FVhFUBTjx{bE5c3gB@eUZ||$UNo@7n=WE4}rvwIwqvPJp5ug6C)n2jqgaxy4}^FgQ>dVkx8#D*_^xLV8_`d$;U*-u@Q#gEoM^S8Ko^MZq7`CT7}iJe~c zxgcKKb2>sS-F?F}@!_k_RTMM+=<<}zl(qRynIwV^4*rV#VTn}IK`zk z>Sv479!=;V`kZ|HEpcFzxn5#Y^0R-5ArU2SijP$r_N~}s>)z+Zu-*Z=V%d&=j1uP# z-ub#X$^YGE;=FR#`-!gBWlxD`AN*va*ks+{@5M96svZ(sf8BAC*zeD+Z;EY`F7^^9 zZY&ol{@QF!l6Z2%wVq=CPjAJF<;Jx6Q=GjwYJxc9(pT%nM^+C1L9E|6xVab`{{C`t z+=^-yME}}{e-hK9Th|dIUMiI*KIyfng?Qjwe?Rffqf2&)3$lC1i-TwW_Ov)Qv(XMQ zdB8}A*sjmg%_90Y^A`P^exm;s%18e%i|F61YxHlfBl`bL`RM-z5&fIqyOiW zkN#_m=-^uJSS^gmrh|MNxkA1$K)VIul}RYd=0oY8*~rP05MXXxL=CG>CRA^Ja| z{pkNC5&fI%hW_^`js8u%L;u^9M*n7Bqkj{}(Ek(4NB`z}p#Kp{qyPOP`p*>6znQP- zf2h*v-^6M3AE-3?H|rVw4^kTaw-?d>K@t6%c!&P0DvkbM6w!ZQ5&c&Z(Z7jD=-Y(f=F~{hRfN{ue5Z{u_ztKSf0UW`3goPnAah#YFV~ zsEGb=i0HqKi2hCdNB?GhqyO)ekN%5_=-d61dn=9p&HVxT ze^6=kZ`L9DucI(t{U?g(zk-PVONi+I zn27!(Mf6`yME@m4^#6s3{(lhB|5_3K&l1tUiLdDYN2SsKEfM{{Bcgv3f6)IbrO|(5 z5&b_UqW^s&`X4T$|2Pr-uNTq3@fZ3JR~r2XiRgcdi2kpO=)a7J{!M&F|DBaa|CdDc ze_BNUCVrv+kxHZg*F^OHmx%sriRgc&i2f^!=zo`p{?Cc%e~pO#vqkiOTSWg&MfAT( zME`X}^#6c}{yz}W|4|YBmlDzc8zTB2BBKB0BKpq}(f{Kj`oAKg|6fG(KUqZopNQ!H zTM_-Y64C$1BKkL;NB>VNjsA;^=>M{a{=G!>zeq&?2_pKRDx!Zs5&a(*(f@7{{Tt7r z|Lsbn|35_Z|CNaTLq+u8P(=S(BKmJ8qW_OX^uJg{|K&yW-$_LO*F^MRT15X3i|9W? zME_kx^dBRl|35|ae?&z8RYdf^R7C&NMD*_?qW^&+`cD$ke~5_w9~04k4-x%`iRizq zi2mn_=zo%k{^yD4-zB2|vm*L$BBK8@BKmJFqW^v(`fn?u|A`{{|5Zf)Cq?w%Uqt`q zMD#yfME^5H^#6#6{_BhAKUPHl<3#lDFQWf+5&cJq=>JI({T~p~|C=KEUm&9Y!6Nz} zE295o5&gFl(Z88j=-L5Y{hRB9{>}A8|7L!m|19lC|0yE+KQE&HS48xGLqz{(oY22H zZ}e}jBlM`Zwc>{=2Ch`u{>i|DTKK zzom%&&HP6HW__T4Gr!RPH0?+K<3;rUrHK9)is-+di2jF*=-N|4c;xbwu=U;tcvXaRL3Cb&md@)qeEi#`oAcm z|HC5szbvBvUqtjjL`46$MfBfOME|`+^nXZ1|EEOse_BNU=S1}Xj)?vPMD#yLME}J^ z^j}m&|38c9KSMM{||`h|B8tITZ!m@s)+vUis*l}i2nZ;(f*li|D_Ni2fUi=>J_2{XZ~*Afo>tMD*_v z(SN*%{^yG5zpaS=>x<}rfr$RQis*lfi2mOb(SJ!1{m&55{{a#G&lA!AuOj+?OGN+A zi|GG%5&d@%(ZBHw`ZwM}|JRg{{=X8@|8x=kzaXOj1|s^uPelK(iRgcdi2mb5^#7xX z{wIj&KTt&f-;3z~brJoK7SaE45&drx(SMGJ{@)PMe^U|t7Z=h0WD)&;BclKNMfCro zi2jF)=>I1X{jV3%|7H>W?-J4ft0MZ}BclKABKp57qW?cc^uJg{|6N4%?<1oB??m*U zCZhiuBKm(+ME@s6^xs58|H&fyj}Xy+H4*)9714ih5&d@*(f?o({rijPznqBvTZ`!b zK@t706Vd-M5&eHHqW?cd^q(Z6|BWL0ZziJu4I=vgR7C${MD)K`ME{pW^uJO>|BXfT zA1|Dz)M&lb`DOcDKOis*lUi2nPC=>M{a{>^xy z|B6bZe{b|ImLOrP2SdBKr3h(SHRI{hN4z{wFGp{*4FFf1=Xp->h5oUsP%I zZ{{cZ|43={A0ndvOCtK8BclJSBKl7i(f=S3{XZ$9|Is4)uPLJc{UZ8*L`454UZMXO zrP04xALzfB(&*pB5%h233;O?A`RG4fME|=*^q(W5f0u~^gmrh|Mf)lUq(d#kBR8Nw21y^iRk}J5&bt1(Z5|p|7P8x|Ncs&|E41PFCn6T zvmVj^S*6jxiOc9eOKJ4KPelLYMf884i2h#?(f>jb{nrxFe-#n^7Z=fgx`_TWMf6`? zME@5=^q(T4|JOwHKTSmcULyJ*C!+tMBKp56qW?${{ogO5|1%=`KPsaCP!avdiRk|o z5&f?g(SIiq{r@PU{~IFuuPdVew?*{-jfnndi|9XFME~E4=zpw;{$CW)f3ArB{}$2z zc@h0j5z&8`i2kER^xs@W|LaBczgiOGN)IMD)KxME?Ut^uJj||2stVUt2`~pNr@}P(=T4is-+Ui2ffC(SJV?{l6}v z|864se@R6Dc_RA%NksqUMf86{ME_$%^#6{C{uhhr{{s>Iw-wRKC8{riaM|FnqyH;Cx}IT8JL5z+rS5&eHEqW_mg^uI+!|4)eMzrKk66GZg? zxQPDeis=7Y5&f4G(f=?J{qGgge}IVo{}9ptP7(dTE297FBKj{YqW@1s^gmcc|5Ziw z-%&*WTSfGLQAGdcMD)K#ME}=B^nXi4|7}F{A0?vyuSE1eTtxrDBKm({ME})9^nX}H z|E)#zUrI#(i$wJAFQWe?BKq$wqW|AS^xsHC|06~8zf|l|{D~b6n||2**YNk>o^fgC z&B;evJoCoGdCxYU7a6tkK<;lbPrumcqviLvIqQ6>WV^psboC2)cW9m6!QVX9|Gnr@ z>04@#yM8r(_V8CLZ2w@u?*kJupC38a(f!t$AND+6zC)jnGv1oHVAp51xj$_yaqVc| zqD#MRUu<*z>hssPwHL|$>Su4SvU5hXe6CDHw_*EEzH)KX#`8;#C%yc6a;+0zG?{hy zMzfr%DSwXcc%kx>)#|Qtjro2^=}!ZHsq%O4M>?Y3w9DqHH}MYFDc z9x=kqr?1D1IiUQQRjbVW?%uHBAf>&%yo!jQ3>&sloKmlzxjvO{-#(%Akexff6OVuP znOP67KlRjUrQ5GxKVN+7nP+N<;p^79#Al~W`AaPS*I#Dcoa)!l#K(0VJI+&n$@Aw; zd_Mif7ZFMi+rHhz$y@Ke7pwHiufA$8{`%&dCT?H*;Deq@zdUZ-QL%qk)*IsT@4rtG ztKPbGNc{4J7tH*9;(-U8N>>m^5q{A{e67=#2#zbyd);ve}4z@@f9oH z66*v8HWc4|`Q>J!y=c)A;&-pV{;*iw*SECT^uGJdx_V>!^eRfXIehp}@qtsPOgu__ z@WHN1Z}{e$Y2rI6DJR7$#f#r3zMYzC)?sq}`hArSsa(0S*q}<4rs6lFNADBQKK7Vd ze>WE{?636I9z8w~yR>TclsNd0Ka4*%e*5hdrJt@_w}<%1kt6%XCm(*;#Isjs&NT7o zO3$7jDS!BuEkB6Xr~a8U+1efy6cjGj`1|i8;@Ypjo+zH_-#=6Q<%uV@iyeOZEkoS> z%P%G#c5cyPoYDt6b$U_U?sUE?=Jf4r;(n)1n`S9JV)N!7#fQ$FTO}U*^wVx)g_}3e zh^rzaXNx<#bTQuP6&5yB=@)0sDkE-c-+qo5aN~yYaj_yre3YIsZQ5<|eE04P#i-V; zCy2}5d8f8`WYMC|;-yC)eNVjo{`&*Ptnl#Z;+*;Oi;7=7|9o5Vr?=j!DLR@rA1nG? zxpG1L^QWKU#O%z>@nXuJJzt4&HEXsL8wLafiIwy6j)|o&UAiTX+Pd{u(Yay646&TQ zf1uc;Y}p6IhfkkgAs&j2{Y8B5l~J9hYoXZ@zJ)WOGhZ(x?H(B;=}_7J{Au@{`gN~wUQ+( zh)?a^`-Rx*;K99O??#Q{#jY(|CX3Zelqe_ejgEd^EK#hOzc?~D*dZPrFkq=TaQX6g z#L?Tf{VbmQ{PRfhqaj09ii@6mu7&u)OD~0r&5j=3CZjoKs5&B*W)<9F=%THM;T>zm@iPd+jCbxTH$+@w-%p&_Sy1cP;TxzF?!?1XT`i; zy*?Eu)Tz^5ob>0PABywm&h-|rzx!@Kak|S@T&!5CRAq6`ufHaUp~sK!6j!%vH&a|X zeE4Q@MMT6i;^Nm{t1FIg(BLt#jNM*c{B7~#r^UZxVit*i{P^Q2v1Nq{^+oh=`hosU zyXfDH6Z+54e)Mmy7y38z8~vMki2lubME~Y|(7!nk^l!!q{ZCPS^j}Ft|3gIdZ`M8f ze_d(x-(E!jZ;9wXTtxq7+|a)nPxNoD5BfLbivCNg9Qr>kqW@tc`oATj|C1v6H*pUA zn|Xu&&3r`v{k0$coB4zO&3r=tX8xdmGoR4E8Bg>dq4Mazh=~5XiRgcTi2lv`LjR4G zM*n^y`hP)0|7P8w|DH;te-qcyzrWJxzlVta&GknAk1LJ->xk&ztS|I$)(!gqPWkBH z%op_ERB818hKT;#i0JI10wq0E~5V&5&d@((fks|+QX2igD5C#OBKi*y(Z7jP=zoUN=>NQk{-Z?nzf45`M@000Nksp* zMf9H~qW?J}`u{>i|38W7-yx!Z9})fkDWd;u5&fr#=s!+G{|!a-Us*)|rA72VN<{xo z5&f4F(SH*W{XZI7Z{kIa)e{T`}cNNipbrJpV714hQ5&e%8(f?5q{SOq;|7a2YpA*slMga{Vx&G|6d~d_Z889 zZ4v!{C8Ga%BKpr2(SM+b{&$GzKTSmcV?^{nPDKB~BKp4|qW?7_`hQeJ|M!dNf3k@F z-xSe*O%eU?7t#Md5&h2=(f=$F{Vx#Fe>)NVuN2Y$ry}|{_c!RjsM6@)OGN+oiRk}* z5&g%C=>LX@{x^&0|2Yx;_Z89qgChF>K}7$}Mf6`*ME~E4=s!$E|38Z8f2@f9T_XDb zSVaGsBKof-qW|Yb^dBUm|7a2Y=ZWZlf{6YniRgd6i2kpO=zqG1{ws>;e~*a%Lq+tz zT15X#MfATyME{FL^gmui|7Aq<|C@;Z{}$2zA0qm1DWd;45&fI%hyG1{^l#!e`oF0C z=zoNW{>}VA|ErWn{|!a-Z>|gaAEq?=uP35^6W7rHPNmWRXCnGH>ks{#^^5*Z+(7^9 zv>*LX5z)Vy*XZA@3-sSn`RM<=i2hA{ME@qfqyP7mkN(ZPNB?F%qJOjg(7&12=s!#4 z(Es-$`oATj{})8`|A2`8n~Lbat%&}wiRj=>LL<{+|@l|1J^z zo9l@F%PWokeMI!XMnwN+J)wVdUD1D_^3ngxBKkM$0R6wNH2U`y(Z5*_=zqG>=>M>Y z{!fYM|3MM`eKb_(SLst{XZe1 z|KCLP|BHzJTZrhtlZgJEBKq$uqW?`I`rj;~|8pYxH*p93-&7j?M~djbi-`VBTtNS` zlt%yUMf7jt4*D;mH2R+=qW|t9`fn|w|93?6Z@hs1A5|Ltzb~Tya1s5_7t#OoBKm(z zME}i2^nXP}|38W7KT|~idqnha;xGCSP#XQ`iRk~5i2k>V=zoKV{{2PtUsgo_r$zK1 zE295bMD!meqW_8_`d=xc|BprVKUGBkZAA1RFQWf}BKof+qW>fj{SOk+{|piRcN5Y7 zJ`w%b64C!dBKlt-qJQHn^xr{g^nX@F|4T*mZ|-N%{{f}Z|KlS1FDat`y(0QQD5C#H zBKmJBqW=;i`Zw_#{TEXj{RfNae}IVomy76sn~45D7t#L^5&b_WqW_mf^nX-D{}V;@ ze?mn6Yen?mOho^qMD(8_qW>Kt`tK^D|4&5pKT<^hy+!nYSw#OQMf9H_qW?)E`u|cy z|9&F+UnZje2_pKhEu#Mr5&c&c(f>yx`d=cV|J5S;&k@o8ei8jAi|D_Gi2gf^=>KOC z{iloQe~yU$$BO9x5fS}=C!+tMBKmiT=-*pJ|5ruyUs^=}gGKb;L`466MD+iIi2i4b z=s#OT|J6kF|F($!4~gjix`_T?714hg5&b8M=zq6}{^yD4|3wk~zagUk>LU7oRz&}~ zBKqGbqW@kZ`mZCR|35|aKUYNm?~3T(C8GaQBKrSTME}P{^xsZI|HDP}A0eXu*F^N+ zKt%s`5&bU~(SMAH{(ltFe+3c!$BF2FvWWi2i|GGp5&d5j(fME}J_^q(rC|N0{OuPmbf zDkAzHEu#O&MD)KzK5fS}AETaFJBKq$sqW>+T^+Eq< zw2l6QMD+i+i2lD8(SLst{XZe1|KCLP|BHzJTZrhtlZgJEBKq$uqW?`I`rj;~|8pYx z|5QZ(H%0UxDWd-_BKi*#(f=$F{kIp<{|yoS7ZK6_G!gxG7tw!f5&gd-qW?uA`hQeJ z|L=?FKU_rr^F{Rkyomna648Hi5&d5g(f>~(`p*>6{~i(j*A&rzfQbI{MD%}2ME_ew z^uIww|NbKSFDs(|(<1th7194IBKnUK(SJn|{jU_!|HmTwpDLpNHX{0u7t#Mf5&c&Z z(SMSN{s)QZe};(uyNT$3pNRfziRk|!5&bU^(SMqV{yT{1|E!4qmx}1WoQVDpi0J=u z5&f4G(f?i%{T~$3e^gl;L|6@h;|A>hGzZ22_P!at*MD*`1qW`NR`Y$b_|G^^q zZz7`qJ|g=6K}7$vMf9I7qW@|l`hQzQ|A$2Me_cfXuZrlujEMddMfAU0ME~CM$H&W)SHwdf5B)qe z@rCUmPdePg=^j?`u&Joy#pU}a;M)z3*FW|UG%{iIu;k%2&6@M7;@wIYT?d%_VP2jB zi1X9DJPmm3&-3z@z&F3n%Ts_F`F&nqFg=|7V_x0>xbG**!TLYv<$VszQAQuHNj+>{ z69T-d`1|@T(0=kXu>)v-mCmSL>45g71KX7jiYQ$tqIAO!sl{d&o$Hg~J^?TeIxmq{u%%La#1#DubpYHooO$kbn~dvV|%9h z&Mr2$Xok-`@A>X_Rqq3S7V(deum9hyw~u<^nR$8Uod@pYMXF&=A~M5=<3`Rc%Be)o zF2>O#Q+=aKPxsRC%5naI%kuKfy9}t;v2Z`Tclbv?HI7c|g)`1(L6g=;`SIjWAitl< z`R|YaM&Q2@_-_RM8-f2u;J*?0Zv_4uf&ZHk;Gd*3AFGYRX)B*wF!M1FeQf!A(m}rN z4f8O;=B^-j`pWIRG1@xssi<2|eFk~%?u)sTw(g$0esg0^UWli>d59(*=qYC&&e+Zw z6J&C%hlNF+{kl)o$I~5~`SO7D1kZu2e48h2JzSWFQpy!qWj@yN_%{zt|Fb+DXIxC$ z8i(_q^gPf08-@0}Tl;_ar}cik!bj}tIq}Cm9PQy$58eN_8Q+U|(qDSmd~A?us$*ni z%b+^FhNO&6PI=hLtGvdI32Gd|e+@z#rmM!Nf;hGbll)k39-ZDscWTdA#urL#wBq;H=c-N zCv6TMl^DkX$IuLagqXJ7?eL;6DvcR6Dq(bT;Sv5nOY5OkLv79DwVsJ(F}P5(o@H|< zH-xRL%sH#RwazRK@|3jJjWyq`2AZ>>HD=Yf)}h4!&kh||n^t{meT0*zwypZsy0&Po zV~UuMwQj7mgAJ`ItG>04Eb94LAFKaXIg9$TLu=GZTkF(fxGM8e|8;Du|H{)Ro*(Pn zOokPZJhbA1b$qM7b^fCZ)wkk|#YLu3cmEaJ3fEWr>X&s`D~?*Uj^k;@^wruH_2p*% z`pvC%>)~`2%JSKMGGL`N8^|L(nECyCG#qa!K)w42Y7OHQ>eT${7f*xAOx3cGW z>RZ>(R@1Y`!!qrxl5PeSOxpCNwfb+>FZ}*aeV3B|Q~3A`*_propg4DFi<7l8-^aRs zR@SF%sE9Q`?!4Dj6N69T@hV*ZYs%!W-*9x0s@WR;FVqXo6VgvS>_L`zQy0j&EJ2^>p7nK z-S1YtXZ`Jk>RY~Y|Lujl>&aCWR$MTo(rpK0_3g&y?xeqLu?{nvVv0#2to8^gu z`NeEj{4bc#U|4}R-wtm^#fsPYvKg0uzDHI2y=^6IR-7$ZzT}G)|Zx_+8JbJcpCb* z{<}RIV5?JT{GT)J+iknzJq>v9j>-RL|34#N$6XlYDd<5qyw|w7<8LY3bj!1z{%)eY zmyP^Ro(4Sm`sJg^?@=&+r*s9HPG`zwrC4hmn7;_h=`X@^|}ZZRyu(<~`k>`D%Wr$IkaX?R59V z0jvBX^0np*e}3|mFB}h7nD+1Cow&gxN9KFQwk>X4a#C{2(4qW`_OY>%G2ME^I-`5W z=*`=)Q3X<+BJ=Hv9}_!ll)D|I@0`NS#K&Y{8n=7m)iy2CmtSdy$f7#{Vx29^n1Jqy}&!o@7C7)!|(Mj z^@1e||IS-)InVb3^XPwhp?Utlx?kX3=7rvxe)nLF?~H=ytj+t{&AZRtZ&klr)b!l* Ko^Mvd(mwtG diff --git a/Other_Tools/KindleBooks/lib/mobidedrm.py b/Other_Tools/KindleBooks/lib/mobidedrm.py deleted file mode 100644 index cd993e1..0000000 --- a/Other_Tools/KindleBooks/lib/mobidedrm.py +++ /dev/null @@ -1,460 +0,0 @@ -#!/usr/bin/python -# -# This is a python script. You need a Python interpreter to run it. -# For example, ActiveState Python, which exists for windows. -# -# Changelog -# 0.01 - Initial version -# 0.02 - Huffdic compressed books were not properly decrypted -# 0.03 - Wasn't checking MOBI header length -# 0.04 - Wasn't sanity checking size of data record -# 0.05 - It seems that the extra data flags take two bytes not four -# 0.06 - And that low bit does mean something after all :-) -# 0.07 - The extra data flags aren't present in MOBI header < 0xE8 in size -# 0.08 - ...and also not in Mobi header version < 6 -# 0.09 - ...but they are there with Mobi header version 6, header size 0xE4! -# 0.10 - Outputs unencrypted files as-is, so that when run as a Calibre -# import filter it works when importing unencrypted files. -# Also now handles encrypted files that don't need a specific PID. -# 0.11 - use autoflushed stdout and proper return values -# 0.12 - Fix for problems with metadata import as Calibre plugin, report errors -# 0.13 - Formatting fixes: retabbed file, removed trailing whitespace -# and extra blank lines, converted CR/LF pairs at ends of each line, -# and other cosmetic fixes. -# 0.14 - Working out when the extra data flags are present has been problematic -# Versions 7 through 9 have tried to tweak the conditions, but have been -# only partially successful. Closer examination of lots of sample -# files reveals that a confusion has arisen because trailing data entries -# are not encrypted, but it turns out that the multibyte entries -# in utf8 file are encrypted. (Although neither kind gets compressed.) -# This knowledge leads to a simplification of the test for the -# trailing data byte flags - version 5 and higher AND header size >= 0xE4. -# 0.15 - Now outputs 'heartbeat', and is also quicker for long files. -# 0.16 - And reverts to 'done' not 'done.' at the end for unswindle compatibility. -# 0.17 - added modifications to support its use as an imported python module -# both inside calibre and also in other places (ie K4DeDRM tools) -# 0.17a- disabled the standalone plugin feature since a plugin can not import -# a plugin -# 0.18 - It seems that multibyte entries aren't encrypted in a v7 file... -# Removed the disabled Calibre plug-in code -# Permit use of 8-digit PIDs -# 0.19 - It seems that multibyte entries aren't encrypted in a v6 file either. -# 0.20 - Correction: It seems that multibyte entries are encrypted in a v6 file. -# 0.21 - Added support for multiple pids -# 0.22 - revised structure to hold MobiBook as a class to allow an extended interface -# 0.23 - fixed problem with older files with no EXTH section -# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well -# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption -# 0.26 - Now enables Text-To-Speech flag and sets clipping limit to 100% -# 0.27 - Correct pid metadata token generation to match that used by skindle (Thank You Bart!) -# 0.28 - slight additional changes to metadata token generation (None -> '') -# 0.29 - It seems that the ideas about when multibyte trailing characters were -# included in the encryption were wrong. They are for DOC compressed -# files, but they are not for HUFF/CDIC compress files! -# 0.30 - Modified interface slightly to work better with new calibre plugin style -# 0.31 - The multibyte encrytion info is true for version 7 files too. -# 0.32 - Added support for "Print Replica" Kindle ebooks -# 0.33 - Performance improvements for large files (concatenation) -# 0.34 - Performance improvements in decryption (libalfcrypto) -# 0.35 - add interface to get mobi_version -# 0.36 - fixed problem with TEXtREAd and getBookTitle interface -# 0.37 - Fixed double announcement for stand-alone operation - - -__version__ = '0.37' - -import sys - -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) -sys.stdout=Unbuffered(sys.stdout) - -import os -import struct -import binascii -from alfcrypto import Pukall_Cipher - -class DrmException(Exception): - pass - - -# -# MobiBook Utility Routines -# - -# Implementation of Pukall Cipher 1 -def PC1(key, src, decryption=True): - return Pukall_Cipher().PC1(key,src,decryption) -# sum1 = 0; -# sum2 = 0; -# keyXorVal = 0; -# if len(key)!=16: -# print "Bad key length!" -# return None -# wkey = [] -# for i in xrange(8): -# wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1])) -# dst = "" -# for i in xrange(len(src)): -# temp1 = 0; -# byteXorVal = 0; -# for j in xrange(8): -# temp1 ^= wkey[j] -# sum2 = (sum2+j)*20021 + sum1 -# sum1 = (temp1*346)&0xFFFF -# sum2 = (sum2+sum1)&0xFFFF -# temp1 = (temp1*20021+1)&0xFFFF -# byteXorVal ^= temp1 ^ sum2 -# curByte = ord(src[i]) -# if not decryption: -# keyXorVal = curByte * 257; -# curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF -# if decryption: -# keyXorVal = curByte * 257; -# for j in xrange(8): -# wkey[j] ^= keyXorVal; -# dst+=chr(curByte) -# return dst - -def checksumPid(s): - letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" - crc = (~binascii.crc32(s,-1))&0xFFFFFFFF - crc = crc ^ (crc >> 16) - res = s - l = len(letters) - for i in (0,1): - b = crc & 0xff - pos = (b // l) ^ (b % l) - res += letters[pos%l] - crc >>= 8 - return res - -def getSizeOfTrailingDataEntries(ptr, size, flags): - def getSizeOfTrailingDataEntry(ptr, size): - bitpos, result = 0, 0 - if size <= 0: - return result - while True: - v = ord(ptr[size-1]) - result |= (v & 0x7F) << bitpos - bitpos += 7 - size -= 1 - if (v & 0x80) != 0 or (bitpos >= 28) or (size == 0): - return result - num = 0 - testflags = flags >> 1 - while testflags: - if testflags & 1: - num += getSizeOfTrailingDataEntry(ptr, size - num) - testflags >>= 1 - # Check the low bit to see if there's multibyte data present. - # if multibyte data is included in the encryped data, we'll - # have already cleared this flag. - if flags & 1: - num += (ord(ptr[size - num - 1]) & 0x3) + 1 - return num - - - -class MobiBook: - def loadSection(self, section): - if (section + 1 == self.num_sections): - endoff = len(self.data_file) - else: - endoff = self.sections[section + 1][0] - off = self.sections[section][0] - return self.data_file[off:endoff] - - def __init__(self, infile, announce = True): - if announce: - print ('MobiDeDrm v%(__version__)s. ' - 'Copyright 2008-2012 The Dark Reverser et al.' % globals()) - - # initial sanity check on file - self.data_file = file(infile, 'rb').read() - self.mobi_data = '' - self.header = self.data_file[0:78] - if self.header[0x3C:0x3C+8] != 'BOOKMOBI' and self.header[0x3C:0x3C+8] != 'TEXtREAd': - raise DrmException("invalid file format") - self.magic = self.header[0x3C:0x3C+8] - self.crypto_type = -1 - - # build up section offset and flag info - self.num_sections, = struct.unpack('>H', self.header[76:78]) - self.sections = [] - for i in xrange(self.num_sections): - offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', self.data_file[78+i*8:78+i*8+8]) - flags, val = a1, a2<<16|a3<<8|a4 - self.sections.append( (offset, flags, val) ) - - # parse information from section 0 - self.sect = self.loadSection(0) - self.records, = struct.unpack('>H', self.sect[0x8:0x8+2]) - self.compression, = struct.unpack('>H', self.sect[0x0:0x0+2]) - - if self.magic == 'TEXtREAd': - print "Book has format: ", self.magic - self.extra_data_flags = 0 - self.mobi_length = 0 - self.mobi_codepage = 1252 - self.mobi_version = -1 - self.meta_array = {} - return - self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18]) - self.mobi_codepage, = struct.unpack('>L',self.sect[0x1c:0x20]) - self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C]) - print "MOBI header version = %d, length = %d" %(self.mobi_version, self.mobi_length) - self.extra_data_flags = 0 - if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5): - self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4]) - print "Extra Data Flags = %d" % self.extra_data_flags - if (self.compression != 17480): - # multibyte utf8 data is included in the encryption for PalmDoc compression - # so clear that byte so that we leave it to be decrypted. - self.extra_data_flags &= 0xFFFE - - # if exth region exists parse it for metadata array - self.meta_array = {} - try: - exth_flag, = struct.unpack('>L', self.sect[0x80:0x84]) - exth = 'NONE' - if exth_flag & 0x40: - exth = self.sect[16 + self.mobi_length:] - if (len(exth) >= 4) and (exth[:4] == 'EXTH'): - nitems, = struct.unpack('>I', exth[8:12]) - pos = 12 - for i in xrange(nitems): - type, size = struct.unpack('>II', exth[pos: pos + 8]) - content = exth[pos + 8: pos + size] - self.meta_array[type] = content - # reset the text to speech flag and clipping limit, if present - if type == 401 and size == 9: - # set clipping limit to 100% - self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8) - elif type == 404 and size == 9: - # make sure text to speech is enabled - self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8) - # print type, size, content, content.encode('hex') - pos += size - except: - self.meta_array = {} - pass - self.print_replica = False - - def getBookTitle(self): - codec_map = { - 1252 : 'windows-1252', - 65001 : 'utf-8', - } - title = '' - codec = 'windows-1252' - if self.magic == 'BOOKMOBI': - if 503 in self.meta_array: - title = self.meta_array[503] - else: - toff, tlen = struct.unpack('>II', self.sect[0x54:0x5c]) - tend = toff + tlen - title = self.sect[toff:tend] - if self.mobi_codepage in codec_map.keys(): - codec = codec_map[self.mobi_codepage] - if title == '': - title = self.header[:32] - title = title.split("\0")[0] - return unicode(title, codec).encode('utf-8') - - def getPIDMetaInfo(self): - rec209 = '' - token = '' - if 209 in self.meta_array: - rec209 = self.meta_array[209] - data = rec209 - # The 209 data comes in five byte groups. Interpret the last four bytes - # of each group as a big endian unsigned integer to get a key value - # if that key exists in the meta_array, append its contents to the token - for i in xrange(0,len(data),5): - val, = struct.unpack('>I',data[i+1:i+5]) - sval = self.meta_array.get(val,'') - token += sval - return rec209, token - - def patch(self, off, new): - self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):] - - def patchSection(self, section, new, in_off = 0): - if (section + 1 == self.num_sections): - endoff = len(self.data_file) - else: - endoff = self.sections[section + 1][0] - off = self.sections[section][0] - assert off + in_off + len(new) <= endoff - self.patch(off + in_off, new) - - def parseDRM(self, data, count, pidlist): - found_key = None - keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96" - for pid in pidlist: - bigpid = pid.ljust(16,'\0') - temp_key = PC1(keyvec1, bigpid, False) - temp_key_sum = sum(map(ord,temp_key)) & 0xff - found_key = None - for i in xrange(count): - verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30]) - if cksum == temp_key_sum: - cookie = PC1(temp_key, cookie) - ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie) - if verification == ver and (flags & 0x1F) == 1: - found_key = finalkey - break - if found_key != None: - break - if not found_key: - # Then try the default encoding that doesn't require a PID - pid = "00000000" - temp_key = keyvec1 - temp_key_sum = sum(map(ord,temp_key)) & 0xff - for i in xrange(count): - verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30]) - if cksum == temp_key_sum: - cookie = PC1(temp_key, cookie) - ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie) - if verification == ver: - found_key = finalkey - break - return [found_key,pid] - - def getMobiFile(self, outpath): - file(outpath,'wb').write(self.mobi_data) - - def getMobiVersion(self): - return self.mobi_version - - def getPrintReplica(self): - return self.print_replica - - def processBook(self, pidlist): - crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2]) - print 'Crypto Type is: ', crypto_type - self.crypto_type = crypto_type - if crypto_type == 0: - print "This book is not encrypted." - # we must still check for Print Replica - self.print_replica = (self.loadSection(1)[0:4] == '%MOP') - self.mobi_data = self.data_file - return - if crypto_type != 2 and crypto_type != 1: - raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type) - if 406 in self.meta_array: - data406 = self.meta_array[406] - val406, = struct.unpack('>Q',data406) - if val406 != 0: - raise DrmException("Cannot decode library or rented ebooks.") - - goodpids = [] - for pid in pidlist: - if len(pid)==10: - if checksumPid(pid[0:-2]) != pid: - print "Warning: PID " + pid + " has incorrect checksum, should have been "+checksumPid(pid[0:-2]) - goodpids.append(pid[0:-2]) - elif len(pid)==8: - goodpids.append(pid) - - if self.crypto_type == 1: - t1_keyvec = "QDCVEPMU675RUBSZ" - if self.magic == 'TEXtREAd': - bookkey_data = self.sect[0x0E:0x0E+16] - elif self.mobi_version < 0: - bookkey_data = self.sect[0x90:0x90+16] - else: - bookkey_data = self.sect[self.mobi_length+16:self.mobi_length+32] - pid = "00000000" - found_key = PC1(t1_keyvec, bookkey_data) - else : - # calculate the keys - drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16]) - if drm_count == 0: - raise DrmException("Not yet initialised with PID. Must be opened with Mobipocket Reader first.") - found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids) - if not found_key: - raise DrmException("No key found in " + str(len(goodpids)) + " keys tried. Read the FAQs at Alf's blog. Only if none apply, report this failure for help.") - # kill the drm keys - self.patchSection(0, "\0" * drm_size, drm_ptr) - # kill the drm pointers - self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8) - - if pid=="00000000": - print "File has default encryption, no specific PID." - else: - print "File is encoded with PID "+checksumPid(pid)+"." - - # clear the crypto type - self.patchSection(0, "\0" * 2, 0xC) - - # decrypt sections - print "Decrypting. Please wait . . .", - mobidataList = [] - mobidataList.append(self.data_file[:self.sections[1][0]]) - for i in xrange(1, self.records+1): - data = self.loadSection(i) - extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags) - if i%100 == 0: - print ".", - # print "record %d, extra_size %d" %(i,extra_size) - decoded_data = PC1(found_key, data[0:len(data) - extra_size]) - if i==1: - self.print_replica = (decoded_data[0:4] == '%MOP') - mobidataList.append(decoded_data) - if extra_size > 0: - mobidataList.append(data[-extra_size:]) - if self.num_sections > self.records+1: - mobidataList.append(self.data_file[self.sections[self.records+1][0]:]) - self.mobi_data = "".join(mobidataList) - print "done" - return - -def getUnencryptedBook(infile,pid,announce=True): - if not os.path.isfile(infile): - raise DrmException('Input File Not Found') - book = MobiBook(infile,announce) - book.processBook([pid]) - return book.mobi_data - -def getUnencryptedBookWithList(infile,pidlist,announce=True): - if not os.path.isfile(infile): - raise DrmException('Input File Not Found') - book = MobiBook(infile, announce) - book.processBook(pidlist) - return book.mobi_data - - -def main(argv=sys.argv): - print ('MobiDeDrm v%(__version__)s. ' - 'Copyright 2008-2012 The Dark Reverser et al.' % globals()) - if len(argv)<3 or len(argv)>4: - print "Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks" - print "Usage:" - print " %s []" % sys.argv[0] - return 1 - else: - infile = argv[1] - outfile = argv[2] - if len(argv) is 4: - pidlist = argv[3].split(',') - else: - pidlist = {} - try: - stripped_file = getUnencryptedBookWithList(infile, pidlist, False) - file(outfile, 'wb').write(stripped_file) - except DrmException, e: - print "Error: %s" % e - return 1 - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/Other_Tools/KindleBooks/lib/scrolltextwidget.py b/Other_Tools/KindleBooks/lib/scrolltextwidget.py deleted file mode 100644 index 98b4147..0000000 --- a/Other_Tools/KindleBooks/lib/scrolltextwidget.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab - -import Tkinter -import Tkconstants - -# basic scrolled text widget -class ScrolledText(Tkinter.Text): - def __init__(self, master=None, **kw): - self.frame = Tkinter.Frame(master) - self.vbar = Tkinter.Scrollbar(self.frame) - self.vbar.pack(side=Tkconstants.RIGHT, fill=Tkconstants.Y) - kw.update({'yscrollcommand': self.vbar.set}) - Tkinter.Text.__init__(self, self.frame, **kw) - self.pack(side=Tkconstants.LEFT, fill=Tkconstants.BOTH, expand=True) - self.vbar['command'] = self.yview - # Copy geometry methods of self.frame without overriding Text - # methods = hack! - text_meths = vars(Tkinter.Text).keys() - methods = vars(Tkinter.Pack).keys() + vars(Tkinter.Grid).keys() + vars(Tkinter.Place).keys() - methods = set(methods).difference(text_meths) - for m in methods: - if m[0] != '_' and m != 'config' and m != 'configure': - setattr(self, m, getattr(self.frame, m)) - - def __str__(self): - return str(self.frame) diff --git a/Other_Tools/KindleBooks/lib/stylexml2css.py b/Other_Tools/KindleBooks/lib/stylexml2css.py deleted file mode 100644 index 2347f6a..0000000 --- a/Other_Tools/KindleBooks/lib/stylexml2css.py +++ /dev/null @@ -1,266 +0,0 @@ -#! /usr/bin/python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab -# For use with Topaz Scripts Version 2.6 - -import csv -import sys -import os -import getopt -import re -from struct import pack -from struct import unpack - - -class DocParser(object): - def __init__(self, flatxml, fontsize, ph, pw): - self.flatdoc = flatxml.split('\n') - self.fontsize = int(fontsize) - self.ph = int(ph) * 1.0 - self.pw = int(pw) * 1.0 - - stags = { - 'paragraph' : 'p', - 'graphic' : '.graphic' - } - - attr_val_map = { - 'hang' : 'text-indent: ', - 'indent' : 'text-indent: ', - 'line-space' : 'line-height: ', - 'margin-bottom' : 'margin-bottom: ', - 'margin-left' : 'margin-left: ', - 'margin-right' : 'margin-right: ', - 'margin-top' : 'margin-top: ', - 'space-after' : 'padding-bottom: ', - } - - attr_str_map = { - 'align-center' : 'text-align: center; margin-left: auto; margin-right: auto;', - 'align-left' : 'text-align: left;', - 'align-right' : 'text-align: right;', - 'align-justify' : 'text-align: justify;', - 'display-inline' : 'display: inline;', - 'pos-left' : 'text-align: left;', - 'pos-right' : 'text-align: right;', - 'pos-center' : 'text-align: center; margin-left: auto; margin-right: auto;', - } - - - # find tag if within pos to end inclusive - def findinDoc(self, tagpath, pos, end) : - result = None - docList = self.flatdoc - cnt = len(docList) - if end == -1 : - end = cnt - else: - end = min(cnt,end) - foundat = -1 - for j in xrange(pos, end): - item = docList[j] - if item.find('=') >= 0: - (name, argres) = item.split('=',1) - else : - name = item - argres = '' - if name.endswith(tagpath) : - result = argres - foundat = j - break - return foundat, result - - - # return list of start positions for the tagpath - def posinDoc(self, tagpath): - startpos = [] - pos = 0 - res = "" - while res != None : - (foundpos, res) = self.findinDoc(tagpath, pos, -1) - if res != None : - startpos.append(foundpos) - pos = foundpos + 1 - return startpos - - # returns a vector of integers for the tagpath - def getData(self, tagpath, pos, end, clean=False): - if clean: - digits_only = re.compile(r'''([0-9]+)''') - argres=[] - (foundat, argt) = self.findinDoc(tagpath, pos, end) - if (argt != None) and (len(argt) > 0) : - argList = argt.split('|') - for strval in argList: - if clean: - m = re.search(digits_only, strval) - if m != None: - strval = m.group() - argres.append(int(strval)) - return argres - - def process(self): - - classlst = '' - csspage = '.cl-center { text-align: center; margin-left: auto; margin-right: auto; }\n' - csspage += '.cl-right { text-align: right; }\n' - csspage += '.cl-left { text-align: left; }\n' - csspage += '.cl-justify { text-align: justify; }\n' - - # generate a list of each \n' - final += '\n\n' - in_tags = [] - st_tags = [] - - def inSet(slist): - rval = False - j = len(in_tags) - if j == 0: - return False - while True: - j = j - 1 - if in_tags[j][0] in slist: - rval = True - break - if j == 0: - break - return rval - - def inBlock(): - return inSet(self.html_block_tags) - - def inLink(): - return inSet(self.html_link_tags) - - def inComment(): - return inSet(self.html_comment_tags) - - def inParaNow(): - j = len(in_tags) - if j == 0: - return False - if in_tags[j-1][0] == 'P': - return True - return False - - def getTag(ti, end): - cmd, attr = ti - r = self.html_tags[cmd][end] - if type(r) != str: - r = r(attr) - return r - - def getSTag(ti, end): - cmd, attr = ti - r = self.html_style_tags[cmd][end] - if type(r) != str: - r = r(attr) - return r - - def applyStyles(ending): - s = '' - j = len(st_tags) - if j > 0: - if ending: - while True: - j = j - 1 - s += getSTag(st_tags[j], True) - if j == 0: - break - else: - k = 0 - while True: - s += getSTag(st_tags[k], False) - k = k + 1 - if k == j: - break - return s - - def indentLevel(line_start): - nb = 0 - while line_start[nb:nb+1] == ' ': - nb = nb + 1 - line_start = line_start[nb:] - if nb > 5: - nb = 5 - return nb, line_start - - - def makeText(s): - # handle replacements required for html - s = s.replace('&', '&') - s = s.replace('<', '<') - s = s.replace('>', '>') - return_s ='' - # parse the text line by line - lp = s.find('\n') - while lp != -1: - line = s[0:lp] - s = s[lp+1:] - if not inBlock() and not inLink() and not inComment(): - if len(line) > 0: - # text should not exist in the tag level unless it is in a comment - nb, line = indentLevel(line) - return_s += '

' % nb - return_s += applyStyles(False) - return_s += line - return_s += applyStyles(True) - return_s += '

\n' - else: - return_s += '

 

\n' - elif inParaNow(): - # text is a continuation of a previously started paragraph - return_s += line - return_s += applyStyles(True) - return_s += '

\n' - j = len(in_tags) - del in_tags[j-1] - else: - if len(line) > 0: - return_s += line + '
\n' - else: - return_s += '
\n' - lp = s.find('\n') - linefrag = s - if len(linefrag) > 0: - if not inBlock() and not inLink() and not inComment(): - nb, linefrag = indentLevel(linefrag) - return_s += '

' % nb - return_s += applyStyles(False) - return_s += linefrag - ppair = ('P', None) - in_tags.append(ppair) - else: - return_s += linefrag - return return_s - - while True: - r = self.next() - if not r: - break - text, cmd, attr = r - - if text: - final += makeText(text) - - if cmd: - - # handle pseudo paragraph P tags - # close if starting a new block element - if cmd in self.html_block_tags or cmd == 'w': - j = len(in_tags) - if j > 0: - if in_tags[j-1][0] == 'P': - final += applyStyles(True) - final += getTag(in_tags[j-1],True) - del in_tags[j-1] - - if cmd in self.html_block_tags: - pair = (cmd, attr) - if cmd not in [a for (a,b) in in_tags]: - # starting a new block tag - final += getTag(pair, False) - final += applyStyles(False) - in_tags.append(pair) - else: - # process ending tag for a tag pair - # ending tag should be for the most recently added start tag - j = len(in_tags) - if cmd == in_tags[j-1][0]: - final += applyStyles(True) - final += getTag(in_tags[j-1], True) - del in_tags[j-1] - else: - # ow: things are not properly nested - # process ending tag for block - # ending tag **should** be for the most recently added block tag - # but in too many cases it is not so we must fix this by - # closing all open tags up to the current one and then - # reopen all of the tags we had to close due to improper nesting of styles - print 'Warning: Improperly Nested Block Tags: expected %s found %s' % (cmd, in_tags[j-1][0]) - print 'after processing %s' % final[-40:] - j = len(in_tags) - while True: - j = j - 1 - final += applyStyles(True) - final += getTag(in_tags[j], True) - if in_tags[j][0] == cmd: - break - del in_tags[j] - # now create new block start tags if they were previously open - while j < len(st_tags): - final += getTag(in_tags[j], False) - final += applyStyles(False) - j = j + 1 - self.skipNewLine() - - elif cmd in self.html_link_tags: - pair = (cmd, attr) - if cmd not in [a for (a,b) in in_tags]: - # starting a new link tag - # first close out any still open styles - if inBlock(): - final += applyStyles(True) - # output start tag and styles needed - final += getTag(pair, False) - final += applyStyles(False) - in_tags.append(pair) - else: - # process ending tag for a tag pair - # ending tag should be for the most recently added start tag - j = len(in_tags) - if cmd == in_tags[j-1][0]: - j = len(in_tags) - # apply closing styles and tag - final += applyStyles(True) - final += getTag(in_tags[j-1], True) - # if needed reopen any style tags - if inBlock(): - final += applyStyles(False) - del in_tags[j-1] - else: - # ow: things are not properly nested - print 'Error: Improperly Nested Link Tags: expected %s found %s' % (cmd, in_tags[j-1][0]) - print 'after processing %s' % final[-40:] - - elif cmd in self.html_style_tags: - spair = (cmd, attr) - if cmd not in [a for (a,b) in st_tags]: - # starting a new style - if inBlock() or inLink(): - final += getSTag(spair,False) - st_tags.append(spair) - else: - # process ending tag for style - # ending tag **should** be for the most recently added style tag - # but in too many cases it is not so we must fix this by - # closing all open tags up to the current one and then - # reopen all of the tags we had to close due to improper nesting of styles - j = len(st_tags) - while True: - j = j - 1 - if inBlock() or inLink(): - final += getSTag(st_tags[j], True) - if st_tags[j][0] == cmd: - break - del st_tags[j] - # now create new style start tags if they were previously open - while j < len(st_tags): - if inBlock() or inLink(): - final += getSTag(st_tags[j], False) - j = j + 1 - - elif cmd in self.html_one_tags: - final += self.html_one_tags[cmd] - - elif cmd == 'p': - # create page breaks at the level so - # they can be easily used for safe html file segmentation breakpoints - # first close any open tags - j = len(in_tags) - if j > 0: - while True: - j = j - 1 - if in_tags[j][0] in self.html_block_tags: - final += applyStyles(True) - final += getTag(in_tags[j], True) - if j == 0: - break - - # insert the page break tag - final += '\n

\n' - - if sigil_breaks: - if (len(final) - lastbreaksize) > 3000: - final += '
\n' - lastbreaksize = len(final) - - # now create new start tags for all tags that - # were previously open - while j < len(in_tags): - final += getTag(in_tags[j], False) - if in_tags[j][0] in self.html_block_tags: - final += applyStyles(False) - j = j + 1 - self.skipNewLine() - - elif cmd[0:1] == 'C': - if self.markChapters: - # create toc entries at the level - # since they will be in an invisible block - # first close any open tags - j = len(in_tags) - if j > 0: - while True: - j = j - 1 - if in_tags[j][0] in self.html_block_tags: - final += applyStyles(True) - final += getTag(in_tags[j], True) - if j == 0: - break - level = int(cmd[1:2]) + 1 - final += '' % (level, attr, level) - # now create new start tags for all tags that - # were previously open - while j < len(in_tags): - final += getTag(in_tags[j], False) - if in_tags[j][0] in self.html_block_tags: - final += applyStyles(False) - j = j + 1 - else: - final += '' % (cmd[1:2], attr) - - # now handle single tags (non-paired) that have attributes - elif cmd == 'm': - unquotedimagepath = bookname + '_img/' + attr - imagepath = urllib.quote( unquotedimagepath ) - final += '' % imagepath - - elif cmd == 'Q': - final += ' ' % attr - - elif cmd == 'a': - if not inBlock() and not inLink() and not inComment(): - final += '

' - final += applyStyles(False) - final += self.pml_chars.get(attr, '&#%d;' % attr) - ppair = ('P', None) - in_tags.append(ppair) - else: - final += self.pml_chars.get(attr, '&#%d;' % attr) - - elif cmd == 'U': - if not inBlock() and not inLink() and not inComment(): - final += '

' - final += applyStyles(False) - final += '&#%d;' % attr - ppair = ('P', None) - in_tags.append(ppair) - else: - final += makeText('&#%d;' % attr) - - elif cmd == 'w': - # hr width and align parameters are not allowed in strict xhtml but style widths are possible - final += '\n


' % attr - # final += '
 
' % attr - self.skipNewLine() - - elif cmd == 'T': - if inBlock() or inLink() or inComment(): - final += ' ' % attr - else: - final += '

' % attr - final += applyStyles(False) - ppair = ('P', None) - in_tags.append(ppair) - - else: - logging.warning("Unknown tag: %s-%s", cmd, attr) - - - # handle file ending condition for imputed P tags - j = len(in_tags) - if (j > 0): - if in_tags[j-1][0] == 'P': - final += '

' - - final += '\n\n' - - # recode html back to a single slash - final = final.replace('_amp#92_', '\\') - - # cleanup the html code for issues specifically generated by this translation process - # ending divs already break the line at the end so we don't need the
we added - final = final.replace('
\n','\n') - - # clean up empty elements that can be created when fixing improperly nested pml tags - # and by moving page break tags to the body level so that they can be used as html file split points - while True: - s = final - final = final.replace('','') - final = final.replace('','') - final = final.replace('','') - final = final.replace('','') - final = final.replace('','') - final = final.replace('','') - final = final.replace('','') - final = final.replace('','') - final = final.replace(' ','') - final = final.replace(' ','') - final = final.replace(' ','') - final = final.replace('

','') - final = final.replace('

','') - final = final.replace('

','') - final = final.replace('

','') - final = final.replace('

','') - final = final.replace('

','') - final = final.replace('

','') - final = final.replace('

','') - final = final.replace('

\n','') - final = final.replace('

\n','') - final = final.replace('

\n','') - final = final.replace('

\n','') - final = final.replace('
\n','') - final = final.replace('
\n','') - final = final.replace('
\n','') - final = final.replace('
\n','') - final = final.replace('
\n','') - if s == final: - break - return final - - -def tidy(rawhtmlfile): - # processes rawhtmlfile through command line tidy via pipes - rawfobj = file(rawhtmlfile,'rb') - # --doctype strict forces strict dtd checking - # --enclose-text yes - enclosees non-block electment text inside into its own

block to meet xhtml spec - # -w 100 -i will wrap text at column 120 and indent it to indicate level of nesting to make structure clearer - # -win1252 sets the input encoding of pml files - # -asxhtml convert to xhtml - # -q (quiet) - cmdline = 'tidy -w 120 -i -q -asxhtml -win1252 --enclose-text yes --doctype strict ' - if sys.platform[0:3] == 'win': - cmdline = 'tidy.exe -w 120 -i -q -asxhtml -win1252 --enclose-text yes --doctype strict ' - p2 = Popen(cmdline, shell=True, stdin=rawfobj, stdout=PIPE, stderr=PIPE, close_fds=False) - stdout, stderr = p2.communicate() - # print "Tidy Original Conversion Warnings and Errors" - # print stderr - return stdout - -def usage(): - print "Converts PML file to XHTML" - print "Usage:" - print " xpml2xhtml [options] infile.pml outfile.html " - print " " - print "Options: " - print " -h prints this message" - print " --sigil-breaks insert Sigil Chapterbbreaks" - print " --use-tidy use tidy to further clean up the html " - print " " - return - -def main(argv=None): - global bookname - global footnote_ids - global sidebar_ids - global sigil_breaks - try: - opts, args = getopt.getopt(sys.argv[1:], "h", ["sigil-breaks", "use-tidy"]) - except getopt.GetoptError, err: - print str(err) - usage() - return 1 - if len(args) != 2: - usage() - return 1 - sigil_breaks = False - use_tidy = False - for o, a in opts: - if o == "-h": - usage() - return 0 - elif o == "--sigil-breaks": - sigil_breaks = True - elif o == "--use-tidy": - use_tidy = True - infile, outfile = args[0], args[1] - bookname = os.path.splitext(os.path.basename(infile))[0] - footnote_ids = { } - sidebar_ids = { } - try: - print "Processing..." - import time - start_time = time.time() - print " Converting pml to raw html" - pml_string = file(infile,'rb').read() - pml = PmlConverter(pml_string) - html_src = pml.process() - if use_tidy: - print " Tidying html to xhtml" - fobj = tempfile.NamedTemporaryFile(mode='w+b',suffix=".html",delete=False) - tempname = fobj.name - fobj.write(html_src) - fobj.close() - html_src = tidy(tempname) - os.remove(tempname) - file(outfile,'wb').write(html_src) - end_time = time.time() - convert_time = end_time - start_time - print 'elapsed time: %.2f seconds' % (convert_time, ) - print 'output is in file %s' % outfile - print "Finished Processing" - except ValueError, e: - print "Error: %s" % e - return 1 - return 0 - -if __name__ == "__main__": - #import cProfile - #command = """sys.exit(main())""" - #cProfile.runctx( command, globals(), locals(), filename="cprofile.profile" ) - - sys.exit(main()) diff --git a/ReadMe_First.txt b/ReadMe_First.txt index 6509d0b..6bf798a 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.4.1 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.5 archive. The is archive includes tools to remove DRM from: @@ -24,7 +24,7 @@ You can find the latest updates and get support at Apprentice Alf's blog: http:/ If you re-post these tools, a link to the blog would be appreciated. -The original inept and ignoble scripts were by I♥cabbages +The original inept and ignoble scripts were by i♥cabbages The original mobidedrm and erdr2pml scripts were by The Dark Reverser The original topaz DRM removal script was by CMBDTC The original topaz format conversion scripts were by some_updates, clarknova and Bart Simpson @@ -39,7 +39,7 @@ Many fixes, updates and enhancements to the scripts and applicatons have been by Calibre Users (Mac OS X, Windows, and Linux) -------------------------------------------- -If you are a calibre user, the quickest and easiest way, especially on Windows, to remove DRM from your ebooks is to install each of the plugins in the Calibre_Plugins folder, following the instructions and configuration directions provided in each plugin's ReadMe file. +If you are a calibre user, the quickest and easiest way, especially on Windows, to remove DRM from your ebooks is to install the relevant plugins from the Calibre_Plugins folder, following the instructions and configuration directions provided in each plugin's ReadMe file. Once installed and configured, you can simply add a DRM book to calibre and the DeDRMed version will be imported into the calibre database. Note that DRM removal ONLY occurs on import. If you have already imported DRM books you'll need to remove them from calibre and re-import them. @@ -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.4.1.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.5.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. @@ -60,14 +60,14 @@ For more detailed instructions, see the "DeDRM ReadMe.rtf" file in the DeDRM_App -DeDRM application for Windows users: (Windows XP through Windows 7) +DeDRM application for Windows users: (Windows XP through Windows 8) ------------------------------------------------------------------ ***This program requires that Python and PyCrypto be properly installed.*** ***See below for details on recommended versions are where to get them.*** This application combines all the tools into one easy-to-use tool for Windows users. -Drag the DeDRM_5.4.1 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.4.1 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.5 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 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. @@ -77,233 +77,63 @@ For more detailed instructions, see the DeDRM_ReadMe.txt file in the DeDRM_Appli Other_Tools ----------- -This folder includes two non-python tools: +This folder includes three non-python tools: Kindle_for_Android_Patches --------------------------- + Definitely only for the adventurous, this folder contains information on how to modify the Kindel for Android app to b able to get a PID for use with the other Kindle tools (DeDRM apps and calibre plugin). B&N_Download_Helper -------------------- + A Javascript to enable a download button at the B&N website for ebooks that normally won't download to your PC. Another one only for the adventurous. - -And then there are a number of other python based tools that have graphical user interfaces to make them easy to use. To use any of these tools, you need to have Python 2.5, 2.6, or 2.7 for 32 bits installed on your machine as well as a matching PyCrypto or OpenSSL for some tools. - -On Mac OS X (10.5, 10.6 and 10.7), your systems already have the proper Python and OpenSSL installed. So nothing need be done, you can already run these tools by double-clicking on the .pyw python scripts. - -Users of Mac OS X 10.3 and 10.4, need to download and install the "32-bit Mac Installer disk Image (2.7.3) for OS X 10.3 and later from http://www.python.org/ftp/python/2.7.3/python-2.7.3-macosx10.3.dmg. - -On Windows, you need to install a 32 bit version of Python (even on Windows 64) plus a matching 32 bit version of PyCrypto *OR* OpenSSL. We ***strongly*** recommend the free community edition of ActiveState's Active Python version. See the end of this document for details. - -Linux users should have python 2.7, and openssl installed, but may need to run some of these tools under recent versions of Wine. See the Linux_Users section below: - -The scripts in the Other_Tools folder are organized by type of ebook you need to remove the DRM from. Choose from among: - - "Adobe_ePub_Tools" - "Adobe_PDF_Tools" - "Barnes_and_Noble_ePub_Tools" - "ePub_Fixer" (for fixing incorrectly made Adobe and Barnes and Noble ePubs) - "eReader_PDB_Tools" - "Kindle/Mobi_Tools" - "KindleBooks" - -by simply opening that folder. - -Look for a README inside of the relevant folder to get you started. - - - -Additional Tools ----------------- -Some additional useful tools **unrelated to DRM** are also provided in the "Additional_Tools" folder inside the "Other_Tools" folder. There are tools for working with finding Topaz ebooks, unpacking Kindle/Mobipocket ebooks (without DRM) to get to the Mobipocket markup language inside, tools to strip source archive from Kindlegen generated mobis, tools to work with Kindle for iPhone/iPad, etc, and tools to dump the contents of mobi headers to see all EXTH (metadata) and related values. - - Scuolabook_DRM -------------- -This is a Windows-only tool produced by Hex and included with permission. + +A windows-only application (including source code) for removing DRM from ScuolaBooks PDFs, created by "Hex" and included with permission. + + Windows and Python ------------------ We **strongly** recommend ActiveState's Active Python 2.7 Community Edition for Windows (x86) 32 bits. This can be downloaded for free from: - http://www.activestate.com/activepython/downloads +http://www.activestate.com/activepython/downloads We do **NOT** recommend the version of Python from python.org. The version from python.org is not as complete as most normal Python installations on Linux and even Mac OS X. It is missing various Windows specific libraries, does not install the default Tk Widget kit (for graphical user interfaces) unless you select it as an option in the installer, and does not properly update the system PATH environment variable. Therefore using the default python.org build on Windows is simply an exercise in frustration for most Windows users. -In addition, Windows Users need one of PyCrypto OR OpenSSL. - -For OpenSSL: - - Win32 OpenSSL v0.9.8o (8Mb) - http://www.slproweb.com/download/Win32OpenSSL-0_9_8o.exe - (if you get an error message about missing Visual C++ - redistributables... cancel the install and install the - below support program from Microsoft, THEN install OpenSSL) - - Visual C++ 2008 Redistributables (1.7Mb) - http://www.microsoft.com/downloads/details.aspx?familyid=9B2DA534-3E03-4391-8A4D-074B9F2BC1BF +In addition, Windows Users need one of PyCrypto OR OpenSSL. Because of potential conflicts with other software, we recommend using PyCrypto. For PyCrypto: - There are many places to get PyCrypto installers for Windows. One such place is: + There are many places to get PyCrypto installers for Windows. One such place is: - http://www.voidspace.org.uk/python/modules.shtml + http://www.voidspace.org.uk/python/modules.shtml - Please get the latest PyCrypto meant for Windows 32 bit that matches the version of Python you installed (2.7) + Please get the latest PyCrypto meant for Windows 32 bit that matches the version of Python you installed (2.7) -Once Windows users have installed Python 2.X for 32 bits, and the matching OpenSSL OR PyCrypto pieces, they too are ready to run the scripts. +For OpenSSL: + Win32 OpenSSL v0.9.8o (8Mb) + http://www.slproweb.com/download/Win32OpenSSL-0_9_8o.exe + (if you get an error message about missing Visual C++ + redistributables... cancel the install and install the + below support program from Microsoft, THEN install OpenSSL) + Visual C++ 2008 Redistributables (1.7Mb) + http://www.microsoft.com/downloads/details.aspx?familyid=9B2DA534-3E03-4391-8A4D-074B9F2BC1BF +Once Windows users have installed Python 2.X for 32 bits, and the matching OpenSSL OR PyCrypto pieces, they too are ready to run a DeDRM application. -Linux Users Only -================ - -Since Kindle for PC and Adobe Digital Editions do not offer native Linux versions, here are instructions for using Windows versions under Wine as well as related instructions for the special way to handle some of these tools: - - - -Linux and Kindle for PC ------------------------ - -It is possible to run the Kindle for PC application under Wine. - -1. Install a recent version of Wine (>=1.3.15) - -2. Some versions of winecfg have a bug in setting the volume serial number, so create a .windows-serial file at root of drive_c to set a proper windows volume serial number (8 digit hex value for unsigned integer). -cd ~ -cd .wine -cd drive_c -echo deadbeef > .windows-serial - -Replace "deadbeef" with whatever hex value you want but I would stay away from the default setting of "ffffffff" which does not seem to work. BTW: deadbeef is itself a valid possible hex value if you want to use it - -3. Download and install Kindle for PC under Wine. - - - - -Linux and Kindle for PC (Other_Tools/KindleBooks/) --------------------------------------------------- - -Here are the instructions for using Kindle for PC and KindleBooks.pyw on Linux under Wine. (Thank you Eyeless and Pete) - -1. upgrade to very recent versions of Wine; This has been tested with Wine 1.3.15 – 1.3.2X. It may work with earlier versions but no promises. It does not work with wine 1.2.X versions. - -If you have not already installed Kindle for PC under wine, follow steps 2 and 3 otherwise jump to step 4 - -2. Some versions of winecfg have a bug in setting the volume serial number, so create a .windows-serial file at root of drive_c to set a proper windows volume serial number (8 digit hex value for unsigned integer). -cd ~ -cd .wine -cd drive_c -echo deadbeef > .windows-serial - -Replace "deadbeef" with whatever hex value you want but I would stay away from the default setting of "ffffffff" which does not seem to work. BTW: deadbeef is itself a valid possible hex value if you want to use it - -3. Only ***after*** setting the volume serial number properly – download and install under wine K4PC version for Windows. Register it and download from your Archive one of your Kindle ebooks. Versions known to work are K4PC 1.7.1 and earlier. Later version may work but no promises. - -4. Download and install under wine ActiveState Active Python 2.7 for Windows 32bit - -5. Download and unzip tools_vX.X.zip - -6. Now make sure the executable bit is NOT set for KindleBooks.pyw as Linux will actually keep trying to ignore wine and launch it under Linux python which will cause it to fail. - -cd tools_vX.X/KindleBooks/ -chmod ugo-x KindleBooks.pyw - -7. Then run KindleBook.pyw ***under python running on wine*** using the Linux shell as follows: - -wine python KindleBooks.pyw - -Select the ebook file directly from your “My Kindle Content” folder, select a new/unused directory for the output. You should not need to enter any PID or Serial Number for Kindle for PC. - - - - -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!) - - -1. download the most recent version of wine from winehq.org (1.3.29 in my case) - -For debian users: - -to get a recent version of wine I decited to use aptosid (2011-02, xfce) -(because I’m used to debian) -install aptosid and upgrade it (see aptosid site for detaild instructions) - - -2. properly install Wine (see the Wine site for details) - -For debian users: - -cd to this dir and install the packages as root: -‘dpkg -i *.deb’ -you will get some error messages, which can be ignored. -again as root use -‘apt-get -f install’ to correct this errors - -3. python 2.7 should already be installed on your system but you may need the following additional python package - -'apt-get install python-tk’ - -4. all programms need to be installed as normal user. All these programm are installed the same way: -‘wine ‘ -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) - -b) ActivePython-2.7.2.5-win32-x86.msi (from: http://www.activestate.com/activepython/downloads) - -c) Win32OpenSSL_Light-0_9_8r.exe (from: http://www.slproweb.com/) - -d) pycrypto-2.3.win32-py2.7.msi (from: http://www.voidspace.org.uk/python/modules.shtml) - -5. now get and unpack the very latest tools_vX.X (from Apprentice Alf) in the users drive_c of wine -(~/.wine/drive_c/) - -6. start ADE with: -‘wine digitaleditions.exe’ or from the start menue wine-adobe-digital.. - -7. register this instance of ADE with your adobeID and close it - change to the tools_vX.X dir: -cd ~/.wine/drive_c/tools_vX.X/Other_Tools/Adobe_ePub_Tools - -8. create the adeptkey.der with: -‘wine python ineptkey_v5.4.1.pyw’ (only need once!) -(key will be here: ~/.wine/drive_c/tools_v4.X/Other_Tools/Adobe_ePub_Tools/adeptkey.der) - -9. Use ADE running under Wine to dowload all of your purchased ePub ebooks - -10. for each book you have downloaded via Adobe Digital Editions -There is no need to use Wine for this step! - -'python ineptpub_v5.6.pyw’ -this will launch a window with 3 lines -1. key: (allready filled in, otherwise it’s in the path where you did step 8. -2. input file: drmbook.epub -3. output file: name-ypu-want_for_free_book.epub - -Also… once you successfully generate your adept.der keyfile using Wine, you can use the regular ineptepub plugin with the standard Linux calibre. Just put the *.der file(s) in your calibre configuration directory. -so if you want you can use calibre in Linux: - -11. install the plugins from the tools as discribed in the readmes for win - -12. copy the adeptkey.der into the config dir of calibre (~/.config/calibre in debian). Every book imported to calibre will automaticly freed from DRM. - Apple's iBooks FairPlay DRM --------------------------- -The only tool that removes Apple's iBooks Fairplay DRM that is Requiem by Brahms version 3.3 or later. Requiem is NOT included in this tools package. It is under active development because Apple constantly updates its DRM scheme to stop Requiem from working. -The latest version as of October 2012 is 3.3.5 and works with iTunes 10.5 and above. +The only tool that removed Apple's iBooks Fairplay DRM was Requiem by Brahms version 3.3.x. Requiem is NOT included in this tools package. It is under active development because Apple constantly updates its DRM scheme to stop Requiem from working. +The latest version that worked was 3.3.5 and worked with iTunes 10.5 and above. Requiem 4.0 and later do not remove DRM from ebooks. Requiem has a Tor website: http://tag3ulp55xczs3pn.onion. To reach the site using Tor, you will need to install Tor (http://www.torproject.org). If you're willing to sacrifice your anonymity, you can use the regular web with tor2web. Just go to http://tag3ulp55xczs3pn.tor2web.com.