From d617822610c24644dc90dfc6f2012945197c0c00 Mon Sep 17 00:00:00 2001 From: Zhuoyun Wei Date: Mon, 7 May 2018 04:35:49 -0400 Subject: [PATCH 1/9] Move core source files into src/ --- .../DeDRM_Adobe Digital Editions Key_Help.htm | 0 .../DeDRM_Barnes and Noble Key_Help.htm | 0 .../DeDRM_EInk Kindle Serial Number_Help.htm | 0 .../DeDRM_plugin => src}/DeDRM_Help.htm | 0 .../DeDRM_Kindle for Android Key_Help.htm | 0 .../DeDRM_Kindle for Mac and PC Key_Help.htm | 0 .../DeDRM_Mobipocket PID_Help.htm | 0 .../DeDRM_plugin => src}/DeDRM_eReader Key_Help.htm | 0 .../DeDRM_plugin => src}/__init__.py | 0 .../DeDRM_plugin => src}/activitybar.py | 0 .../DeDRM_plugin => src}/adobekey.py | 0 .../DeDRM_plugin => src}/aescbc.py | 0 .../DeDRM_plugin => src}/alfcrypto.dll | Bin .../DeDRM_plugin => src}/alfcrypto.py | 0 .../DeDRM_plugin => src}/alfcrypto64.dll | Bin .../DeDRM_plugin => src}/alfcrypto_src.zip | Bin .../DeDRM_plugin => src}/androidkindlekey.py | 0 .../DeDRM_plugin => src}/argv_utils.py | 0 .../DeDRM_plugin => src}/askfolder_ed.py | 0 .../DeDRM_plugin => src}/config.py | 0 .../DeDRM_plugin => src}/convert2xml.py | 0 .../DeDRM_plugin => src}/encodebase64.py | 0 .../DeDRM_plugin => src}/epubtest.py | 0 .../DeDRM_plugin => src}/erdr2pml.py | 0 .../DeDRM_plugin => src}/flatxml2html.py | 0 .../DeDRM_plugin => src}/flatxml2svg.py | 0 .../DeDRM_plugin => src}/genbook.py | 0 .../DeDRM_plugin => src}/ignobleepub.py | 0 .../DeDRM_plugin => src}/ignoblekey.py | 0 .../DeDRM_plugin => src}/ignoblekeyfetch.py | 0 .../DeDRM_plugin => src}/ignoblekeygen.py | 0 .../DeDRM_plugin => src}/ineptepub.py | 0 .../DeDRM_plugin => src}/ineptpdf.py | 0 {DeDRM_calibre_plugin/DeDRM_plugin => src}/ion.py | 0 .../DeDRM_plugin => src}/k4mobidedrm.py | 0 .../DeDRM_plugin => src}/kfxdedrm.py | 0 .../DeDRM_plugin => src}/kgenpids.py | 0 .../DeDRM_plugin => src}/kindlekey.py | 0 .../DeDRM_plugin => src}/kindlepid.py | 0 .../DeDRM_plugin => src}/libalfcrypto.dylib | Bin .../DeDRM_plugin => src}/libalfcrypto32.so | Bin .../DeDRM_plugin => src}/libalfcrypto64.so | Bin .../DeDRM_plugin => src}/mobidedrm.py | 0 .../DeDRM_plugin => src}/openssl_des.py | 0 .../plugin-import-name-dedrm.txt | 0 {DeDRM_calibre_plugin/DeDRM_plugin => src}/prefs.py | 0 .../DeDRM_plugin => src}/pycrypto_des.py | 0 .../DeDRM_plugin => src}/python_des.py | 0 .../DeDRM_plugin => src}/scriptinterface.py | 0 .../DeDRM_plugin => src}/scrolltextwidget.py | 0 .../DeDRM_plugin => src}/simpleprefs.py | 0 .../DeDRM_plugin => src}/stylexml2css.py | 0 .../DeDRM_plugin => src}/subasyncio.py | 0 .../DeDRM_plugin => src}/topazextract.py | 0 .../DeDRM_plugin => src}/utilities.py | 0 .../DeDRM_plugin => src}/wineutils.py | 0 .../DeDRM_plugin => src}/zipfilerugged.py | 0 .../DeDRM_plugin => src}/zipfix.py | 0 58 files changed, 0 insertions(+), 0 deletions(-) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/DeDRM_Adobe Digital Editions Key_Help.htm (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/DeDRM_Barnes and Noble Key_Help.htm (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/DeDRM_EInk Kindle Serial Number_Help.htm (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/DeDRM_Help.htm (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/DeDRM_Kindle for Android Key_Help.htm (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/DeDRM_Kindle for Mac and PC Key_Help.htm (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/DeDRM_Mobipocket PID_Help.htm (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/DeDRM_eReader Key_Help.htm (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/__init__.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/activitybar.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/adobekey.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/aescbc.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/alfcrypto.dll (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/alfcrypto.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/alfcrypto64.dll (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/alfcrypto_src.zip (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/androidkindlekey.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/argv_utils.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/askfolder_ed.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/config.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/convert2xml.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/encodebase64.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/epubtest.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/erdr2pml.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/flatxml2html.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/flatxml2svg.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/genbook.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/ignobleepub.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/ignoblekey.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/ignoblekeyfetch.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/ignoblekeygen.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/ineptepub.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/ineptpdf.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/ion.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/k4mobidedrm.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/kfxdedrm.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/kgenpids.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/kindlekey.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/kindlepid.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/libalfcrypto.dylib (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/libalfcrypto32.so (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/libalfcrypto64.so (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/mobidedrm.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/openssl_des.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/plugin-import-name-dedrm.txt (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/prefs.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/pycrypto_des.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/python_des.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/scriptinterface.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/scrolltextwidget.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/simpleprefs.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/stylexml2css.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/subasyncio.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/topazextract.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/utilities.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/wineutils.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/zipfilerugged.py (100%) rename {DeDRM_calibre_plugin/DeDRM_plugin => src}/zipfix.py (100%) diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/DeDRM_Adobe Digital Editions Key_Help.htm b/src/DeDRM_Adobe Digital Editions Key_Help.htm similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/DeDRM_Adobe Digital Editions Key_Help.htm rename to src/DeDRM_Adobe Digital Editions Key_Help.htm diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/DeDRM_Barnes and Noble Key_Help.htm b/src/DeDRM_Barnes and Noble Key_Help.htm similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/DeDRM_Barnes and Noble Key_Help.htm rename to src/DeDRM_Barnes and Noble Key_Help.htm diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/DeDRM_EInk Kindle Serial Number_Help.htm b/src/DeDRM_EInk Kindle Serial Number_Help.htm similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/DeDRM_EInk Kindle Serial Number_Help.htm rename to src/DeDRM_EInk Kindle Serial Number_Help.htm diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/DeDRM_Help.htm b/src/DeDRM_Help.htm similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/DeDRM_Help.htm rename to src/DeDRM_Help.htm diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/DeDRM_Kindle for Android Key_Help.htm b/src/DeDRM_Kindle for Android Key_Help.htm similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/DeDRM_Kindle for Android Key_Help.htm rename to src/DeDRM_Kindle for Android Key_Help.htm diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/DeDRM_Kindle for Mac and PC Key_Help.htm b/src/DeDRM_Kindle for Mac and PC Key_Help.htm similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/DeDRM_Kindle for Mac and PC Key_Help.htm rename to src/DeDRM_Kindle for Mac and PC Key_Help.htm diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/DeDRM_Mobipocket PID_Help.htm b/src/DeDRM_Mobipocket PID_Help.htm similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/DeDRM_Mobipocket PID_Help.htm rename to src/DeDRM_Mobipocket PID_Help.htm diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/DeDRM_eReader Key_Help.htm b/src/DeDRM_eReader Key_Help.htm similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/DeDRM_eReader Key_Help.htm rename to src/DeDRM_eReader Key_Help.htm diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/__init__.py b/src/__init__.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/__init__.py rename to src/__init__.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/activitybar.py b/src/activitybar.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/activitybar.py rename to src/activitybar.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/adobekey.py b/src/adobekey.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/adobekey.py rename to src/adobekey.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/aescbc.py b/src/aescbc.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/aescbc.py rename to src/aescbc.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/alfcrypto.dll b/src/alfcrypto.dll similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/alfcrypto.dll rename to src/alfcrypto.dll diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/alfcrypto.py b/src/alfcrypto.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/alfcrypto.py rename to src/alfcrypto.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/alfcrypto64.dll b/src/alfcrypto64.dll similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/alfcrypto64.dll rename to src/alfcrypto64.dll diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/alfcrypto_src.zip b/src/alfcrypto_src.zip similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/alfcrypto_src.zip rename to src/alfcrypto_src.zip diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/androidkindlekey.py b/src/androidkindlekey.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/androidkindlekey.py rename to src/androidkindlekey.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/argv_utils.py b/src/argv_utils.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/argv_utils.py rename to src/argv_utils.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/askfolder_ed.py b/src/askfolder_ed.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/askfolder_ed.py rename to src/askfolder_ed.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/config.py b/src/config.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/config.py rename to src/config.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/convert2xml.py b/src/convert2xml.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/convert2xml.py rename to src/convert2xml.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/encodebase64.py b/src/encodebase64.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/encodebase64.py rename to src/encodebase64.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/epubtest.py b/src/epubtest.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/epubtest.py rename to src/epubtest.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/erdr2pml.py b/src/erdr2pml.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/erdr2pml.py rename to src/erdr2pml.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/flatxml2html.py b/src/flatxml2html.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/flatxml2html.py rename to src/flatxml2html.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/flatxml2svg.py b/src/flatxml2svg.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/flatxml2svg.py rename to src/flatxml2svg.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/genbook.py b/src/genbook.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/genbook.py rename to src/genbook.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/ignobleepub.py b/src/ignobleepub.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/ignobleepub.py rename to src/ignobleepub.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/ignoblekey.py b/src/ignoblekey.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/ignoblekey.py rename to src/ignoblekey.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/ignoblekeyfetch.py b/src/ignoblekeyfetch.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/ignoblekeyfetch.py rename to src/ignoblekeyfetch.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/ignoblekeygen.py b/src/ignoblekeygen.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/ignoblekeygen.py rename to src/ignoblekeygen.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/ineptepub.py b/src/ineptepub.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/ineptepub.py rename to src/ineptepub.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/ineptpdf.py b/src/ineptpdf.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/ineptpdf.py rename to src/ineptpdf.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/ion.py b/src/ion.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/ion.py rename to src/ion.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/k4mobidedrm.py b/src/k4mobidedrm.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/k4mobidedrm.py rename to src/k4mobidedrm.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/kfxdedrm.py b/src/kfxdedrm.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/kfxdedrm.py rename to src/kfxdedrm.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/kgenpids.py b/src/kgenpids.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/kgenpids.py rename to src/kgenpids.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/kindlekey.py b/src/kindlekey.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/kindlekey.py rename to src/kindlekey.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/kindlepid.py b/src/kindlepid.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/kindlepid.py rename to src/kindlepid.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/libalfcrypto.dylib b/src/libalfcrypto.dylib similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/libalfcrypto.dylib rename to src/libalfcrypto.dylib diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/libalfcrypto32.so b/src/libalfcrypto32.so similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/libalfcrypto32.so rename to src/libalfcrypto32.so diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/libalfcrypto64.so b/src/libalfcrypto64.so similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/libalfcrypto64.so rename to src/libalfcrypto64.so diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/mobidedrm.py b/src/mobidedrm.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/mobidedrm.py rename to src/mobidedrm.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/openssl_des.py b/src/openssl_des.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/openssl_des.py rename to src/openssl_des.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/plugin-import-name-dedrm.txt b/src/plugin-import-name-dedrm.txt similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/plugin-import-name-dedrm.txt rename to src/plugin-import-name-dedrm.txt diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/prefs.py b/src/prefs.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/prefs.py rename to src/prefs.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/pycrypto_des.py b/src/pycrypto_des.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/pycrypto_des.py rename to src/pycrypto_des.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/python_des.py b/src/python_des.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/python_des.py rename to src/python_des.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/scriptinterface.py b/src/scriptinterface.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/scriptinterface.py rename to src/scriptinterface.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/scrolltextwidget.py b/src/scrolltextwidget.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/scrolltextwidget.py rename to src/scrolltextwidget.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/simpleprefs.py b/src/simpleprefs.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/simpleprefs.py rename to src/simpleprefs.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/stylexml2css.py b/src/stylexml2css.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/stylexml2css.py rename to src/stylexml2css.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/subasyncio.py b/src/subasyncio.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/subasyncio.py rename to src/subasyncio.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/topazextract.py b/src/topazextract.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/topazextract.py rename to src/topazextract.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/utilities.py b/src/utilities.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/utilities.py rename to src/utilities.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/wineutils.py b/src/wineutils.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/wineutils.py rename to src/wineutils.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/zipfilerugged.py b/src/zipfilerugged.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/zipfilerugged.py rename to src/zipfilerugged.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/zipfix.py b/src/zipfix.py similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin/zipfix.py rename to src/zipfix.py From 79b10f3dfb7a93081ffd6899a517e902885cfdef Mon Sep 17 00:00:00 2001 From: Zhuoyun Wei Date: Mon, 7 May 2018 04:36:59 -0400 Subject: [PATCH 2/9] Move calibre-related into contrib/calibre/ --- DeDRM_calibre_plugin/DeDRM_plugin.zip | Bin 363030 -> 0 bytes .../calibre}/DeDRM_plugin_ReadMe.txt | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 DeDRM_calibre_plugin/DeDRM_plugin.zip rename {DeDRM_calibre_plugin => contrib/calibre}/DeDRM_plugin_ReadMe.txt (100%) diff --git a/DeDRM_calibre_plugin/DeDRM_plugin.zip b/DeDRM_calibre_plugin/DeDRM_plugin.zip deleted file mode 100644 index 3c83c307eff09df6bc98e7c3af3749566d9f4e69..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 363030 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%0A~p$khH?c({{+ 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¨U#Q!gU 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%0D{49ZGp*V+% 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 z z)+QSffwBnQiHabaFilH+ylAlcbC1)3hpMs(00`Ln1S3lG;9Yn3yO-7!rayP0`1NPS(vEv)XYI1E zc|$xu947b+OQiTk_00p~&|BGUY4-t*Z7}uVZXvck8cc_x8w|zF0;_N(yd;BDz*Vvm zWDHdWMNVBjFA}ggc0Ac7jX2TTf=NwURttV`ckyDkWj99aYDBTzvJG)cV7PXvMJin9 zXWR^vRtYVRb)aV{Axy6514}Pxc5rn)eBb@){0cyJ_SEXtpgTOj7+!XJz0>ozu=PUi zD32vZX;f<}s?e3gDgahoO+rLEF#e(cv!&f@+Ict*c<5(1_TN~o)E~k%b4x`B`-WM2 z_^(p-Z`qD*+ieE0sDIXy0i}wIWL6PMuGPf4I^KJ(SaZz<880e-jbgnEl0wQUNhJM> zByz1T$)MXTg{T8W4BS*9H0Wv~n5ie0>|<>fxl>MMmy*IHhv$kwL(P;rL|tZME7rX1OST$(o>jZ0wQ)?(C9P#`B>( zO2(KXcCz%e8un{E2~P95)3!zjBBr893bc$>prajc)H*>~$%ha%BOr|@ajR6M=XZl4 zM8ahnJVKHDLCQq20$MKZw%VnGmOnQVPv^bLLM=7y<6&*Zlxh)jKoBsY6Tw>9c?q$$ z|FnD{;YyRVnHJs%16-qn5hK*pT1DP^GK!O8Gj$rSj^TQtekBR*35URjmSrJy6_^UE zHnxQ=%>v+3DZ7S{W_X!OSAs+Fd&$F6OY|L%bI7zt1ey-wF1H)aWvT7Z7u2GZju>c% z?x@kRXRm)%b~OB;rKCTlp|C{{>!zwHQ{ALC)@Vi*ZWiFwRr5h*z-tIB3ymSfkm9r% zmOUWyiI6is)lRu=R$_m!G^^uh*``3uywNqrGkx96HJT`*d3>|8kJRt#b{>L)p$y2n zfz=tyP`VpOh7~SXIbmr8dl|@#JET+V@ld!L(&k@jcgo=^#jlDU@tNg?a0QN$gy-Zn zxsV-jBD(hSmNOiMvG*X$5xQ5=sltjnEilAy)%I(EMhUmzpLU%~DQKgQ5c-K~(WG5f zg-#QIkXTsTq+QLg`pFl5;v!#_LR1(KrNIIvSz<1$2m%W>Z6w@m0oZU-ceXV8HAKbx zi(Y?td~{q@`}wibW>dK4YO~;RRn!36-w`yML?4qTm_%{PeFl}!;|xSb(V4oBo8MzW z)c1S0=_X_rLpG;P%!%kB(Oybbt1jPHFr>e<>2khlZ+CC?-iuwEJ*x=e{WiMOn8;#| z3e<}bp^}T2GD}==>Sz&vs%gK`{sEqa1dp`K{@bw3m!Q-O@FoQYlQe+FFM; z>oof7@bH-qL*G2NXV$8bT(z*B(5y(qy}jb3L)`_h9(0I1%Z_-&3}l|DBUebSp4xxV7wcSu>FBjW0Q3U=gy9v#l)in^gL3Sg@HPfmbG_qzO&+d z2|wzLsfut3lpzR~YqkJcxs9KO85xhsvpO7pC@5$bqKdv(zyy)c=OV(ghirJ4=8BjU6Q9`zF zNb{{hsX_Fr*aqxM1!PP$D)FP6E-P`i(hCMVr3(gL1#P7q3S1k`G}&q+p9NVpw6&P9 z(I_N{cGJ0o?pVS#N8h|2d8>^|_8lBljm$*LwptzZ$cZ+DtvZ0e>Dr*f=IRDRWgnoWbB z&$5grRqMaaR}IDloXB}a3%f5xy<0M4O$1@gq^>zVk$R?NZ&oiDn_vfrJG-wM?5FOn zl#o@J74dEsVs==O=U-T7{_12Ur=+Qn3+Vr^(?2)$_5gq`q#UI7$Dhtg7GHAuX3l;@ z$5A-)A+d__K6OThSAdTC`%A3*<>zHCjl3j)VPu2eA82l3_4}>Ov!{&xd+zRt(^#WM z+{CGG&F!-E0n(k_gmeR6jxM)zTNZ1LC?A>GX|p~1u8{y~MtlM=DW|9I2(h6ADO^kZI5=Rh zZMY<_K1_!nedhzfp1!&C$$$V0GzY{JXF~N8A$<@;2RL+#Nn}kWVg+m{t_G~;XJ^{Y zCrvVFKIZ|IC%7r9?G+F3NN(sc+{YuQ$e?F1$?5Gf)3#M>;W$kc>g8jUa`&NJ;&cMV ztW!GLq9Lnoob4?Q9s0S4l}=icqv|hlvR8^P9upeSN$2T>I$~3Qudhh~4f#yO)p+>7 zO?)7K#??DPeB}#e`W>i0AyNl~j`*W^m1hV3pnD4CBYWr+ddCd+cVXWRXYPW|G~V{l zhd*8QPlki*_l%Bl5at?RFoXZodzQMs(3^@yiZ7n(7X;SVK-M9?X*A4oe3F}nbhgQ6 z_?poOUu+@vm%Wn#PsP!=d|4GA>Q~2)DQFW!>Ls1TA5Y=0R6bPe-D61;FOBt&ej2{( zp7+lBSAq)rkmKtdfjFYUbo1`x1cT3t@WlA9Dk55~)>~kcWDG5}U=d6=^w_XWT-Ha6p-h$YZXJw;c!a%mbjY^%cVxn+aStamhZfNQx(nRtR z>vV+`qCPvzkl48ty%KPD8BnwgN9xjg^R5N{v2Qweb=()9#ytbv*6qC0X?@#BV()n* zD!7$VS6v1!5-#r<$k+Si@Vn2x_FH%)BU|JDMCXvTHoxxd&M|l~ zb#bv)h>(ST`icQCa^=jd4teK$}}8W7zjc* z6|p8zl^%A=Ag?PS*4Edm1q5KLUeen(f|?Q704lcu^z8JS_8W@R`q~d4d$idQ&#>A5 z`Qyb^Z+LXoJ?;;W`fpFq0T@uM)~|S9ySm>$@2TD2%-!zQRrlvA_RaFXfqiH0_Mib) zkq-`*_6_7WZ(6c>Vo1n3KSblhJS80{OPlff-Mn2Is1`M%2iIIL&SlL;*i!kR-lB$H zo?(_geFjrXoi8N&&44HuB@;1FU}Y~ z$L-+D)B%pMt()8QHlk1%TprgqZ!_FDzc_E&8{po%{!dF3J6qqoTjTE536X`M8A5LK zue!^;H@CmOy}7-!?1MMI{&t&bc%V8&Gt!#~Rg^#9W>xwKd)Ym`dP0m}cYphvqiCOh zcz@JihW;~vYyL9opDphSSuOi^=fxgDT|oM*fa+|$5`+fFCCIO=HEQj@hCw-R?SBtn zQEPvv1K(r#-i7ZJzF)w%)!N^~bsN5SaIU@u_+9p!IjWUmu(}1khsltQ>O*2eSfV8* zJf;5zgt$NzpgF!#AN)f91&n$-KKgJnIQ=)!Wv}!7%O@JuI9X~^W4zrkPHL+e=MR;a z>i)OAZ|!O$NfP}l)zsfduW4uyyLc6a6jgPN2icVP>rP0R7rrlPw#Kv zc;x$0lCV8HA2#Pq8>r04$jHpd$jEqXer7jcnAE@>*FkITc~SKrk6z;LYY7B|!4iLL zn|-hsZ@2@q-f&V>nOBd&n~uQY){AU1sg*n!vn@UsLC~Xk8oK?w5bZk&j*bn&XZ`0p zCOr7EBhS|Hyi0u=SP8ZVwHzJdynHDf`LYt`qDg2! z29_$AA-3u$I3PEUS~Jrm&F!Inc~C7y6q6Ea)nYk=YOFtz>%XAiuJYS#hAnery?d?e z2A!ZzXFOur2< zEQeE#8oQprKGnzhWCG}Vt67?4Nt|C8wpdH3sX*y*jI+h?nh=Kz(QG&4IOMqXd3$x?Kk6#V3^}wOFRG_;ZtHsHNlcxXU4d zT2#YBc^Z})TF!)aght5#@Ei0XRL&N&#cM-#{@X`qV7vx|BeBU5ezR~IqGG4{xapv! zK_v8fCDQ2AoT2m2Z(F@~pob^*J*@6Ph_b;B;8N4v-fH4qYzUY2QGS&#yvw@P;c#Vt z95yOCo^I`UR)ZH&PD1cW-7vpSKX(K|WR4D@wb7JF!1S@VvU4_Cj`saxqrL<1?}~Xg zi-Ldo+2Bb?aN$#hbTk?;cg72s6&E17nHdaMM0TQKFgF3|&yz)X151@r%h}Z2ruI;^ zHf9amg3HHdJj)>F7V@6_zE~M+rUv2yN?jV`SJ`ha+2L$X(VYVb!jT;dR-{#dhuzoZhn)R8q?% z4-jf*8WhAb;tKwQyDqG{ReqxeXF_i*0*@#)~`kmApPi$rFJ*S zO73I}r2j=`TaoY!O}9hoBV(|i$xVbLwSsO!Ira4krv$q%L^#QNgnR_sV^JWZ0V?cdj*SSd7rp?R(0f2fTN z;9CqA>mBIAC$YET<$~`IzGu^>+;_$8qFvMZw%55_!1Uc|s2azR!?HGl&cY2Y?iF_k ztm3ZTL2nZoM{B$zq7Ezq6m|Au3uJ>C!UYnRa{$o#at>7PB+osM*Erd8cU9!GM62;v zcC4$O4?ot=jq1_4;qh0xrEW4gc=@Mr{w<14Z_1+wD@PCFV3YvyOSf^K_1+vEis#PO zjjq*@q8o2eB#92{v#Q~w;#lHR_Va)OuIw$0TD_`uo)$+16NvD^i z6fjm>aoW8?Q$GOAmyY#Av*<$lQGniTb_}Gvhl9^P+_efFE1FYwca342mG+n_xz8x{ zMQX>L^nD`_3NzZil$hwH{1!^A(&Xi zYbQF`EXR>5KAu?E;JrGZ9emshwk4vRAIt&^-Je4Jav;r<&K9r;Pw*5I_DRZUQh#Wf z1{>OLWu+a7*{ti=bXF<7kJlSya20o-@qR953*1=tLpXn0mLhtl?b0ScRyfj7L+EZf zSyaiY$Mv{LgGB+S+UCQbOQV-g@@;YXTjdi$45kZ|K>kgYe25Aj2p|m33P*#j0lMmMK_U?-tG_g0Q4{KKy;%zOf&!(Oxmse0IxA>p%rc7F0}4Q`Cb5G_bbJF} zu^82Z-dq{bh_%?Uhh_4VR$aZM&mpioK!%!$g-%UGT^!}bTyBhS`c%c?T>*1R{1NlI z&^;B+gRrSE60*^zE)5LD9G~M4Wcbn=bnsPE87Zq7Hl}wq-PE!X1jiK}IKv5!m|#t% zQ8BQ!y*#0G3pwzKs~h48&PIHqA=}OON!X1_0Dq$}#zv+EVRe6?Z1+3LS)(I!E*KXt zk=zR7376?7&WSe4IXhXIXH_>j41drdQcPx;&Dp69B#pF^Ve6~xA4_tO^PomXtKS=K}&g5uh>;p6R)~>N!X;xWl?NY zA7*xw>e^V_RSRKhz!a32LPy*h-S0#&IycO@a41KLeQ`tWCjn#jV8@r)6>MHwjFEl~ zj!B(2Q#V9hUC+nwj)MK7+#BiDf0C0$(}8|c3=&Za2z26s&R~8KxVf@5XMVZP@U+gY zhOTN4u1#Do{$5S*b+oPPIZj}M$I6|qi5!e%i6v)WPj2=&*{ur=g@rl9qc^jm&O|A( zO_W#tqc{NUNgm6O9<^b}3%+_`(wbR@NxT45Hx2+AWaxQ}9b)n__VR`QjFyzQ4xM{p zJ7UVY>Obnh$m7*?M69cirTIqqP=-b=&2=3?#w?gQ1)H{7mbb_aFxuO;nf@VK=;*%b z+|t3V9EEqq%tyL)nb7E~Zn>bIfKV_Fe1jR1l^rv(R_XVjX3vzAel~bc#@uFMYybY4GawZzoY-saBJsVB)3~_0I;c-yVKA z9vmIN!l=6-g+>~_o}`QH2xB&7cyW%_BCe}|zy1HetOgC0Rsx1PHsr&zS8v`t-(L+~ zR9xA^;q!g!LEWH_UMK98Sy}hzqu0WDS37OG?z=~?wIg$lbMet@!w1=D%RERJ#3`k!b%qFkY^lN4>IO!|kk~{%>R^0^S>aqs=u;>JZ65LGqV>&O z)Eave=cEuBW=EJl8pTx10NeF!!T~AdiN(IJluZO0Cj^L{cW>3DxY_1ihqh?YvoDU+c{4g6KuxWmEP0O{K6n9^iBKg>9I&q>{ z-qRi3B$OH1=w4pVNwTyd;q04`EJ5MEi>ws>Kdn4V_xtl3R36!Vh9?lM$S^E3Rg%Bh zJf)M=&#Y{f{OVHH8&MU^0x1Es++OA89HP!uyr|wDtcUaMM)QMx*z>afwA?ER@TQXp zKy5i$Nno}DQ$Yc4P!lw~&M*Z#PY&#k0foBJd+T1$Yfm}K<{>{`yrtWxL7fM$c_M;H zyvVL#LhD?31@=4fWd&F0hhs051`iy8Xuj46x{)!=meWn}Z~5d$79JkfL&pU(gFs(X zlH+v-7akhy!%c=(yNNeJpCe1HgSccNF6Ov*!=#qQ1GJ%PYOjyUSmvlASGk7 z0Q1;nO!e$&6U>kD;aZr>xEg_Vl%*T(67QHj^Rh5+G5`f&ol&UDtv~+kzzPfS;YN~H z!#sb!Pcd>;)?<1D@jnOz@rCm;Q7(|2eL^l^3YJ6B6QPAY%H}uCW z<26~T0G12uLMN6YD(H+Gomg|$jShi??sGZZ=gRbs6OTyIhP>R(0je0mT0nr76vnp6 z5p0{7j>MQyHX0F-&qcT}V|R%jF{Lm`z!_#@&5a zls7nMIT|zk49uc9PtEMP#=IHxE4SLxcE7dBqQWk~e(&Q{YKB zTMP({*b*`A#y>%&kdG~Ly2p{Ba1dMkaf#mj>0SRZVEniw)k9bVY$+s^wUk_i9W}k? z7LLZu@lZe@55>VpKvPPbeJ-5@+t&+6q+g_6Y%NG5QJ-!umXlf0p98s09SSs(zpxde zNZFJZ&V!u^T-`In+8_~a*W12kFwww%b#?{)(@1WzY@P^Sm5mZ_P|er*1a5>)7K)-# z#mvoU!%$!KX7>|p$@gCg-O0pZ+Bjet6Hdx-1mliLJZ;l*N0Jlm{JXUc^j!UHK%+AT zDImW2?32C*mF9uj5J6tyC`>XG5i5Bd$>6ZK2E}5@WW+hM2wMJ*Nyc%#Pv;=YR;|F& zAi2dxPAlC5s0qk2KB3aRm#s4qa93m|>z2z|%YDxY^kysK<2?pN+5l`gUahx`;Nc`> z@Q}K@`Gnp$#7vUyw(@L(gK96WOX6r7C=tZ(l zbg7}cZ`$6%e=ghYu55pw*|KC7=@ne|^CemH#l{)c#rS1{d^;z~xH#W_a| z#Z;?B@T+eqK$BUlvJvpF%;Pa!v8y%s$68PAh{XX}D#=Kb+7X(2vTl&H4ScsAbL)y` z^k4?-p=wzGFb#$@Empo0kpY^~pu_$tVcWZw@=Zd!JMN|%3TB8!*;pOFe;t5j$Vqw& zI#KnKZSprta0e~%TJj&Rd?Bt}*#FvnfD|}Q7x?;}6=Nk`G0i;a2${_vMLrhc85Pt} z%?;lR-%SWh!10EjaJiQf4%v3Mn#0y_HIu+taS4q{aTgLn!^8c+2os&g1&D4qJ-eP8*ULz}^;$UI3LKtCA(>XfxcD(z>s_-*d!iY=zLz>);a7M|6HDFrQ6A05k zTa0!9lNy`T!P+Yy%Zk;H<;&~n|gE7cbKO^`X5*fA;htyqnVz4k15l8*9 zB7z27%>YgKh&W;>Cq8ZPKX&gHpb%gZ*ef^xrj?`$v&*z4oE;k?Y+4iBr~svo;X7H zEBXRAxe3J_2z(n)RAk}tWLN*V_r*mmkf8dHD-yQ!P}Y#LNY#pYG2wj3f7i(@sJ$~q zp|eG_rGGF+0l)|yF6>?_o@8w9U~vU};JjR3^{vp|?8BBv*a6(YIB4a_YQYR}lNM_* z9H_<5mtp>4)bthN+|aadpy&`5t+);=715%h`BC0&&5KNE=R^-Iye*0+*9usRZJLWS z-nV)^gFjh{eX%8U9adRVg4kv!Y3+1q&fW7xwnODfUQKc!0R5?x>~s>|)VsCePV7cK z-N>8{xyd?ekEx&b+Rn&+X}TOz-psj@QjNGnQ5#(Lwk120KkPXzH0#v})NS>8uFL#1 zvCJnn*|xbzv5UqeynX3j%|E_KTC4VMKyVi*p`8RY(%(Ywk+eD`(`R!|bDv1M3Mzk_ zw73o(ekEFFZHey8BYCD1>yVc5Am1{em`_Q_fMs4f_C>2PyFtiarW1Y- zS}ZL#Zt1^VGybP|k`(WhBD&4<)H6{hADrH#BQ z$ir7*FRfpYFjZ9y^AtrsiW+3TOLiUMMh^M`q76P)yAc-XL%|;7UY0B#(;)V_EQ&=~ z<{7f~D*RE5J0eI)4PitC(-taqvQr~La}n;yzS*F2-=QRTWxoIu?NeCE=>$Wb!2AO- zISnXC1p@8DbSIF8DWWH4-u~T2>XG@2pXk9R3;$qlpbySFH6^zw&~HIHNao~8fjEzr zWJ)_MCKJGuOvW`J>0ZIvZ1NdjVv*YEB^te%}S#q6W-rJ6dilZjW0M((B z1;3|5T>D9hW?+>GNG51C*0@Cq9*G;wsF9pzaKX=RGvOklu`k9+mJY84b{vcA4z0?_ z`_z>28cA!Kegeu(my<<4$NY2oG=qza9qFeIJGqU7?1#vb9f&D3P!d-7n>v)Kk;AOD zh=5{dL1FK`2VNe5LS6#1*rqImO!?Ntlu0@?l zLrxirAokvYWrc&dwbNUM`L^i(y!qy+`Eku?c*gBkHJ3yB7}CNtlpW(`N+ttl=muH% zvbz2tn&UM}bG;E^*=STai>JVlS4<(HjVue9cPrEb0SOY#&2AkN^qeQC=DQ+!f=~tT z{079jH^P$}Aks;RZ7khh9n+f?vu$P(=@kVjT*981Nakk3fa7;gdAc z1pi~4RA(ZK5m9fX9nr%a3R@f_8&m?$bO}=ZbqX9ayH7FOXoBWkM>34u{%;jdD$PMG% zZ^`x1S8f4Fjaj_5~b7zUx#srZEVztQTIM6AqxO7p#jAmFp*DM^a3~Fg= z@lb;a$%*)l-NO|6QXksCYT4Ch1t+5REa<;$~Q;yc`q!QIP>%_=gzh*z# z<^Ad|;xqfx>}FQnNmI`QZrN8alLRHSyfqQ+b?%^GjNxihT&5Fz&3OIh?eWRshrc*1 zOu7-^aUj+ST;Sn1bHeO#8I_zSw@-cO^7>-+mYU*6%?1gnjcnv3A)IkHl35OSUs%@* z*p|h3OhGYmPXj_g5PVP%Mx$v+lEG41z@5QrLITtea3F%f`F=LZa^Oc#lJE1&NnQYN zx=D^OWf*2zm*7-$>X7q(s_NS#t`1~r9wg23^zMh--Nohnuf=Wero6rV_mlTKchz@i zPxqcZnH-iUlW{)(egEVi*LVGEBh71sd4K4A{UTEmWmsJb9GyH(2p5~hJJS#b2k4!`WXBclV#QOjJx>eVd`ZDb(vd zLzBgib{}g`f!T+DbFlvl#$xa!-JiE9b?OeYEKs!(1Ps5A*^gDo(jn4Al zg{5;N!FTX7N0ATq4IGil{4Nm|;#|d%29&h3t~Ag{hQ%DVV_8g5WaoqKHZAEq(A9p9 zar>WLcS=|}t)G(@{T*B@Kf~g98r}?bLQp!nDoT*{>TDDG-e}l#1SG434Rr7hE zrY*Fns=$x^y2NzpzUo%r+YMBwJm?wK9hsHkj0)67UJ^lOzZU|JTE*$*#KU}NH$)Lc zCP=>Bou?*U8p)yDNy%P!SAS6V>VBdwE&RIWGQIXWo+$vyFw?(IE8KPX54wU)NBas0 zZiijFW_LMO_Y8H@7a7uF^F zJ8sWd0@FYU!4(4J#8z86M?ZOP?pT(tVihf9PeaeE_L_1c2Q2ZK`SA4BQ<6+jWYaQ+ zTplYRSRVBF!QYp+GwG_;8J>}vPXQ8;u1X~9{hSX>{YUZ?k6*rf;~ zj7!fQ5*;~eMe_F~Z|m?FIAv|Wv5lq1@!qLRlXAaxkK^3E{6uelg>@y3%{4NnC0t8q zaP6h1S=bdzdUXG(UQP=)BzjS6rksTUn+e&?R!Wu9#X{=Cgrh|u@Sdg9fd86YLntnQ zNn95;OZb^AAy|?L(5FW2v1`&tLJB563%B13wST&Pd!zqGT{mi!do+)6AW1oV>d@w! z2u1-F4+kX!5D4klUw@@geQ@J}yNcO|r35m@P4B61g}8_Am(Sg+bX~V=LWQF|CawiN zZUJ{_Vm&ifa6>6EChz1^%)xgf)s~}QTvw?7qS~IaLMiU_>@dW_*A&4$RS-XjClL({ zZSld|H?NN0ogLTfVH6K2*lD6+S}%mPM2c3hbuH3Uzi-K#UYfICVZ#(27Y=)UPFj ztu09?33t4l4TZNf{@r*yyWKU-t^+Zpq*bkbQ-9l(hT7)`d&b6>2ETr727G?7Yt%N# zWfFKxbh_onsR~>G?IZCfbjO{-Av{r`oOC~UfbCP>QC~4kv+EOU@AHTS*Snqjz5Qq3 zrKT6s3Yt-)Vfx)OZ=CLc?C)E`+8wu$pd|muf9>hN_VizO@yBL$IQh{0UnV>2bZ&Qv zo!mavs$qXto_^k}{1cY&zY0-Bz%u#n9-x~D!Eg{9ArssRNo|UX>S>OA68J?$eiS-k z`hxFf`b+4^?q=nCof9Ym1I13f+0~i@+<@#rA(VP z5)Wcp4V@39X}Xed_$X!eZ<(Z~Mm$5Z=eD{nq*o+;N1q9&YspmI2)H`43^wvzW_<~5 zE$ebDHU9k7VyeA$5@CWaoX`r#LCY+W+^O}By|Qq)7pq-Z{%qtm2A-6lDQ4w4V|N`? zq4JdURjQ`on8bqhVWV+}FT%;t2PND-`*uifv5|QrAugdSU@qhIYLCybi7toYiBO?# zB4vr0jW3c^4uFR#ABKek?j{R`#`+gd}u-3ABlvC0KI!^X%K*E#dv|evZ?V4FXgGD1PTv2*{IMM@*wKk8P*80A<6V!>`jn1e{Cx=ABx zK!FPwhJY{8Ya*GZ6STm<5S^p3>p9hsKdg;hLp6~_ic&pP=hIIIp=wy}1>XpNW!@ly z!YHB85IUo9v8+L*c;kD?G7v~L{l(aVdx9g?BX8_JO z*}HQJ;gr#rmlpP2iLXQ9U>yzB2oZ21oTe>bc_ELSoSk6~>l5Xd9i<56O&rfnDp-xX zPJO8Aj60U(5Hkcip5orEdE>}GjMU4fd!eog&vhzqw6gB;yQA9sH*m3nui$1YkbjR4 ze~hJv7XlG%eToZRYJvuk!G=QB6nbk1V~iz znmSs{(cB&I-E9Y2S^PK=VeBNFT!pj?G-V!qqVlB5P3LID{f3N?PMD0$*_9HdbZ?x3 zfie5i{RUIO8_Jsp>-KAZY>CKk9gD24z&DbqV8YSgu!Lt2y5xvQxtUAoT$;7GPmil_ z&Z}xsrY|_XgHey~&u4;wEJ#k*N_M?S+Z5Hj5m$WBt04lTZV?(H*SxR9fpz3XyofMG$Q zRzO2rHV*sZN@y6)AuWgOo)4nvf_3$5V0if{snF+5MwOs24VNHb-lZJhTtbNzQ8D4qeT#)af3A|KNeiFduBg-JEq&KHa%X5BgiflrOKV)7?X zjZ(vyHl>Pv=cS&!l)hLRiH?GLfbh3_t}6k6`r-H;NxB0HrSHeR!y??`%mW9_D5}qN#%!nxtqoT}e1fF#r zKB!=ca2zbPC!S54wW&h;K&q$k7i)>v2c*Jdj3;7R6K05mC2FxzMHZ4xUD;90zqNOX zD7L9TnMnroa zf_*gj?1^i!0H+SChYnA&Fl!t5v`;4!Jj`Ny-_^g*Q;H-+Hsx~|S*vXRy>&6#YF|`a z@?SX8FD_ltC!EXyOZ~QZkR#PD9-Bj@-1MoRs{Z+Y*0AT8_6EQsB&14PXqnC zWON&o>x4PQv@Qa@cWa+^ugYRMuR_$3fx-2~p2F4dxG_$#h|Wcm12;T47UENSF^Hc$ zFq%pMGV0~2s|7Gu9MuYCAE_AQutKPzDu(v*z4?CXC^4;XEv5CV*JG0K407|@n#?mh z)u3)ACGf=2Y6c|Y2@23I8>99?l@;pA<1)6X%lzvvM~3F|LHfP_(|D=kUUt5Q#!LSG zSy!zFNBeCgKYmKkvLBs=I&=pDHqQ5}GGkE=E-_?SjsXHE=`^z*NzSs8&ACeFl_aop z^4XZua$t%fb0rBW*c6yM5t-PLg)JHTrRgq2q__p@aQsN$*VwG zu$fHPG|>?JgBAz?HEQAY+6WmZ2k&%8i;8%x{K1jeO{26D{Vu9(gxz($n-`WOm!!_bbpYWigOL&Bv-pLDs94uN6vFpvtzQEeo=2 z-0r%Aph?gpQ3TT~d9<9)<;&i35y~lUrS~*U|odVLW24PpP<)tBavKuP#WY z8k=Mxlb>Vb z2BrXuf&HV$JH4GzvV$d=B^NVvD>J7l?9^9d3FrfVyt6+_#sW4D`q}>e-hSJf6sM)U z=|m{6S3|+}ApUN50bb11(g3~%6ZRMis?*5eQhc#cqA>z#f5>K8i3Y^OvZyLtTEe8! z#lMlfN+-kR1nW``4h%#-!yx63T!fP@Q+OalQ~k^J(Dv#}TZfTf@V}+d0WLb2+o%^t$E&T75r!dkxa_Tki~U{F z(KRsTlqaqlg`kC0?^wi}mljg4)%J9? zh*fV5cP0cPd8Q2ntmJWKX(V@@@_UfK$H7$*Z-Xw37Lj0K?_*06AIp;LU+iJ8Ll3N8jvx^ zAMA#4VC4hrBrN0}sIAsXM1rmZ#x9(9p!o}YKZnYcHw%XL5!wkGj{_q+e7LcQiLFpM z8@zsd_~CePbo}c5(J?HkNV`+aF>s*ebNc;b$5G|tZR*WzT&zfGKxQda580rC$$ts( zOuCf^UYqO14kd&#$L&a1H3G{w6>>=+bA>_6py|ZHh#pVbj5a zwE~ue8n!JsW4o*xrgIFeNl{`LH~z)Kj{cJL#Sd_?olnvsN?;eTXW+l^Z+o5UGpJrR zZ3!@1yTz4TYgKM@<+deTqHX;mDRavBGR)K2s7Op6TxM4w+LhV;JaHZ=L`a;wySpy8 zqo6p<1cqb$X>m%YfBEL<$;If8{Xd;ITn$?2-z8P$thB;?1Rlt_r8#S!tSpoQAY~p{ zSh)99yi5BJ$*(xBm|^+(RUp8*0@vBYx${(MuBkb&{(e^32we416Sp;A*^vFS04 z*_id0v*qQ2{>W~a^dTHf7OUIn%WR3k=Wr}kNA@7@+p4HgY28U~(}@^8W;>sT6<#K+Cyy&+E4JFss_3l= z=%a>py?kj$UkNN0jI8Y9Wr8>l4Ns3OHlhs?{kRfO*dKLhwjNh)l`+-kcZIDO4?@T2 zqb~S-tT}QwuhIz!N-L+(OIB@s1bhgY?gZ~St(8b|D)LDI4CSAO+bW!nvSAJ!FglF={>@VMw4;>Ep)gPbYG849ByeHw!+BycwflJ}QvwTv-Z>L~ATK!(Cze2v


@B39u*t@FYt)W7`x} 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

J$N75E+E?3?gukBk@Lj1nBiih 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 CpVaOE diff --git a/DeDRM_calibre_plugin/DeDRM_plugin_ReadMe.txt b/contrib/calibre/DeDRM_plugin_ReadMe.txt similarity index 100% rename from DeDRM_calibre_plugin/DeDRM_plugin_ReadMe.txt rename to contrib/calibre/DeDRM_plugin_ReadMe.txt From 95247503f02051cc2c355b1ca2f2224541b75490 Mon Sep 17 00:00:00 2001 From: Zhuoyun Wei Date: Mon, 7 May 2018 04:38:49 -0400 Subject: [PATCH 3/9] Remove redundant files in Windows app --- .../DeDRM_Adobe Digital Editions Key_Help.htm | 60 - .../lib/DeDRM_Barnes and Noble Key_Help.htm | 68 - .../DeDRM_EInk Kindle Serial Number_Help.htm | 43 - .../DeDRM_App/DeDRM_lib/lib/DeDRM_Help.htm | 73 - .../lib/DeDRM_Kindle for Android Key_Help.htm | 61 - .../DeDRM_Kindle for Mac and PC Key_Help.htm | 59 - .../lib/DeDRM_Mobipocket PID_Help.htm | 42 - .../DeDRM_lib/lib/DeDRM_eReader Key_Help.htm | 56 - .../DeDRM_App/DeDRM_lib/lib/__init__.py | 647 ----- .../DeDRM_App/DeDRM_lib/lib/activitybar.py | 75 - .../DeDRM_App/DeDRM_lib/lib/adobekey.py | 603 ----- .../DeDRM_App/DeDRM_lib/lib/aescbc.py | 568 ---- .../DeDRM_App/DeDRM_lib/lib/alfcrypto.dll | Bin 70144 -> 0 bytes .../DeDRM_App/DeDRM_lib/lib/alfcrypto.py | 301 --- .../DeDRM_App/DeDRM_lib/lib/alfcrypto64.dll | Bin 52224 -> 0 bytes .../DeDRM_App/DeDRM_lib/lib/alfcrypto_src.zip | Bin 17393 -> 0 bytes .../DeDRM_lib/lib/androidkindlekey.py | 468 ---- .../DeDRM_App/DeDRM_lib/lib/argv_utils.py | 88 - .../DeDRM_App/DeDRM_lib/lib/askfolder_ed.py | 211 -- .../DeDRM_App/DeDRM_lib/lib/config.py | 1021 ------- .../DeDRM_App/DeDRM_lib/lib/convert2xml.py | 884 ------- .../DeDRM_App/DeDRM_lib/lib/encodebase64.py | 45 - .../DeDRM_App/DeDRM_lib/lib/epubtest.py | 208 -- .../DeDRM_App/DeDRM_lib/lib/erdr2pml.py | 597 ----- .../DeDRM_App/DeDRM_lib/lib/flatxml2html.py | 801 ------ .../DeDRM_App/DeDRM_lib/lib/flatxml2svg.py | 249 -- .../DeDRM_App/DeDRM_lib/lib/genbook.py | 721 ----- .../DeDRM_App/DeDRM_lib/lib/ignobleepub.py | 455 ---- .../DeDRM_App/DeDRM_lib/lib/ignoblekey.py | 336 --- .../DeDRM_lib/lib/ignoblekeyfetch.py | 258 -- .../DeDRM_App/DeDRM_lib/lib/ignoblekeygen.py | 332 --- .../DeDRM_App/DeDRM_lib/lib/ineptepub.py | 601 ----- .../DeDRM_App/DeDRM_lib/lib/ineptpdf.py | 2350 ----------------- .../DeDRM_App/DeDRM_lib/lib/ion.py | 985 ------- .../DeDRM_App/DeDRM_lib/lib/k4mobidedrm.py | 353 --- .../DeDRM_App/DeDRM_lib/lib/kfxdedrm.py | 108 - .../DeDRM_App/DeDRM_lib/lib/kgenpids.py | 310 --- .../DeDRM_App/DeDRM_lib/lib/kindlekey.py | 1727 ------------ .../DeDRM_App/DeDRM_lib/lib/kindlepid.py | 144 - .../DeDRM_lib/lib/libalfcrypto.dylib | Bin 87160 -> 0 bytes .../DeDRM_App/DeDRM_lib/lib/libalfcrypto32.so | Bin 23859 -> 0 bytes .../DeDRM_App/DeDRM_lib/lib/libalfcrypto64.so | Bin 33417 -> 0 bytes .../DeDRM_App/DeDRM_lib/lib/mobidedrm.py | 543 ---- .../DeDRM_App/DeDRM_lib/lib/openssl_des.py | 89 - .../lib/plugin-import-name-dedrm.txt | 0 .../DeDRM_App/DeDRM_lib/lib/prefs.py | 295 --- .../DeDRM_App/DeDRM_lib/lib/pycrypto_des.py | 30 - .../DeDRM_App/DeDRM_lib/lib/python_des.py | 220 -- .../DeDRM_lib/lib/scriptinterface.py | 198 -- .../DeDRM_lib/lib/scrolltextwidget.py | 27 - .../DeDRM_App/DeDRM_lib/lib/simpleprefs.py | 77 - .../DeDRM_App/DeDRM_lib/lib/stylexml2css.py | 284 -- .../DeDRM_App/DeDRM_lib/lib/subasyncio.py | 148 -- .../DeDRM_App/DeDRM_lib/lib/topazextract.py | 538 ---- .../DeDRM_App/DeDRM_lib/lib/utilities.py | 39 - .../DeDRM_App/DeDRM_lib/lib/wineutils.py | 73 - .../DeDRM_App/DeDRM_lib/lib/zipfilerugged.py | 1400 ---------- .../DeDRM_App/DeDRM_lib/lib/zipfix.py | 188 -- 58 files changed, 20057 deletions(-) delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Adobe Digital Editions Key_Help.htm delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Barnes and Noble Key_Help.htm delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_EInk Kindle Serial Number_Help.htm delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Help.htm delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Kindle for Android Key_Help.htm delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Kindle for Mac and PC Key_Help.htm delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Mobipocket PID_Help.htm delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_eReader Key_Help.htm delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/__init__.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/activitybar.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/adobekey.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/aescbc.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/alfcrypto.dll delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/alfcrypto.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/alfcrypto64.dll delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/alfcrypto_src.zip delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/androidkindlekey.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/argv_utils.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/askfolder_ed.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/config.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/convert2xml.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/encodebase64.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/epubtest.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/erdr2pml.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/flatxml2html.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/flatxml2svg.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/genbook.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignobleepub.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignoblekey.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignoblekeyfetch.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignoblekeygen.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ineptepub.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ineptpdf.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ion.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/k4mobidedrm.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kfxdedrm.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kgenpids.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kindlekey.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kindlepid.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/libalfcrypto.dylib delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/libalfcrypto32.so delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/libalfcrypto64.so delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/mobidedrm.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/openssl_des.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/plugin-import-name-dedrm.txt delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/prefs.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/pycrypto_des.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/python_des.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/scriptinterface.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/scrolltextwidget.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/simpleprefs.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/stylexml2css.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/subasyncio.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/topazextract.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/utilities.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/wineutils.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/zipfilerugged.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/zipfix.py diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Adobe Digital Editions Key_Help.htm b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Adobe Digital Editions Key_Help.htm deleted file mode 100644 index b258afe..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Adobe Digital Editions Key_Help.htm +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - -Managing Adobe Digital Editions Keys - - - - - -

Managing Adobe Digital Editions Keys

- - -

If you have upgraded from an earlier version of the plugin, any existing Adobe Digital Editions keys will have been automatically imported, so you might not need to do any more configuration. In addition, on Windows and Mac, the default Adobe Digital Editions key is added the first time the plugin is run. Continue reading for key generation and management instructions.

- -

Creating New Keys:

- -

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog prompting you to enter a key name for the default Adobe Digital Editions key.

-
    -
  • Unique Key Name: this is a unique name you choose to help you identify the key. This name will show in the list of configured keys.
  • -
- -

Click the OK button to create and store the Adobe Digital Editions key for the current installation of Adobe Digital Editions. Or Cancel if you don’t want to create the key.

-

New keys are checked against the current list of keys before being added, and duplicates are discarded.

- -

Deleting Keys:

- -

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

- -

Renaming Keys:

- -

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will promt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..

- -

Exporting Keys:

- -

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

- -

Linux Users: WINEPREFIX

- -

Under the list of keys, Linux users will see a text field labeled "WINEPREFIX". If you are use Adobe Digital Editions under Wine, and your wine installation containing Adobe Digital Editions isn't the default Wine installation, you may enter the full path to the correct Wine installation here. Leave blank if you are unsure.

- -

Importing Existing Keyfiles:

- -

At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing ‘.der’ key files. Key files might come from being exported from this or older plugins, or may have been generated using the adobekey.pyw script running under Wine on Linux systems.

- -

Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes will only be saved permanently when you click OK in the main configuration dialog.

- - - - - diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Barnes and Noble Key_Help.htm b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Barnes and Noble Key_Help.htm deleted file mode 100644 index 8f22f21..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Barnes and Noble Key_Help.htm +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - -Managing Barnes and Noble Keys - - - - - -

Managing Barnes and Noble Keys

- - -

If you have upgraded from an earlier version of the plugin, any existing Barnes and Noble keys will have been automatically imported, so you might not need to do any more configuration. Continue reading for key generation and management instructions.

- -

Changes at Barnes & Noble

- -

In mid-2014, Barnes & Noble changed the way they generated encryption keys. Instead of deriving the key from the user's name and credit card number, they started generating a random key themselves, sending that key through to devices when they connected to the Barnes & Noble servers. This means that most users will now find that no combination of their name and CC# will work in decrypting their recently downloaded ebooks.

- -

Someone commenting at Apprentice Alf's blog detailed a way to retrieve a new account key using the account's email address and password. This method has now been incorporated into the plugin. - -

Creating New Keys:

- -

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

-
    -
  • Unique Key Name: this is a unique name you choose to help you identify the key. This name will show in the list of configured keys. Choose something that will help you remember the data (account email address) it was created with.
  • -
  • B&N/nook account email address: This is the default email address for your Barnes and Noble/nook account. This email will not be stored anywhere on your computer or in calibre. It will only be used to fetch the account key that from the B&N server, and it is that key that will be stored in the preferences.
  • -
  • B&N/nook account password: this is the password for your Barnes and Noble/nook account. As with the email address, this will not be stored anywhere on your computer or in calibre. It will only be used to fetch the key from the B&N server.
  • -
- -

Click the OK button to create and store the generated key. Or Cancel if you don’t want to create a key.

-

New keys are checked against the current list of keys before being added, and duplicates are discarded.

- -

Deleting Keys:

- -

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

- -

Renaming Keys:

- -

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will promt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..

- -

Exporting Keys:

- -

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

- -

Importing Existing Keyfiles:

- -

At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing ‘.b64’ key files. Key files might come from being exported from this or older plugins, or may have been generated using the original i♥cabbages script, or you may have made it by following the instructions above.

- -

Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.

- -

NOOK Study

-

Books downloaded through NOOK Study may or may not use the key found using the above method. If a book is not decrypted successfully with any of the keys, the plugin will attempt to recover keys from the NOOK Study log file and use them.

- - - - - - diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_EInk Kindle Serial Number_Help.htm b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_EInk Kindle Serial Number_Help.htm deleted file mode 100644 index 5a4692d..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_EInk Kindle Serial Number_Help.htm +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - -Managing eInk Kindle serial numbers - - - - - -

Managing eInk Kindle serial numbers

- -

If you have upgraded from an earlier version of the plugin, any existing eInk Kindle serial numbers will have been automatically imported, so you might not need to do any more configuration.

- -

Please note that Kindle serial numbers are only valid keys for eInk Kindles like the Kindle Touch and PaperWhite. The Kindle Fire and Fire HD do not use their serial number for DRM and it is useless to enter those serial numbers.

- -

Creating New Kindle serial numbers:

- -

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering a new Kindle serial number.

-
- -

Click the OK button to save the serial number. Or Cancel if you didn’t want to enter a serial number.

- -

Deleting Kindle serial numbers:

- -

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

- -

Once done creating/deleting serial numbers, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.

- - - - diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Help.htm b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Help.htm deleted file mode 100644 index 3fed9df..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Help.htm +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - -DeDRM Plugin Configuration - - - - - -

DeDRM Plugin (v6.3.0)

- -

This plugin removes DRM from ebooks when they are imported into calibre. If you already have DRMed ebooks in your calibre library, you will need to remove them and import them again.

- -

Installation

-

You have obviously managed to install the plugin, as otherwise you wouldn’t be reading this help file. However, you should also delete any older DeDRM plugins, as this DeDRM plugin replaces the five older plugins: Kindle and Mobipocket DeDRM (K4MobiDeDRM), Ignoble Epub DeDRM (ignobleepub), Inept Epub DeDRM (ineptepub), Inept PDF DeDRM (ineptepub) and eReader PDB 2 PML (eReaderPDB2PML).

- -

Configuration

-

On Windows and Mac, the keys for ebooks downloaded for Kindle for Mac/PC and Adobe Digital Editions are automatically generated. If all your DRMed ebooks can be opened and read in Kindle for Mac/PC and/or Adobe Digital Editions on the same computer on which you are running calibre, you do not need to do any configuration of this plugin. On Linux, keys for Kindle for PC and Adobe Digital Editions need to be generated separately (see the Linux section below)

- -

If you have other DRMed ebooks, you will need to enter extra configuration information. The buttons in this dialog will open individual configuration dialogs that will allow you to enter the needed information, depending on the type and source of your DRMed eBooks. Additional help on the information required is available in each of the the dialogs.

- -

If you have used previous versions of the various DeDRM plugins on this machine, you may find that some of the configuration dialogs already contain the information you entered through those previous plugins.

- -

When you have finished entering your configuration information, you must click the OK button to save it. If you click the Cancel button, all your changes in all the configuration dialogs will be lost.

- -

Troubleshooting:

- -

If you find that it’s not working for you , you can save a lot of time by trying to add the ebook to Calibre in debug mode. This will print out a lot of helpful info that can be copied into any online help requests.

- -

Open a command prompt (terminal window) and type "calibre-debug -g" (without the quotes). Calibre will launch, and you can can add the problem ebook the usual way. The debug info will be output to the original command prompt (terminal window). Copy the resulting output and paste it into the comment you make at my blog.

-

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

- -

Credits:

-
    -
  • The Dark Reverser for the Mobipocket and eReader scripts
  • -
  • i♥cabbages for the Adobe Digital Editions scripts
  • -
  • Skindle aka Bart Simpson for the Amazon Kindle for PC script
  • -
  • CMBDTC for Amazon Topaz DRM removal script
  • -
  • some_updates, clarknova and Bart Simpson for Amazon Topaz conversion scripts
  • -
  • DiapDealer for the first calibre plugin versions of the tools
  • -
  • some_updates, DiapDealer, Apprentice Alf and mdlnx for Amazon Kindle/Mobipocket tools
  • -
  • some_updates for the DeDRM all-in-one Python tool
  • -
  • Apprentice Alf for the DeDRM all-in-one AppleScript tool
  • -
  • Apprentice Alf for the DeDRM all-in-one calibre plugin
  • -
  • And probably many more.
  • -
- -

For additional help read the FAQs at Apprentice Harpers’s GitHub repository. You can ask questions in the comments section of the first post at Apprentice Alf's blog or raise an issue.

- -

Linux Systems Only

-

Generating decryption keys for Adobe Digital Editions and Kindle for PC

-

If you install Kindle for PC and/or Adobe Digital Editions in Wine, you will be able to download DRMed ebooks to them under Wine. To be able to remove the DRM, you will need to generate key files and add them in the plugin's customisation dialogs.

- -

To generate the key files you will need to install Python and PyCrypto under the same Wine setup as your Kindle for PC and/or Adobe Digital Editions installations. (Kindle for PC, Python and Pycrypto installation instructions in the ReadMe.)

- -

Once everything's installed under Wine, you'll need to run the adobekey.pyw script (for Adobe Digital Editions) and kindlekey.pyw (For Kindle for PC) using the python installation in your Wine system. The scripts can be found in Other_Tools/Key_Retrieval_Scripts.

- -

Each script will create a key file in the same folder as the script. Copy the key files to your Linux system and then load the key files using the Adobe Digital Editions ebooks dialog and the Kindle for Mac/PC ebooks dialog.

- - - - - diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Kindle for Android Key_Help.htm b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Kindle for Android Key_Help.htm deleted file mode 100644 index 65b11b4..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Kindle for Android Key_Help.htm +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - -Managing Kindle for Android Keys - - - - - -

Managing Kindle for Android Keys

- -

Amazon's Kindle for Android application uses an internal key equivalent to an eInk Kindle's serial number. Extracting that key is a little tricky, but worth it, as it then allows the DRM to be removed from any Kindle ebooks that have been downloaded to that Android device.

- -

Please note that it is not currently known whether the same applies to the Kindle application on the Kindle Fire and Fire HD.

- -

Getting the Kindle for Android backup file

- -

Obtain and install adb (Android Debug Bridge) on your computer. Details of how to do this are beyond the scope of this help file, but there are plenty of on-line guides.

-

Enable developer mode on your Android device. Again, look for an on-line guide for your device.

-

Once you have adb installed and your device in developer mode, connect your device to your computer with a USB cable and then open up a command line (Terminal on Mac OS X and cmd.exe on Windows) and enter "adb backup com.amazon.kindle" (without the quotation marks!) and press return. A file "backup.ab" should be created in your home directory. - -

Adding a Kindle for Android Key

- -

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog with two main controls. -

    -
  • Choose backup file: click this button and you will be prompted to find the backup.ab file you created earlier. Once selected the file will be processed to extract the decryption key, and if successful the file name will be displayed to the right of the button.
  • -
  • Unique Key Name: this is a unique name you choose to help you identify the key. This name will show in the list of Kindle for Android keys. Enter a name that will help you remember which device this key came from.
  • -
- -

Click the OK button to store the Kindle for Android key for the current list of Kindle for Android keys. Or click Cancel if you don’t want to store the key.

-

New keys are checked against the current list of keys before being added, and duplicates are discarded.

- -

Deleting Keys:

- -

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

- -

Renaming Keys:

- -

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will prompt you to enter a new name for the highlighted key in the list. Enter the new name for the key and click the OK button to use the new name, or Cancel to revert to the old name.

- -

Exporting Keys:

- -

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

- -

Importing Existing Keyfiles:

- -

At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import any ‘.k4a’ file you obtained by using the androidkindlekey.py script manually, or by exporting from another copy of calibre.

- - - - diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Kindle for Mac and PC Key_Help.htm b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Kindle for Mac and PC Key_Help.htm deleted file mode 100644 index 35f1a50..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Kindle for Mac and PC Key_Help.htm +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - -Managing Kindle for Mac/PC Keys - - - - - -

Managing Kindle for Mac/PC Keys

- - -

If you have upgraded from an earlier version of the plugin, any existing Kindle for Mac/PC keys will have been automatically imported, so you might not need to do any more configuration. In addition, on Windows and Mac, the default Kindle for Mac/PC key is added the first time the plugin is run. Continue reading for key generation and management instructions.

- -

Creating New Keys:

- -

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog prompting you to enter a key name for the default Kindle for Mac/PC key.

-
    -
  • Unique Key Name: this is a unique name you choose to help you identify the key. This name will show in the list of configured keys.
  • -
- -

Click the OK button to create and store the Kindle for Mac/PC key for the current installation of Kindle for Mac/PC. Or Cancel if you don’t want to create the key.

-

New keys are checked against the current list of keys before being added, and duplicates are discarded.

- -

Deleting Keys:

- -

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

- -

Renaming Keys:

- -

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will promt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..

- -

Exporting Keys:

- -

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

- -

Linux Users: WINEPREFIX

- -

Under the list of keys, Linux users will see a text field labeled "WINEPREFIX". If you are use Kindle for PC under Wine, and your wine installation containing Kindle for PC isn't the default Wine installation, you may enter the full path to the correct Wine installation here. Leave blank if you are unsure.

- -

Importing Existing Keyfiles:

- -

At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing ‘.k4i’ key files. Key files might come from being exported from this plugin, or may have been generated using the kindlekey.pyw script running under Wine on Linux systems.

- -

Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.

- - - - diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Mobipocket PID_Help.htm b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Mobipocket PID_Help.htm deleted file mode 100644 index 00aeeca..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Mobipocket PID_Help.htm +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - -Managing Mobipocket PIDs - - - - - -

Managing Mobipocket PIDs

- -

If you have upgraded from an earlier version of the plugin, any existing Mobipocket PIDs will have been automatically imported, so you might not need to do any more configuration.

- - -

Creating New Mobipocket PIDs:

- -

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

-
    -
  • PID: this is a PID used to decrypt your Mobipocket ebooks. It is eight or ten characters long. Mobipocket PIDs are usualy displayed in the About screen of your Mobipocket device.
  • -
- -

Click the OK button to save the PID. Or Cancel if you didn’t want to enter a PID.

- -

Deleting Mobipocket PIDs:

- -

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

- -

Once done creating/deleting PIDs, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.

- - - - diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_eReader Key_Help.htm b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_eReader Key_Help.htm deleted file mode 100644 index c1c78ad..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_eReader Key_Help.htm +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - -Managing eReader Keys - - - - - -

Managing eReader Keys

- -

If you have upgraded from an earlier version of the plugin, any existing eReader (Fictionwise ‘.pdb’) keys will have been automatically imported, so you might not need to do any more configuration. Continue reading for key generation and management instructions.

- -

Creating New Keys:

- -

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

-
    -
  • Unique Key Name: this is a unique name you choose to help you identify the key. This name will show in the list of configured keys. Choose something that will help you remember the data (name, cc#) it was created with.
  • -
  • Your Name: This is the name used by Fictionwise to generate your encryption key. Since Fictionwise has now closed down, you might not have easy access to this. It was often the name on the Credit Card used at Fictionwise.
  • -
  • Credit Card#: this is the default credit card number that was on file with Fictionwise at the time of download of the ebook to be de-DRMed. Just enter the last 8 digits of the number. As with the name, this number will not be stored anywhere on your computer or in calibre. It will only be used in the creation of the one-way hash/key that’s stored in the preferences.
  • -
- -

Click the OK button to create and store the generated key. Or Cancel if you don’t want to create a key.

-

New keys are checked against the current list of keys before being added, and duplicates are discarded.

- -

Deleting Keys:

- -

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

- -

Renaming Keys:

- -

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will promt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..

- -

Exporting Keys:

- -

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

- -

Importing Existing Keyfiles:

- -

At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing ‘.b63’ key files that have previously been exported.

- -

Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.

- - - - diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/__init__.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/__init__.py deleted file mode 100644 index 553687d..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/__init__.py +++ /dev/null @@ -1,647 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# __init__.py for DeDRM_plugin -# Copyright © 2008-2018 Apprentice Harper et al. - -__license__ = 'GPL v3' -__docformat__ = 'restructuredtext en' - - -# Released under the terms of the GNU General Public Licence, version 3 -# -# -# All credit given to i♥cabbages and The Dark Reverser for the original standalone scripts. -# We had the much easier job of converting them to a calibre plugin. -# -# This plugin is meant to decrypt eReader PDBs, Adobe Adept ePubs, Barnes & Noble ePubs, -# Adobe Adept PDFs, Amazon Kindle and Mobipocket files without having to -# install any dependencies... other than having calibre installed, of course. -# -# Configuration: -# Check out the plugin's configuration settings by clicking the "Customize plugin" -# button when you have the "DeDRM" plugin highlighted (under Preferences-> -# Plugins->File type plugins). Once you have the configuration dialog open, you'll -# see a Help link on the top right-hand side. -# -# Revision history: -# 6.0.0 - Initial release -# 6.0.1 - Bug Fixes for Windows App, Kindle for Mac and Windows Adobe Digital Editions -# 6.0.2 - Restored call to Wine to get Kindle for PC keys, added for ADE -# 6.0.3 - Fixes for Kindle for Mac and Windows non-ascii user names -# 6.0.4 - Fixes for stand-alone scripts and applications -# and pdb files in plugin and initial conversion of prefs. -# 6.0.5 - Fix a key issue -# 6.0.6 - Fix up an incorrect function call -# 6.0.7 - Error handling for incomplete PDF metadata -# 6.0.8 - Fixes a Wine key issue and topaz support -# 6.0.9 - Ported to work with newer versions of Calibre (moved to Qt5). Still supports older Qt4 versions. -# 6.1.0 - Fixed multiple books import problem and PDF import with no key problem -# 6.2.0 - Support for getting B&N key from nook Study log. Fix for UTF-8 filenames in Adobe ePubs. -# Fix for not copying needed files. Fix for getting default Adobe key for PDFs -# 6.2.1 - Fix for non-ascii Windows user names -# 6.2.2 - Added URL method for B&N/nook books -# 6.3.0 - Added in Kindle for Android serial number solution -# 6.3.1 - Version number bump for clarity -# 6.3.2 - Fixed Kindle for Android help file -# 6.3.3 - Bug fix for Kindle for PC support -# 6.3.4 - Fixes for Kindle for Android, Linux, and Kobo 3.17 -# 6.3.5 - Fixes for Linux, and Kobo 3.19 and more logging -# 6.3.6 - Fixes for ADE ePub and PDF introduced in 6.3.5 -# 6.4.0 - Updated for new Kindle for PC encryption -# 6.4.1 - Fix for some new tags in Topaz ebooks. -# 6.4.2 - Fix for more new tags in Topaz ebooks and very small Topaz ebooks -# 6.4.3 - Fix for error that only appears when not in debug mode -# Also includes fix for Macs with bonded ethernet ports -# 6.5.0 - Big update to Macintosh app -# Fix for some more 'new' tags in Topaz ebooks. -# Fix an error in wineutils.py -# 6.5.1 - Updated version number, added PDF check for DRM-free documents -# 6.5.2 - Another Topaz fix -# 6.5.3 - Warn about KFX files explicitly -# 6.5.4 - Mac App Fix, improve PDF decryption, handle latest tcl changes in ActivePython -# 6.5.5 - Finally a fix for the Windows non-ASCII user names. -# 6.6.0 - Add kfx and kfx-zip as supported file types (also invoke this plugin if the original -# imported format was azw8 since that may be converted to kfx) - - -""" -Decrypt DRMed ebooks. -""" - -PLUGIN_NAME = u"DeDRM" -PLUGIN_VERSION_TUPLE = (6, 6, 0) -PLUGIN_VERSION = u".".join([unicode(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, re -import time -import zipfile -import traceback -from zipfile import ZipFile - -class DeDRMError(Exception): - pass - -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 safely -# 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") - try: - self.stream.write(data) - self.stream.flush() - except: - # We can do nothing if a write fails - pass - def __getattr__(self, attr): - return getattr(self.stream, attr) - -class DeDRM(FileTypePlugin): - name = PLUGIN_NAME - description = u"Removes DRM from Amazon Kindle, Adobe Adept (including Kobo), Barnes & Noble, Mobipocket and eReader ebooks. Credit given to i♥cabbages and The Dark Reverser for the original stand-alone scripts." - supported_platforms = ['linux', 'osx', 'windows'] - author = u"Apprentice Alf, Aprentice Harper, The Dark Reverser and i♥cabbages" - version = PLUGIN_VERSION_TUPLE - minimum_calibre_version = (1, 0, 0) # Compiled python libraries cannot be imported in earlier versions. - file_types = set(['epub','pdf','pdb','prc','mobi','pobi','azw','azw1','azw3','azw4','azw8','tpz','kfx','kfx-zip']) - on_import = True - priority = 600 - - - def initialize(self): - """ - Dynamic modules can't be imported/loaded from a zipfile. - So this routine will extract the appropriate - library for the target OS and copy it to the 'alfcrypto' subdirectory of - calibre's configuration directory. That 'alfcrypto' directory is then - inserted into the syspath (as the very first entry) in the run function - so the CDLL stuff will work in the alfcrypto.py script. - - The extraction only happens once per version of the plugin - Also perform upgrade of preferences once per version - """ - try: - self.pluginsdir = os.path.join(config_dir,u"plugins") - if not os.path.exists(self.pluginsdir): - os.mkdir(self.pluginsdir) - self.maindir = os.path.join(self.pluginsdir,u"DeDRM") - if not os.path.exists(self.maindir): - os.mkdir(self.maindir) - self.helpdir = os.path.join(self.maindir,u"help") - if not os.path.exists(self.helpdir): - os.mkdir(self.helpdir) - self.alfdir = os.path.join(self.maindir,u"libraryfiles") - if not os.path.exists(self.alfdir): - os.mkdir(self.alfdir) - # only continue if we've never run this version of the plugin before - self.verdir = os.path.join(self.maindir,PLUGIN_VERSION) - if not os.path.exists(self.verdir): - if iswindows: - names = [u"alfcrypto.dll",u"alfcrypto64.dll"] - elif isosx: - names = [u"libalfcrypto.dylib"] - else: - names = [u"libalfcrypto32.so",u"libalfcrypto64.so",u"kindlekey.py",u"adobekey.py",u"subasyncio.py"] - lib_dict = self.load_resources(names) - print u"{0} v{1}: Copying needed library files from plugin's zip".format(PLUGIN_NAME, PLUGIN_VERSION) - - for entry, data in lib_dict.items(): - file_path = os.path.join(self.alfdir, entry) - try: - os.remove(file_path) - except: - pass - - try: - open(file_path,'wb').write(data) - except: - print u"{0} v{1}: Exception when copying needed library files".format(PLUGIN_NAME, PLUGIN_VERSION) - traceback.print_exc() - pass - - # convert old preferences, if necessary. - from calibre_plugins.dedrm.prefs import convertprefs - convertprefs() - - # mark that this version has been initialized - os.mkdir(self.verdir) - except Exception, e: - traceback.print_exc() - raise - - def ePubDecrypt(self,path_to_ebook): - # Create a TemporaryPersistent file to work with. - # Check original epub archive for zip errors. - import calibre_plugins.dedrm.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 \'{2}\' when checking zip archive".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0]) - raise Exception(e) - - # import the decryption keys - import calibre_plugins.dedrm.prefs as prefs - dedrmprefs = prefs.DeDRM_Prefs() - - # import the Barnes & Noble ePub handler - import calibre_plugins.dedrm.ignobleepub as ignobleepub - - - #check the book - if ignobleepub.ignobleBook(inf.name): - print u"{0} v{1}: “{2}” is a secure Barnes & Noble ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)) - - # Attempt to decrypt epub with each encryption key (generated or provided). - for keyname, userkey in dedrmprefs['bandnkeys'].items(): - keyname_masked = 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. - try: - result = ignobleepub.decryptBook(userkey, inf.name, of.name) - except: - print u"{0} v{1}: Exception when trying to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - result = 1 - - of.close() - - if result == 0: - # Decryption was successful. - # Return the modified PersistentTemporary file to calibre. - return of.name - - print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime) - - # perhaps we should see if we can get a key from a log file - print u"{0} v{1}: Looking for new NOOK Study Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - - # get the default NOOK Study keys - defaultkeys = [] - - try: - if iswindows or isosx: - from calibre_plugins.dedrm.ignoblekey import nookkeys - - defaultkeys = nookkeys() - else: # linux - from wineutils import WineGetKeys - - scriptpath = os.path.join(self.alfdir,u"ignoblekey.py") - defaultkeys = WineGetKeys(scriptpath, u".b64",dedrmprefs['adobewineprefix']) - - except: - print u"{0} v{1}: Exception when getting default NOOK Study Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - - newkeys = [] - for keyvalue in defaultkeys: - if keyvalue not in dedrmprefs['bandnkeys'].values(): - newkeys.append(keyvalue) - - if len(newkeys) > 0: - try: - for i,userkey in enumerate(newkeys): - print u"{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION) - - of = self.temporary_file(u".epub") - - # Give the user key, ebook and TemporaryPersistent file to the decryption function. - try: - result = ignobleepub.decryptBook(userkey, inf.name, of.name) - except: - print u"{0} v{1}: Exception when trying to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - result = 1 - - of.close() - - if result == 0: - # Decryption was a success - # Store the new successful key in the defaults - print u"{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION) - try: - dedrmprefs.addnamedvaluetoprefs('bandnkeys','nook_Study_key',keyvalue) - dedrmprefs.writeprefs() - print u"{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) - except: - print u"{0} v{1}: Exception saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - # Return the modified PersistentTemporary file to calibre. - return of.name - - print u"{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) - except Exception, e: - pass - - print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) - raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) - - # import the Adobe Adept ePub handler - import calibre_plugins.dedrm.ineptepub as ineptepub - - if ineptepub.adeptBook(inf.name): - print u"{0} v{1}: {2} is a secure Adobe Adept ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)) - - # Attempt to decrypt epub with each encryption key (generated or provided). - for keyname, userkeyhex in dedrmprefs['adeptkeys'].items(): - userkey = userkeyhex.decode('hex') - print u"{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname) - of = self.temporary_file(u".epub") - - # Give the user key, ebook and TemporaryPersistent file to the decryption function. - try: - result = ineptepub.decryptBook(userkey, inf.name, of.name) - except: - print u"{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - result = 1 - - try: - of.close() - except: - print u"{0} v{1}: Exception closing temporary file after {2:.1f} seconds. Ignored.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - - if result == 0: - # Decryption was successful. - # Return the modified PersistentTemporary file to calibre. - print u"{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime) - return of.name - - print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime) - - # perhaps we need to get a new default ADE key - print u"{0} v{1}: Looking for new default Adobe Digital Editions Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - - # get the default Adobe keys - defaultkeys = [] - - try: - if iswindows or isosx: - from calibre_plugins.dedrm.adobekey import adeptkeys - - defaultkeys = adeptkeys() - else: # linux - from wineutils import WineGetKeys - - scriptpath = os.path.join(self.alfdir,u"adobekey.py") - defaultkeys = WineGetKeys(scriptpath, u".der",dedrmprefs['adobewineprefix']) - - self.default_key = defaultkeys[0] - except: - print u"{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - self.default_key = u"" - - newkeys = [] - for keyvalue in defaultkeys: - if keyvalue.encode('hex') not in dedrmprefs['adeptkeys'].values(): - newkeys.append(keyvalue) - - if len(newkeys) > 0: - try: - for i,userkey in enumerate(newkeys): - print u"{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION) - of = self.temporary_file(u".epub") - - # Give the user key, ebook and TemporaryPersistent file to the decryption function. - try: - result = ineptepub.decryptBook(userkey, inf.name, of.name) - except: - print u"{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - result = 1 - - of.close() - - if result == 0: - # Decryption was a success - # Store the new successful key in the defaults - print u"{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION) - try: - dedrmprefs.addnamedvaluetoprefs('adeptkeys','default_key',keyvalue.encode('hex')) - dedrmprefs.writeprefs() - print u"{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) - except: - print u"{0} v{1}: Exception when saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - print u"{0} v{1}: Decrypted with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) - # Return the modified PersistentTemporary file to calibre. - return of.name - - print u"{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) - except Exception, e: - print u"{0} v{1}: Unexpected Exception trying a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - pass - - # Something went wrong with decryption. - print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) - raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)) - - # Not a Barnes & Noble nor an Adobe Adept - # Import the fixed epub. - print u"{0} v{1}: “{2}” is neither an Adobe Adept nor a Barnes & Noble encrypted ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)) - raise DeDRMError(u"{0} v{1}: Couldn't decrypt after {2:.1f} seconds. DRM free perhaps?".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)) - - def PDFDecrypt(self,path_to_ebook): - import calibre_plugins.dedrm.prefs as prefs - import calibre_plugins.dedrm.ineptpdf - - dedrmprefs = prefs.DeDRM_Prefs() - # Attempt to decrypt epub with each encryption key (generated or provided). - print u"{0} v{1}: {2} is a PDF ebook".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)) - for keyname, userkeyhex in dedrmprefs['adeptkeys'].items(): - userkey = userkeyhex.decode('hex') - print u"{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname) - of = self.temporary_file(u".pdf") - - # Give the user key, ebook and TemporaryPersistent file to the decryption function. - try: - result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name) - except: - print u"{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - result = 1 - - of.close() - - if result == 0: - # Decryption was successful. - # Return the modified PersistentTemporary file to calibre. - return of.name - - print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime) - - # perhaps we need to get a new default ADE key - print u"{0} v{1}: Looking for new default Adobe Digital Editions Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - - # get the default Adobe keys - defaultkeys = [] - - try: - if iswindows or isosx: - from calibre_plugins.dedrm.adobekey import adeptkeys - - defaultkeys = adeptkeys() - else: # linux - from wineutils import WineGetKeys - - scriptpath = os.path.join(self.alfdir,u"adobekey.py") - defaultkeys = WineGetKeys(scriptpath, u".der",dedrmprefs['adobewineprefix']) - - self.default_key = defaultkeys[0] - except: - print u"{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - self.default_key = u"" - - newkeys = [] - for keyvalue in defaultkeys: - if keyvalue.encode('hex') not in dedrmprefs['adeptkeys'].values(): - newkeys.append(keyvalue) - - if len(newkeys) > 0: - try: - for i,userkey in enumerate(newkeys): - print u"{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION) - of = self.temporary_file(u".pdf") - - # Give the user key, ebook and TemporaryPersistent file to the decryption function. - try: - result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name) - except: - print u"{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - result = 1 - - of.close() - - if result == 0: - # Decryption was a success - # Store the new successful key in the defaults - print u"{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION) - try: - dedrmprefs.addnamedvaluetoprefs('adeptkeys','default_key',keyvalue.encode('hex')) - dedrmprefs.writeprefs() - print u"{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) - except: - print u"{0} v{1}: Exception when saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - # Return the modified PersistentTemporary file to calibre. - return of.name - - print u"{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) - except Exception, e: - pass - - # Something went wrong with decryption. - print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) - raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) - - - def KindleMobiDecrypt(self,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 this import here so the custom libs can be - # extracted to the appropriate places beforehand these routines - # look for them. - import calibre_plugins.dedrm.prefs as prefs - import calibre_plugins.dedrm.k4mobidedrm - - dedrmprefs = prefs.DeDRM_Prefs() - pids = dedrmprefs['pids'] - serials = dedrmprefs['serials'] - for android_serials_list in dedrmprefs['androidkeys'].values(): - #print android_serials_list - serials.extend(android_serials_list) - #print serials - androidFiles = [] - kindleDatabases = dedrmprefs['kindlekeys'].items() - - try: - book = k4mobidedrm.GetDecryptedBook(path_to_ebook,kindleDatabases,androidFiles,serials,pids,self.starttime) - except Exception, e: - decoded = False - # perhaps we need to get a new default Kindle for Mac/PC key - defaultkeys = [] - print u"{0} v{1}: Failed to decrypt with error: {2}".format(PLUGIN_NAME, PLUGIN_VERSION,e.args[0]) - print u"{0} v{1}: Looking for new default Kindle Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - - try: - if iswindows or isosx: - from calibre_plugins.dedrm.kindlekey import kindlekeys - - defaultkeys = kindlekeys() - else: # linux - from wineutils import WineGetKeys - - scriptpath = os.path.join(self.alfdir,u"kindlekey.py") - defaultkeys = WineGetKeys(scriptpath, u".k4i",dedrmprefs['kindlewineprefix']) - except: - print u"{0} v{1}: Exception when getting default Kindle Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - pass - - newkeys = {} - for i,keyvalue in enumerate(defaultkeys): - keyname = u"default_key_{0:d}".format(i+1) - if keyvalue not in dedrmprefs['kindlekeys'].values(): - newkeys[keyname] = keyvalue - if len(newkeys) > 0: - print u"{0} v{1}: Found {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), u"key" if len(newkeys)==1 else u"keys") - try: - book = k4mobidedrm.GetDecryptedBook(path_to_ebook,newkeys.items(),[],[],[],self.starttime) - decoded = True - # store the new successful keys in the defaults - print u"{0} v{1}: Saving {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), u"key" if len(newkeys)==1 else u"keys") - for keyvalue in newkeys.values(): - dedrmprefs.addnamedvaluetoprefs('kindlekeys','default_key',keyvalue) - dedrmprefs.writeprefs() - except Exception, e: - pass - if not decoded: - #if you reached here then no luck raise and exception - print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) - raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)) - - of = self.temporary_file(book.getBookExtension()) - book.getFile(of.name) - of.close() - book.cleanup() - return of.name - - - def eReaderDecrypt(self,path_to_ebook): - - import calibre_plugins.dedrm.prefs as prefs - import calibre_plugins.dedrm.erdr2pml - - dedrmprefs = prefs.DeDRM_Prefs() - # Attempt to decrypt epub with each encryption key (generated or provided). - for keyname, userkey in dedrmprefs['ereaderkeys'].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".pmlz") - - # Give the userkey, ebook and TemporaryPersistent file to the decryption function. - result = erdr2pml.decryptBook(path_to_ebook, of.name, True, userkey.decode('hex')) - - of.close() - - # Decryption was successful return the modified PersistentTemporary - # file to Calibre's import process. - if result == 0: - print u"{0} v{1}: Successfully decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime) - return of.name - - print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime) - - print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) - raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) - - - 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)) - self.starttime = time.time() - - booktype = os.path.splitext(path_to_ebook)[1].lower()[1:] - if booktype in ['prc','mobi','pobi','azw','azw1','azw3','azw4','tpz','kfx-zip']: - # Kindle/Mobipocket - decrypted_ebook = self.KindleMobiDecrypt(path_to_ebook) - elif booktype == 'pdb': - # eReader - decrypted_ebook = self.eReaderDecrypt(path_to_ebook) - pass - elif booktype == 'pdf': - # Adobe Adept PDF (hopefully) - decrypted_ebook = self.PDFDecrypt(path_to_ebook) - pass - elif booktype == 'epub': - # Adobe Adept or B&N ePub - decrypted_ebook = self.ePubDecrypt(path_to_ebook) - else: - print u"Unknown booktype {0}. Passing back to calibre unchanged".format(booktype) - return path_to_ebook - print u"{0} v{1}: Finished after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) - return decrypted_ebook - - def is_customizable(self): - # return true to allow customization via the Plugin->Preferences. - return True - - def config_widget(self): - import calibre_plugins.dedrm.config as config - return config.ConfigWidget(self.plugin_path, self.alfdir) - - def save_settings(self, config_widget): - config_widget.save_settings() diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/activitybar.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/activitybar.py deleted file mode 100644 index b21c01d..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/activitybar.py +++ /dev/null @@ -1,75 +0,0 @@ -import sys -import Tkinter -import Tkconstants - -class ActivityBar(Tkinter.Frame): - - def __init__(self, master, length=300, height=20, barwidth=15, interval=50, bg='white', fillcolor='orchid1',\ - bd=2, relief=Tkconstants.GROOVE, *args, **kw): - Tkinter.Frame.__init__(self, master, bg=bg, width=length, height=height, *args, **kw) - self._master = master - self._interval = interval - self._maximum = length - self._startx = 0 - self._barwidth = barwidth - self._bardiv = length / barwidth - if self._bardiv < 10: - self._bardiv = 10 - stopx = self._startx + self._barwidth - if stopx > self._maximum: - stopx = self._maximum - # self._canv = Tkinter.Canvas(self, bg=self['bg'], width=self['width'], height=self['height'],\ - # highlightthickness=0, relief='flat', bd=0) - self._canv = Tkinter.Canvas(self, bg=self['bg'], width=self['width'], height=self['height'],\ - highlightthickness=0, relief=relief, bd=bd) - self._canv.pack(fill='both', expand=1) - self._rect = self._canv.create_rectangle(0, 0, self._canv.winfo_reqwidth(), self._canv.winfo_reqheight(), fill=fillcolor, width=0) - - self._set() - self.bind('', self._update_coords) - self._running = False - - def _update_coords(self, event): - '''Updates the position of the rectangle inside the canvas when the size of - the widget gets changed.''' - # looks like we have to call update_idletasks() twice to make sure - # to get the results we expect - self._canv.update_idletasks() - self._maximum = self._canv.winfo_width() - self._startx = 0 - self._barwidth = self._maximum / self._bardiv - if self._barwidth < 2: - self._barwidth = 2 - stopx = self._startx + self._barwidth - if stopx > self._maximum: - stopx = self._maximum - self._canv.coords(self._rect, 0, 0, stopx, self._canv.winfo_height()) - self._canv.update_idletasks() - - def _set(self): - if self._startx < 0: - self._startx = 0 - if self._startx > self._maximum: - self._startx = self._startx % self._maximum - stopx = self._startx + self._barwidth - if stopx > self._maximum: - stopx = self._maximum - self._canv.coords(self._rect, self._startx, 0, stopx, self._canv.winfo_height()) - self._canv.update_idletasks() - - def start(self): - self._running = True - self.after(self._interval, self._step) - - def stop(self): - self._running = False - self._set() - - def _step(self): - if self._running: - stepsize = self._barwidth / 4 - if stepsize < 2: - stepsize = 2 - self._startx += stepsize - self._set() - self.after(self._interval, self._step) diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/adobekey.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/adobekey.py deleted file mode 100644 index 7fbd516..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/adobekey.py +++ /dev/null @@ -1,603 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# adobekey.pyw, version 6.0 -# Copyright © 2009-2010 i♥cabbages - -# Released under the terms of the GNU General Public Licence, version 3 -# - -# Modified 2010–2016 by several people - -# Windows users: Before running this program, you must first install Python. -# We recommend ActiveState Python 2.7.X for Windows (x86) from -# http://www.activestate.com/activepython/downloads. -# You must also install PyCrypto from -# http://www.voidspace.org.uk/python/modules.shtml#pycrypto -# (make certain to install the version for Python 2.7). -# Then save this script file as adobekey.pyw and double-click on it to run it. -# It will create a file named adobekey_1.der in in the same directory as the script. -# This is your Adobe Digital Editions user key. -# -# Mac OS X users: Save this script file as adobekey.pyw. You can run this -# program from the command line (python adobekey.pyw) or by double-clicking -# it when it has been associated with PythonLauncher. It will create a file -# named adobekey_1.der in the same directory as the script. -# This is your Adobe Digital Editions 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 - Revised to allow use in Plugins to eliminate need for duplicate code -# 5.7 - Unicode support added, renamed adobekey from ineptkey -# 5.8 - Added getkey interface for Windows DeDRM application -# 5.9 - moved unicode_argv call inside main for Windows DeDRM compatibility -# 6.0 - Work if TkInter is missing - -""" -Retrieve Adobe ADEPT user key. -""" - -__license__ = 'GPL v3' -__version__ = '6.0' - -import sys, os, struct, getopt - -# 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 '?'. So use shell32.GetCommandLineArgvW to get sys.argv - # as a list of Unicode strings and encode them as utf-8 - - from ctypes import POINTER, byref, cdll, c_int, windll - from ctypes.wintypes import LPCWSTR, LPWSTR - - GetCommandLineW = cdll.kernel32.GetCommandLineW - GetCommandLineW.argtypes = [] - GetCommandLineW.restype = LPCWSTR - - CommandLineToArgvW = windll.shell32.CommandLineToArgvW - CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] - CommandLineToArgvW.restype = POINTER(LPWSTR) - - cmd = GetCommandLineW() - argc = c_int(0) - argv = CommandLineToArgvW(cmd, byref(argc)) - if argc.value > 0: - # Remove Python executable and commands if present - start = argc.value - len(sys.argv) - return [argv[i] 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"adobekey.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 - -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 adeptkeys(): - 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) - device = winreg.QueryValueEx(regkey, 'key')[0] - except WindowsError: - raise ADEPTError("Adobe Digital Editions not activated") - 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])] - #print "found key:",userkey.encode('hex') - keys.append(userkey) - if len(keys) == 0: - raise ADEPTError('Could not locate privateLicenseKey') - print u"Found {0:d} keys".format(len(keys)) - 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(): - import warnings - warnings.filterwarnings('ignore', category=FutureWarning) - - 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) - ActDatPath = "activation.dat" - 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 adeptkeys(): - actpath = findActivationDat() - if actpath is None: - raise ADEPTError("Could not find ADE activation.dat file.") - 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 adeptkeys(): - raise ADEPTError("This script only supports Windows and Mac OS X.") - return [] - -# interface for Python DeDRM -def getkey(outpath): - keys = adeptkeys() - if len(keys) > 0: - if not os.path.isdir(outpath): - outfile = outpath - with file(outfile, 'wb') as keyfileout: - keyfileout.write(keys[0]) - print u"Saved a key to {0}".format(outfile) - else: - keycount = 0 - for key in keys: - while True: - keycount += 1 - outfile = os.path.join(outpath,u"adobekey_{0:d}.der".format(keycount)) - if not os.path.exists(outfile): - break - with file(outfile, 'wb') as keyfileout: - keyfileout.write(key) - print u"Saved a key to {0}".format(outfile) - return True - return False - -def usage(progname): - print u"Finds, decrypts and saves the default Adobe Adept encryption key(s)." - print u"Keys are saved to the current directory, or a specified output directory." - print u"If a file name is passed instead of a directory, only the first key is saved, in that file." - print u"Usage:" - print u" {0:s} [-h] []".format(progname) - -def cli_main(): - sys.stdout=SafeUnbuffered(sys.stdout) - sys.stderr=SafeUnbuffered(sys.stderr) - argv=unicode_argv() - progname = os.path.basename(argv[0]) - print u"{0} v{1}\nCopyright © 2009-2013 i♥cabbages and Apprentice Alf".format(progname,__version__) - - try: - opts, args = getopt.getopt(argv[1:], "h") - except getopt.GetoptError, err: - print u"Error in options or arguments: {0}".format(err.args[0]) - usage(progname) - sys.exit(2) - - for o, a in opts: - if o == "-h": - usage(progname) - sys.exit(0) - - if len(args) > 1: - usage(progname) - sys.exit(2) - - if len(args) == 1: - # save to the specified file or directory - outpath = args[0] - if not os.path.isabs(outpath): - outpath = os.path.abspath(outpath) - else: - # save to the same directory as the script - outpath = os.path.dirname(argv[0]) - - # make sure the outpath is the - outpath = os.path.realpath(os.path.normpath(outpath)) - - keys = adeptkeys() - if len(keys) > 0: - if not os.path.isdir(outpath): - outfile = outpath - with file(outfile, 'wb') as keyfileout: - keyfileout.write(keys[0]) - print u"Saved a key to {0}".format(outfile) - else: - keycount = 0 - for key in keys: - while True: - keycount += 1 - outfile = os.path.join(outpath,u"adobekey_{0:d}.der".format(keycount)) - if not os.path.exists(outfile): - break - with file(outfile, 'wb') as keyfileout: - keyfileout.write(key) - print u"Saved a key to {0}".format(outfile) - else: - print u"Could not retrieve Adobe Adept key." - return 0 - - -def gui_main(): - try: - import Tkinter - import Tkconstants - import tkMessageBox - import traceback - except: - return cli_main() - - class ExceptionDialog(Tkinter.Frame): - def __init__(self, root, text): - Tkinter.Frame.__init__(self, root, border=5) - 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) - - - argv=unicode_argv() - root = Tkinter.Tk() - root.withdraw() - progpath, progname = os.path.split(argv[0]) - success = False - try: - keys = adeptkeys() - keycount = 0 - for key in keys: - while True: - keycount += 1 - outfile = os.path.join(progpath,u"adobekey_{0:d}.der".format(keycount)) - if not os.path.exists(outfile): - break - - with file(outfile, 'wb') as keyfileout: - keyfileout.write(key) - success = True - tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile)) - except ADEPTError, e: - tkMessageBox.showerror(progname, u"Error: {0}".format(str(e))) - except Exception: - root.wm_state('normal') - root.title(progname) - text = traceback.format_exc() - ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1) - root.mainloop() - if not success: - return 1 - return 0 - -if __name__ == '__main__': - if len(sys.argv) > 1: - sys.exit(cli_main()) - sys.exit(gui_main()) diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/aescbc.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/aescbc.py deleted file mode 100644 index 5667511..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/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/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/alfcrypto.dll b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/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/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/alfcrypto.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/alfcrypto.py deleted file mode 100644 index 036ba10..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/alfcrypto.py +++ /dev/null @@ -1,301 +0,0 @@ -#!/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 - 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' - - # hard code to local location for libalfcrypto - libalfcrypto = os.path.join(sys.path[0],name_of_lib) - if not os.path.isfile(libalfcrypto): - libalfcrypto = os.path.join(sys.path[0], 'lib', name_of_lib) - if not os.path.isfile(libalfcrypto): - libalfcrypto = os.path.join('.',name_of_lib) - if not os.path.isfile(libalfcrypto): - raise Exception('libalfcrypto not found at %s' % libalfcrypto) - - libalfcrypto = CDLL(libalfcrypto) - - 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 u"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: - 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])) - 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 - - print u"Using Library AlfCrypto Python" - 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/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/alfcrypto64.dll b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/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/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/alfcrypto_src.zip b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/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;* 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"kindlekey.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 - -STORAGE = u"backup.ab" -STORAGE1 = u"AmazonSecureStorage.xml" -STORAGE2 = u"map_data_storage.db" - -class AndroidObfuscation(object): - '''AndroidObfuscation - For the key, it's written in java, and run in android dalvikvm - ''' - - key = a2b_hex('0176e04c9408b1702d90be333fd53523') - - def encrypt(self, plaintext): - cipher = self._get_cipher() - padding = len(self.key) - len(plaintext) % len(self.key) - plaintext += chr(padding) * padding - return b2a_hex(cipher.encrypt(plaintext)) - - def decrypt(self, ciphertext): - cipher = self._get_cipher() - plaintext = cipher.decrypt(a2b_hex(ciphertext)) - return plaintext[:-ord(plaintext[-1])] - - def _get_cipher(self): - try: - from Crypto.Cipher import AES - return AES.new(self.key) - except ImportError: - from aescbc import AES, noPadding - return AES(self.key, padding=noPadding()) - -class AndroidObfuscationV2(AndroidObfuscation): - '''AndroidObfuscationV2 - ''' - - count = 503 - password = 'Thomsun was here!' - - def __init__(self, salt): - key = self.password + salt - for _ in range(self.count): - key = md5(key).digest() - self.key = key[:8] - self.iv = key[8:16] - - def _get_cipher(self): - try : - from Crypto.Cipher import DES - return DES.new(self.key, DES.MODE_CBC, self.iv) - except ImportError: - from python_des import Des, CBC - return Des(self.key, CBC, self.iv) - -def parse_preference(path): - ''' parse android's shared preference xml ''' - storage = {} - read = open(path) - for line in read: - line = line.strip() - # value - if line.startswith(' 0: - dsns.append(userdata_utf8) - except: - print "Error getting one of the device serial name keys" - traceback.print_exc() - pass - dsns = list(set(dsns)) - - cursor.execute('''select userdata_value from userdata where userdata_key like '%/%kindle.account.tokens%' ''') - userdata_keys = cursor.fetchall() - tokens = [] - for userdata_row in userdata_keys: - try: - if userdata_row and userdata_row[0]: - userdata_utf8 = userdata_row[0].encode('utf8') - if len(userdata_utf8) > 0: - tokens.append(userdata_utf8) - except: - print "Error getting one of the account token keys" - traceback.print_exc() - pass - tokens = list(set(tokens)) - - serials = [] - for x in dsns: - serials.append(x) - for y in tokens: - serials.append('%s%s' % (x, y)) - for y in tokens: - serials.append(y) - return serials - -def get_serials(path=STORAGE): - '''get serials from files in from android backup.ab - backup.ab can be get using adb command: - shell> adb backup com.amazon.kindle - or from individual files if they're passed. - ''' - if not os.path.isfile(path): - return [] - - basename = os.path.basename(path) - if basename == STORAGE1: - return get_serials1(path) - elif basename == STORAGE2: - return get_serials2(path) - - output = None - try : - read = open(path, 'rb') - head = read.read(24) - if head[:14] == 'ANDROID BACKUP': - output = StringIO(zlib.decompress(read.read())) - except Exception: - pass - finally: - read.close() - - if not output: - return [] - - serials = [] - tar = tarfile.open(fileobj=output) - for member in tar.getmembers(): - if member.name.strip().endswith(STORAGE1): - write = tempfile.NamedTemporaryFile(mode='wb', delete=False) - write.write(tar.extractfile(member).read()) - write.close() - write_path = os.path.abspath(write.name) - serials.extend(get_serials1(write_path)) - os.remove(write_path) - elif member.name.strip().endswith(STORAGE2): - write = tempfile.NamedTemporaryFile(mode='wb', delete=False) - write.write(tar.extractfile(member).read()) - write.close() - write_path = os.path.abspath(write.name) - serials.extend(get_serials2(write_path)) - os.remove(write_path) - return list(set(serials)) - -__all__ = [ 'get_serials', 'getkey'] - -# procedure for CLI and GUI interfaces -# returns single or multiple keys (one per line) in the specified file -def getkey(outfile, inpath): - keys = get_serials(inpath) - if len(keys) > 0: - with file(outfile, 'w') as keyfileout: - for key in keys: - keyfileout.write(key) - keyfileout.write("\n") - return True - return False - - -def usage(progname): - print u"Decrypts the serial number(s) of Kindle For Android from Android backup or file" - print u"Get backup.ab file using adb backup com.amazon.kindle for Android 4.0+." - print u"Otherwise extract AmazonSecureStorage.xml from /data/data/com.amazon.kindle/shared_prefs/AmazonSecureStorage.xml" - print u"Or map_data_storage.db from /data/data/com.amazon.kindle/databases/map_data_storage.db" - print u"" - print u"Usage:" - print u" {0:s} [-h] [-b ] []".format(progname) - - -def cli_main(): - sys.stdout=SafeUnbuffered(sys.stdout) - sys.stderr=SafeUnbuffered(sys.stderr) - argv=unicode_argv() - progname = os.path.basename(argv[0]) - print u"{0} v{1}\nCopyright © 2010-2015 Thom, some_updates, Apprentice Alf and Apprentice Harper".format(progname,__version__) - - try: - opts, args = getopt.getopt(argv[1:], "hb:") - except getopt.GetoptError, err: - usage(progname) - print u"\nError in options or arguments: {0}".format(err.args[0]) - return 2 - - inpath = "" - for o, a in opts: - if o == "-h": - usage(progname) - return 0 - if o == "-b": - inpath = a - - if len(args) > 1: - usage(progname) - return 2 - - if len(args) == 1: - # save to the specified file or directory - outfile = args[0] - if not os.path.isabs(outfile): - outfile = os.path.join(os.path.dirname(argv[0]),outfile) - outfile = os.path.abspath(outfile) - if os.path.isdir(outfile): - outfile = os.path.join(os.path.dirname(argv[0]),"androidkindlekey.k4a") - else: - # save to the same directory as the script - outfile = os.path.join(os.path.dirname(argv[0]),"androidkindlekey.k4a") - - # make sure the outpath is OK - outfile = os.path.realpath(os.path.normpath(outfile)) - - if not os.path.isfile(inpath): - usage(progname) - print u"\n{0:s} file not found".format(inpath) - return 2 - - if getkey(outfile, inpath): - print u"\nSaved Kindle for Android key to {0}".format(outfile) - else: - print u"\nCould not retrieve Kindle for Android key." - return 0 - - -def gui_main(): - try: - import Tkinter - import Tkconstants - import tkMessageBox - import tkFileDialog - except: - print "Tkinter not installed" - return cli_main() - - class DecryptionDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - self.status = Tkinter.Label(self, text=u"Select backup.ab file") - 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"Backup file").grid(row=0, column=0) - self.keypath = Tkinter.Entry(body, width=40) - self.keypath.grid(row=0, column=1, sticky=sticky) - self.keypath.insert(2, u"backup.ab") - button = Tkinter.Button(body, text=u"...", command=self.get_keypath) - button.grid(row=0, column=2) - buttons = Tkinter.Frame(self) - buttons.pack() - button2 = Tkinter.Button( - buttons, text=u"Extract", width=10, command=self.generate) - button2.pack(side=Tkconstants.LEFT) - Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) - button3 = Tkinter.Button( - buttons, text=u"Quit", width=10, command=self.quit) - button3.pack(side=Tkconstants.RIGHT) - - def get_keypath(self): - keypath = tkFileDialog.askopenfilename( - parent=None, title=u"Select backup.ab file", - defaultextension=u".ab", - filetypes=[('adb backup com.amazon.kindle', '.ab'), - ('All Files', '.*')]) - if keypath: - keypath = os.path.normpath(keypath) - self.keypath.delete(0, Tkconstants.END) - self.keypath.insert(0, keypath) - return - - def generate(self): - inpath = self.keypath.get() - self.status['text'] = u"Getting key..." - try: - keys = get_serials(inpath) - keycount = 0 - for key in keys: - while True: - keycount += 1 - outfile = os.path.join(progpath,u"kindlekey{0:d}.k4a".format(keycount)) - if not os.path.exists(outfile): - break - - with file(outfile, 'w') as keyfileout: - keyfileout.write(key) - success = True - tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile)) - except Exception, e: - self.status['text'] = u"Error: {0}".format(e.args[0]) - return - self.status['text'] = u"Select backup.ab file" - - argv=unicode_argv() - progpath, progname = os.path.split(argv[0]) - root = Tkinter.Tk() - root.title(u"Kindle for Android Key Extraction 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.exit(cli_main()) - sys.exit(gui_main()) diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/argv_utils.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/argv_utils.py deleted file mode 100644 index 85ffaa4..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/argv_utils.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import sys, os -import locale -import codecs - -# get sys.argv arguments and encode them into utf-8 -def unicode_argv(): - if sys.platform.startswith('win'): - # 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"DeDRM.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] - - -def add_cp65001_codec(): - try: - codecs.lookup('cp65001') - except LookupError: - codecs.register( - lambda name: name == 'cp65001' and codecs.lookup('utf-8') or None) - return - - -def set_utf8_default_encoding(): - if sys.getdefaultencoding() == 'utf-8': - return - - # Regenerate setdefaultencoding. - reload(sys) - sys.setdefaultencoding('utf-8') - - for attr in dir(locale): - if attr[0:3] != 'LC_': - continue - aref = getattr(locale, attr) - try: - locale.setlocale(aref, '') - except locale.Error: - continue - try: - lang = locale.getlocale(aref)[0] - except (TypeError, ValueError): - continue - if lang: - try: - locale.setlocale(aref, (lang, 'UTF-8')) - except locale.Error: - os.environ[attr] = lang + '.UTF-8' - try: - locale.setlocale(locale.LC_ALL, '') - except locale.Error: - pass - return - - diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/askfolder_ed.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/askfolder_ed.py deleted file mode 100644 index a4a2ae0..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/askfolder_ed.py +++ /dev/null @@ -1,211 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab - -# to work around tk_chooseDirectory not properly returning unicode paths on Windows -# need to use a dialog that can be hacked up to actually return full unicode paths -# originally based on AskFolder from EasyDialogs for Windows but modified to fix it -# to actually use unicode for path - -# The original license for EasyDialogs is as follows -# -# Copyright (c) 2003-2005 Jimmy Retzlaff -# -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# 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 OR COPYRIGHT HOLDERS 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. - -""" -AskFolder(...) -- Ask the user to select a folder Windows specific -""" - -import os - -import ctypes -from ctypes import POINTER, byref, cdll, c_int, windll -from ctypes.wintypes import LPCWSTR, LPWSTR -import ctypes.wintypes as wintypes - - -__all__ = ['AskFolder'] - -# Load required Windows DLLs -ole32 = ctypes.windll.ole32 -shell32 = ctypes.windll.shell32 -user32 = ctypes.windll.user32 - - -# Windows Constants -BFFM_INITIALIZED = 1 -BFFM_SETOKTEXT = 1129 -BFFM_SETSELECTIONA = 1126 -BFFM_SETSELECTIONW = 1127 -BIF_EDITBOX = 16 -BS_DEFPUSHBUTTON = 1 -CB_ADDSTRING = 323 -CB_GETCURSEL = 327 -CB_SETCURSEL = 334 -CDM_SETCONTROLTEXT = 1128 -EM_GETLINECOUNT = 186 -EM_GETMARGINS = 212 -EM_POSFROMCHAR = 214 -EM_SETSEL = 177 -GWL_STYLE = -16 -IDC_STATIC = -1 -IDCANCEL = 2 -IDNO = 7 -IDOK = 1 -IDYES = 6 -MAX_PATH = 260 -OFN_ALLOWMULTISELECT = 512 -OFN_ENABLEHOOK = 32 -OFN_ENABLESIZING = 8388608 -OFN_ENABLETEMPLATEHANDLE = 128 -OFN_EXPLORER = 524288 -OFN_OVERWRITEPROMPT = 2 -OPENFILENAME_SIZE_VERSION_400 = 76 -PBM_GETPOS = 1032 -PBM_SETMARQUEE = 1034 -PBM_SETPOS = 1026 -PBM_SETRANGE = 1025 -PBM_SETRANGE32 = 1030 -PBS_MARQUEE = 8 -PM_REMOVE = 1 -SW_HIDE = 0 -SW_SHOW = 5 -SW_SHOWNORMAL = 1 -SWP_NOACTIVATE = 16 -SWP_NOMOVE = 2 -SWP_NOSIZE = 1 -SWP_NOZORDER = 4 -VER_PLATFORM_WIN32_NT = 2 -WM_COMMAND = 273 -WM_GETTEXT = 13 -WM_GETTEXTLENGTH = 14 -WM_INITDIALOG = 272 -WM_NOTIFY = 78 - -# Windows function prototypes -BrowseCallbackProc = ctypes.WINFUNCTYPE(ctypes.c_int, wintypes.HWND, ctypes.c_uint, wintypes.LPARAM, wintypes.LPARAM) - -# Windows types -LPCTSTR = ctypes.c_char_p -LPTSTR = ctypes.c_char_p -LPVOID = ctypes.c_voidp -TCHAR = ctypes.c_char - -class BROWSEINFO(ctypes.Structure): - _fields_ = [ - ("hwndOwner", wintypes.HWND), - ("pidlRoot", LPVOID), - ("pszDisplayName", LPTSTR), - ("lpszTitle", LPCTSTR), - ("ulFlags", ctypes.c_uint), - ("lpfn", BrowseCallbackProc), - ("lParam", wintypes.LPARAM), - ("iImage", ctypes.c_int) - ] - - -# Utilities -def CenterWindow(hwnd): - desktopRect = GetWindowRect(user32.GetDesktopWindow()) - myRect = GetWindowRect(hwnd) - x = width(desktopRect) // 2 - width(myRect) // 2 - y = height(desktopRect) // 2 - height(myRect) // 2 - user32.SetWindowPos(hwnd, 0, - desktopRect.left + x, - desktopRect.top + y, - 0, 0, - SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER - ) - - -def GetWindowRect(hwnd): - rect = wintypes.RECT() - user32.GetWindowRect(hwnd, ctypes.byref(rect)) - return rect - -def width(rect): - return rect.right-rect.left - -def height(rect): - return rect.bottom-rect.top - - -def AskFolder( - message=None, - version=None, - defaultLocation=None, - location=None, - windowTitle=None, - actionButtonLabel=None, - cancelButtonLabel=None, - multiple=None): - """Display a dialog asking the user for select a folder. - modified to use unicode strings as much as possible - returns unicode path - """ - - def BrowseCallback(hwnd, uMsg, lParam, lpData): - if uMsg == BFFM_INITIALIZED: - if actionButtonLabel: - label = unicode(actionButtonLabel, errors='replace') - user32.SendMessageW(hwnd, BFFM_SETOKTEXT, 0, label) - if cancelButtonLabel: - label = unicode(cancelButtonLabel, errors='replace') - cancelButton = user32.GetDlgItem(hwnd, IDCANCEL) - if cancelButton: - user32.SetWindowTextW(cancelButton, label) - if windowTitle: - title = unicode(windowTitle, erros='replace') - user32.SetWindowTextW(hwnd, title) - if defaultLocation: - user32.SendMessageW(hwnd, BFFM_SETSELECTIONW, 1, defaultLocation.replace('/', '\\')) - if location: - x, y = location - desktopRect = wintypes.RECT() - user32.GetWindowRect(0, ctypes.byref(desktopRect)) - user32.SetWindowPos(hwnd, 0, - desktopRect.left + x, - desktopRect.top + y, 0, 0, - SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER) - else: - CenterWindow(hwnd) - return 0 - - # This next line is needed to prevent gc of the callback - callback = BrowseCallbackProc(BrowseCallback) - - browseInfo = BROWSEINFO() - browseInfo.pszDisplayName = ctypes.c_char_p('\0' * (MAX_PATH+1)) - browseInfo.lpszTitle = message - browseInfo.lpfn = callback - - pidl = shell32.SHBrowseForFolder(ctypes.byref(browseInfo)) - if not pidl: - result = None - else: - path = LPCWSTR(u" " * (MAX_PATH+1)) - shell32.SHGetPathFromIDListW(pidl, path) - ole32.CoTaskMemFree(pidl) - result = path.value - return result - - - - diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/config.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/config.py deleted file mode 100644 index 3a56e44..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/config.py +++ /dev/null @@ -1,1021 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -__license__ = 'GPL v3' - -# Standard Python modules. -import os, traceback, json - -# PyQT4 modules (part of calibre). -try: - from PyQt5.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, - QGroupBox, QPushButton, QListWidget, QListWidgetItem, - QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl) -except ImportError: - from PyQt4.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, - QGroupBox, QPushButton, QListWidget, QListWidgetItem, - QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl) -try: - from PyQt5 import Qt as QtGui -except ImportError: - from PyQt4 import QtGui - -from zipfile import ZipFile - -# calibre modules and constants. -from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url, - choose_dir, choose_files, choose_save_file) -from calibre.utils.config import dynamic, config_dir, JSONConfig -from calibre.constants import iswindows, isosx - -# modules from this plugin's zipfile. -from calibre_plugins.dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION -from calibre_plugins.dedrm.__init__ import RESOURCE_NAME as help_file_name -from calibre_plugins.dedrm.utilities import uStrCmp - -import calibre_plugins.dedrm.prefs as prefs -import calibre_plugins.dedrm.androidkindlekey as androidkindlekey - -class ConfigWidget(QWidget): - def __init__(self, plugin_path, alfdir): - QWidget.__init__(self) - - self.plugin_path = plugin_path - self.alfdir = alfdir - - # get the prefs - self.dedrmprefs = prefs.DeDRM_Prefs() - - # make a local copy - self.tempdedrmprefs = {} - self.tempdedrmprefs['bandnkeys'] = self.dedrmprefs['bandnkeys'].copy() - self.tempdedrmprefs['adeptkeys'] = self.dedrmprefs['adeptkeys'].copy() - self.tempdedrmprefs['ereaderkeys'] = self.dedrmprefs['ereaderkeys'].copy() - self.tempdedrmprefs['kindlekeys'] = self.dedrmprefs['kindlekeys'].copy() - self.tempdedrmprefs['androidkeys'] = self.dedrmprefs['androidkeys'].copy() - self.tempdedrmprefs['pids'] = list(self.dedrmprefs['pids']) - self.tempdedrmprefs['serials'] = list(self.dedrmprefs['serials']) - self.tempdedrmprefs['adobewineprefix'] = self.dedrmprefs['adobewineprefix'] - self.tempdedrmprefs['kindlewineprefix'] = self.dedrmprefs['kindlewineprefix'] - - # 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. - help_label = QLabel('Plugin Help', self) - help_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard) - help_label.setAlignment(Qt.AlignRight) - help_label.linkActivated.connect(self.help_link_activated) - help_layout.addWidget(help_label) - - keys_group_box = QGroupBox(_('Configuration:'), self) - layout.addWidget(keys_group_box) - keys_group_box_layout = QHBoxLayout() - keys_group_box.setLayout(keys_group_box_layout) - - - button_layout = QVBoxLayout() - keys_group_box_layout.addLayout(button_layout) - self.bandn_button = QtGui.QPushButton(self) - self.bandn_button.setToolTip(_(u"Click to manage keys for Barnes and Noble ebooks")) - self.bandn_button.setText(u"Barnes and Noble ebooks") - self.bandn_button.clicked.connect(self.bandn_keys) - self.kindle_android_button = QtGui.QPushButton(self) - self.kindle_android_button.setToolTip(_(u"Click to manage keys for Kindle for Android ebooks")) - self.kindle_android_button.setText(u"Kindle for Android ebooks") - self.kindle_android_button.clicked.connect(self.kindle_android) - self.kindle_serial_button = QtGui.QPushButton(self) - self.kindle_serial_button.setToolTip(_(u"Click to manage eInk Kindle serial numbers for Kindle ebooks")) - self.kindle_serial_button.setText(u"eInk Kindle ebooks") - self.kindle_serial_button.clicked.connect(self.kindle_serials) - self.kindle_key_button = QtGui.QPushButton(self) - self.kindle_key_button.setToolTip(_(u"Click to manage keys for Kindle for Mac/PC ebooks")) - self.kindle_key_button.setText(u"Kindle for Mac/PC ebooks") - self.kindle_key_button.clicked.connect(self.kindle_keys) - self.adept_button = QtGui.QPushButton(self) - self.adept_button.setToolTip(_(u"Click to manage keys for Adobe Digital Editions ebooks")) - self.adept_button.setText(u"Adobe Digital Editions ebooks") - self.adept_button.clicked.connect(self.adept_keys) - self.mobi_button = QtGui.QPushButton(self) - self.mobi_button.setToolTip(_(u"Click to manage PIDs for Mobipocket ebooks")) - self.mobi_button.setText(u"Mobipocket ebooks") - self.mobi_button.clicked.connect(self.mobi_keys) - self.ereader_button = QtGui.QPushButton(self) - self.ereader_button.setToolTip(_(u"Click to manage keys for eReader ebooks")) - self.ereader_button.setText(u"eReader ebooks") - self.ereader_button.clicked.connect(self.ereader_keys) - button_layout.addWidget(self.kindle_serial_button) - button_layout.addWidget(self.kindle_android_button) - button_layout.addWidget(self.bandn_button) - button_layout.addWidget(self.mobi_button) - button_layout.addWidget(self.ereader_button) - button_layout.addWidget(self.adept_button) - button_layout.addWidget(self.kindle_key_button) - - self.resize(self.sizeHint()) - - def kindle_serials(self): - d = ManageKeysDialog(self,u"EInk Kindle Serial Number",self.tempdedrmprefs['serials'], AddSerialDialog) - d.exec_() - - def kindle_android(self): - d = ManageKeysDialog(self,u"Kindle for Android Key",self.tempdedrmprefs['androidkeys'], AddAndroidDialog, 'k4a') - d.exec_() - - def kindle_keys(self): - if isosx or iswindows: - d = ManageKeysDialog(self,u"Kindle for Mac and PC Key",self.tempdedrmprefs['kindlekeys'], AddKindleDialog, 'k4i') - else: - # linux - d = ManageKeysDialog(self,u"Kindle for Mac and PC Key",self.tempdedrmprefs['kindlekeys'], AddKindleDialog, 'k4i', self.tempdedrmprefs['kindlewineprefix']) - d.exec_() - self.tempdedrmprefs['kindlewineprefix'] = d.getwineprefix() - - def adept_keys(self): - if isosx or iswindows: - d = ManageKeysDialog(self,u"Adobe Digital Editions Key",self.tempdedrmprefs['adeptkeys'], AddAdeptDialog, 'der') - else: - # linux - d = ManageKeysDialog(self,u"Adobe Digital Editions Key",self.tempdedrmprefs['adeptkeys'], AddAdeptDialog, 'der', self.tempdedrmprefs['adobewineprefix']) - d.exec_() - self.tempdedrmprefs['adobewineprefix'] = d.getwineprefix() - - def mobi_keys(self): - d = ManageKeysDialog(self,u"Mobipocket PID",self.tempdedrmprefs['pids'], AddPIDDialog) - d.exec_() - - def bandn_keys(self): - d = ManageKeysDialog(self,u"Barnes and Noble Key",self.tempdedrmprefs['bandnkeys'], AddBandNKeyDialog, 'b64') - d.exec_() - - def ereader_keys(self): - d = ManageKeysDialog(self,u"eReader Key",self.tempdedrmprefs['ereaderkeys'], AddEReaderDialog, 'b63') - d.exec_() - - def help_link_activated(self, url): - def get_help_file_resource(): - # Copy the HTML helpfile to the plugin directory each time the - # link is clicked in case the helpfile is updated in newer plugins. - file_path = os.path.join(config_dir, u"plugins", u"DeDRM", u"help", help_file_name) - with open(file_path,'w') as f: - f.write(self.load_resource(help_file_name)) - return file_path - url = 'file:///' + get_help_file_resource() - open_url(QUrl(url)) - - def save_settings(self): - self.dedrmprefs.set('bandnkeys', self.tempdedrmprefs['bandnkeys']) - self.dedrmprefs.set('adeptkeys', self.tempdedrmprefs['adeptkeys']) - self.dedrmprefs.set('ereaderkeys', self.tempdedrmprefs['ereaderkeys']) - self.dedrmprefs.set('kindlekeys', self.tempdedrmprefs['kindlekeys']) - self.dedrmprefs.set('androidkeys', self.tempdedrmprefs['androidkeys']) - self.dedrmprefs.set('pids', self.tempdedrmprefs['pids']) - self.dedrmprefs.set('serials', self.tempdedrmprefs['serials']) - self.dedrmprefs.set('adobewineprefix', self.tempdedrmprefs['adobewineprefix']) - self.dedrmprefs.set('kindlewineprefix', self.tempdedrmprefs['kindlewineprefix']) - self.dedrmprefs.set('configured', True) - self.dedrmprefs.writeprefs() - - def load_resource(self, name): - with ZipFile(self.plugin_path, 'r') as zf: - if name in zf.namelist(): - return zf.read(name) - return "" - - - -class ManageKeysDialog(QDialog): - def __init__(self, parent, key_type_name, plugin_keys, create_key, keyfile_ext = u"", wineprefix = None): - QDialog.__init__(self,parent) - self.parent = parent - self.key_type_name = key_type_name - self.plugin_keys = plugin_keys - self.create_key = create_key - self.keyfile_ext = keyfile_ext - self.import_key = (keyfile_ext != u"") - self.binary_file = (keyfile_ext == u"der") - self.json_file = (keyfile_ext == u"k4i") - self.android_file = (keyfile_ext == u"k4a") - self.wineprefix = wineprefix - - self.setWindowTitle("{0} {1}: Manage {2}s".format(PLUGIN_NAME, PLUGIN_VERSION, self.key_type_name)) - - # 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. - help_label = QLabel('Help', self) - help_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard) - help_label.setAlignment(Qt.AlignRight) - help_label.linkActivated.connect(self.help_link_activated) - help_layout.addWidget(help_label) - - keys_group_box = QGroupBox(_(u"{0}s".format(self.key_type_name)), 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(u"{0}s that will be used to decrypt ebooks".format(self.key_type_name)) - self.listy.setSelectionMode(QAbstractItemView.SingleSelection) - self.populate_list() - keys_group_box_layout.addWidget(self.listy) - - button_layout = QVBoxLayout() - keys_group_box_layout.addLayout(button_layout) - self._add_key_button = QtGui.QToolButton(self) - self._add_key_button.setIcon(QIcon(I('plus.png'))) - self._add_key_button.setToolTip(u"Create new {0}".format(self.key_type_name)) - 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(_(u"Delete highlighted key")) - self._delete_key_button.setIcon(QIcon(I('list_remove.png'))) - self._delete_key_button.clicked.connect(self.delete_key) - button_layout.addWidget(self._delete_key_button) - - if type(self.plugin_keys) == dict and self.import_key: - self._rename_key_button = QtGui.QToolButton(self) - self._rename_key_button.setToolTip(_(u"Rename highlighted key")) - 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(u"Save highlighted key to a .{0} file".format(self.keyfile_ext)) - self.export_key_button.setIcon(QIcon(I('save.png'))) - self.export_key_button.clicked.connect(self.export_key) - button_layout.addWidget(self.export_key_button) - spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) - button_layout.addItem(spacerItem) - - if self.wineprefix is not None: - layout.addSpacing(5) - wineprefix_layout = QHBoxLayout() - layout.addLayout(wineprefix_layout) - wineprefix_layout.setAlignment(Qt.AlignCenter) - self.wp_label = QLabel(u"WINEPREFIX:") - wineprefix_layout.addWidget(self.wp_label) - self.wp_lineedit = QLineEdit(self) - wineprefix_layout.addWidget(self.wp_lineedit) - self.wp_label.setBuddy(self.wp_lineedit) - self.wp_lineedit.setText(self.wineprefix) - - layout.addSpacing(5) - migrate_layout = QHBoxLayout() - layout.addLayout(migrate_layout) - if self.import_key: - migrate_layout.setAlignment(Qt.AlignJustify) - self.migrate_btn = QPushButton(u"Import Existing Keyfiles", self) - self.migrate_btn.setToolTip(u"Import *.{0} files (created using other tools).".format(self.keyfile_ext)) - self.migrate_btn.clicked.connect(self.migrate_wrapper) - migrate_layout.addWidget(self.migrate_btn) - migrate_layout.addStretch() - self.button_box = QDialogButtonBox(QDialogButtonBox.Close) - self.button_box.rejected.connect(self.close) - migrate_layout.addWidget(self.button_box) - - self.resize(self.sizeHint()) - - def getwineprefix(self): - if self.wineprefix is not None: - return unicode(self.wp_lineedit.text()).strip() - return u"" - - def populate_list(self): - if type(self.plugin_keys) == dict: - for key in self.plugin_keys.keys(): - self.listy.addItem(QListWidgetItem(key)) - else: - for key in self.plugin_keys: - self.listy.addItem(QListWidgetItem(key)) - - def add_key(self): - d = self.create_key(self) - d.exec_() - - if d.result() != d.Accepted: - # New key generation cancelled. - return - new_key_value = d.key_value - if type(self.plugin_keys) == dict: - if new_key_value in self.plugin_keys.values(): - old_key_name = [name for name, value in self.plugin_keys.iteritems() if value == new_key_value][0] - info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name), - u"The new {1} is the same as the existing {1} named {0} and has not been added.".format(old_key_name,self.key_type_name), show=True) - return - self.plugin_keys[d.key_name] = new_key_value - else: - if new_key_value in self.plugin_keys: - info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name), - u"This {0} is already in the list of {0}s has not been added.".format(self.key_type_name), show=True) - return - - self.plugin_keys.append(d.key_value) - self.listy.clear() - self.populate_list() - - def rename_key(self): - if not self.listy.currentItem(): - errmsg = u"No {0} selected to rename. Highlight a keyfile first.".format(self.key_type_name) - r = error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), - _(errmsg), show=True, show_copy_button=False) - return - - d = RenameKeyDialog(self) - d.exec_() - - if d.result() != d.Accepted: - # rename cancelled or moot. - return - keyname = unicode(self.listy.currentItem().text()) - if not question_dialog(self, "{0} {1}: Confirm Rename".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to rename the {2} named {0} to {1}?".format(keyname,d.key_name,self.key_type_name), show_copy_button=False, default_yes=False): - return - self.plugin_keys[d.key_name] = self.plugin_keys[keyname] - del self.plugin_keys[keyname] - - self.listy.clear() - self.populate_list() - - def delete_key(self): - if not self.listy.currentItem(): - return - keyname = unicode(self.listy.currentItem().text()) - if not question_dialog(self, "{0} {1}: Confirm Delete".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to delete the {1} {0}?".format(keyname, self.key_type_name), show_copy_button=False, default_yes=False): - return - if type(self.plugin_keys) == dict: - del self.plugin_keys[keyname] - else: - self.plugin_keys.remove(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 - # link is clicked in case the helpfile is updated in newer plugins. - help_file_name = u"{0}_{1}_Help.htm".format(PLUGIN_NAME, self.key_type_name) - file_path = os.path.join(config_dir, u"plugins", u"DeDRM", u"help", help_file_name) - with open(file_path,'w') as f: - f.write(self.parent.load_resource(help_file_name)) - return file_path - url = 'file:///' + get_help_file_resource() - open_url(QUrl(url)) - - def migrate_files(self): - unique_dlg_name = PLUGIN_NAME + u"import {0} keys".format(self.key_type_name).replace(' ', '_') #takes care of automatically remembering last directory - caption = u"Select {0} files to import".format(self.key_type_name) - filters = [(u"{0} files".format(self.key_type_name), [self.keyfile_ext])] - files = choose_files(self, unique_dlg_name, caption, filters, all_files=False) - counter = 0 - skipped = 0 - if files: - for filename in files: - fpath = os.path.join(config_dir, filename) - filename = os.path.basename(filename) - new_key_name = os.path.splitext(os.path.basename(filename))[0] - with open(fpath,'rb') as keyfile: - new_key_value = keyfile.read() - if self.binary_file: - new_key_value = new_key_value.encode('hex') - elif self.json_file: - new_key_value = json.loads(new_key_value) - elif self.android_file: - # convert to list of the keys in the string - new_key_value = new_key_value.splitlines() - match = False - for key in self.plugin_keys.keys(): - if uStrCmp(new_key_name, key, True): - skipped += 1 - msg = u"A key with the name {0} already exists!\nSkipping key file {1}.\nRename the existing key and import again".format(new_key_name,filename) - inf = info_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), - _(msg), show_copy_button=False, show=True) - match = True - break - if not match: - if new_key_value in self.plugin_keys.values(): - old_key_name = [name for name, value in self.plugin_keys.iteritems() if value == new_key_value][0] - skipped += 1 - info_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), - u"The key in file {0} is the same as the existing key {1} and has been skipped.".format(filename,old_key_name), show_copy_button=False, show=True) - else: - counter += 1 - self.plugin_keys[new_key_name] = new_key_value - - msg = u"" - if counter+skipped > 1: - if counter > 0: - msg += u"Imported {0:d} key {1}. ".format(counter, u"file" if counter == 1 else u"files") - if skipped > 0: - msg += u"Skipped {0:d} key {1}.".format(skipped, u"file" if counter == 1 else u"files") - inf = info_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), - _(msg), show_copy_button=False, show=True) - return counter > 0 - - def migrate_wrapper(self): - if self.migrate_files(): - self.listy.clear() - self.populate_list() - - def export_key(self): - if not self.listy.currentItem(): - errmsg = u"No keyfile selected to export. Highlight a keyfile first." - r = error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), - _(errmsg), show=True, show_copy_button=False) - return - keyname = unicode(self.listy.currentItem().text()) - unique_dlg_name = PLUGIN_NAME + u"export {0} keys".format(self.key_type_name).replace(' ', '_') #takes care of automatically remembering last directory - caption = u"Save {0} File as...".format(self.key_type_name) - filters = [(u"{0} Files".format(self.key_type_name), [u"{0}".format(self.keyfile_ext)])] - defaultname = u"{0}.{1}".format(keyname, self.keyfile_ext) - filename = choose_save_file(self, unique_dlg_name, caption, filters, all_files=False, initial_filename=defaultname) - if filename: - with file(filename, 'wb') as fname: - if self.binary_file: - fname.write(self.plugin_keys[keyname].decode('hex')) - elif self.json_file: - fname.write(json.dumps(self.plugin_keys[keyname])) - elif self.android_file: - for key in self.plugin_keys[keyname]: - fname.write(key) - fname.write("\n") - else: - fname.write(self.plugin_keys[keyname]) - - - - -class RenameKeyDialog(QDialog): - def __init__(self, parent=None,): - print repr(self), repr(parent) - QDialog.__init__(self, parent) - self.parent = parent - self.setWindowTitle("{0} {1}: Rename {0}".format(PLUGIN_NAME, PLUGIN_VERSION, parent.key_type_name)) - layout = QVBoxLayout(self) - self.setLayout(layout) - - data_group_box = QGroupBox('', self) - layout.addWidget(data_group_box) - data_group_box_layout = QVBoxLayout() - data_group_box.setLayout(data_group_box_layout) - - data_group_box_layout.addWidget(QLabel('New Key Name:', self)) - self.key_ledit = QLineEdit(self.parent.listy.currentItem().text(), self) - self.key_ledit.setToolTip(u"Enter a new name for this existing {0}.".format(parent.key_type_name)) - data_group_box_layout.addWidget(self.key_ledit) - - layout.addSpacing(20) - - self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - self.button_box.accepted.connect(self.accept) - self.button_box.rejected.connect(self.reject) - layout.addWidget(self.button_box) - - self.resize(self.sizeHint()) - - def accept(self): - if not unicode(self.key_ledit.text()) or unicode(self.key_ledit.text()).isspace(): - errmsg = u"Key name field cannot be empty!" - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), - _(errmsg), show=True, show_copy_button=False) - if len(self.key_ledit.text()) < 4: - errmsg = u"Key name must be at least 4 characters long!" - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), - _(errmsg), show=True, show_copy_button=False) - if uStrCmp(self.key_ledit.text(), self.parent.listy.currentItem().text()): - # Same exact name ... do nothing. - return QDialog.reject(self) - for k in self.parent.plugin_keys.keys(): - if (uStrCmp(self.key_ledit.text(), k, True) and - not uStrCmp(k, self.parent.listy.currentItem().text(), True)): - errmsg = u"The key name {0} is already being used.".format(self.key_ledit.text()) - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), - _(errmsg), show=True, show_copy_button=False) - QDialog.accept(self) - - @property - def key_name(self): - return unicode(self.key_ledit.text()).strip() - - - - - - - - -class AddBandNKeyDialog(QDialog): - def __init__(self, parent=None,): - QDialog.__init__(self, parent) - self.parent = parent - self.setWindowTitle(u"{0} {1}: Create New Barnes & Noble Key".format(PLUGIN_NAME, PLUGIN_VERSION)) - layout = QVBoxLayout(self) - self.setLayout(layout) - - data_group_box = QGroupBox(u"", self) - 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(u"Unique Key Name:", self)) - self.key_ledit = QLineEdit("", self) - self.key_ledit.setToolTip(_(u"

Enter an identifying name for this new key.

" + - u"

It should be something that will help you remember " + - u"what personal information was used to create it.")) - key_group.addWidget(self.key_ledit) - - name_group = QHBoxLayout() - data_group_box_layout.addLayout(name_group) - name_group.addWidget(QLabel(u"B&N/nook account email address:", self)) - self.name_ledit = QLineEdit(u"", self) - self.name_ledit.setToolTip(_(u"

Enter your email address as it appears in your B&N " + - u"account.

" + - u"

It will only be used to generate this " + - u"key and won\'t be stored anywhere " + - u"in calibre or on your computer.

" + - u"

eg: apprenticeharper@gmail.com

")) - name_group.addWidget(self.name_ledit) - name_disclaimer_label = QLabel(_(u"(Will not be saved in configuration data)"), self) - name_disclaimer_label.setAlignment(Qt.AlignHCenter) - data_group_box_layout.addWidget(name_disclaimer_label) - - ccn_group = QHBoxLayout() - data_group_box_layout.addLayout(ccn_group) - ccn_group.addWidget(QLabel(u"B&N/nook account password:", self)) - self.cc_ledit = QLineEdit(u"", self) - self.cc_ledit.setToolTip(_(u"

Enter the password " + - u"for your B&N account.

" + - u"

The password will only be used to generate this " + - u"key and won\'t be stored anywhere in " + - u"calibre or on your computer.")) - ccn_group.addWidget(self.cc_ledit) - ccn_disclaimer_label = QLabel(_('(Will not be saved in configuration data)'), self) - ccn_disclaimer_label.setAlignment(Qt.AlignHCenter) - data_group_box_layout.addWidget(ccn_disclaimer_label) - layout.addSpacing(10) - - key_group = QHBoxLayout() - data_group_box_layout.addLayout(key_group) - key_group.addWidget(QLabel(u"Retrieved key:", self)) - self.key_display = QLabel(u"", self) - self.key_display.setToolTip(_(u"Click the Retrieve Key button to fetch your B&N encryption key from the B&N servers")) - key_group.addWidget(self.key_display) - self.retrieve_button = QtGui.QPushButton(self) - self.retrieve_button.setToolTip(_(u"Click to retrieve your B&N encryption key from the B&N servers")) - self.retrieve_button.setText(u"Retrieve Key") - self.retrieve_button.clicked.connect(self.retrieve_key) - key_group.addWidget(self.retrieve_button) - layout.addSpacing(10) - - self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - self.button_box.accepted.connect(self.accept) - self.button_box.rejected.connect(self.reject) - layout.addWidget(self.button_box) - - self.resize(self.sizeHint()) - - @property - def key_name(self): - return unicode(self.key_ledit.text()).strip() - - @property - def key_value(self): - return unicode(self.key_display.text()).strip() - - @property - def user_name(self): - return unicode(self.name_ledit.text()).strip().lower().replace(' ','') - - @property - def cc_number(self): - return unicode(self.cc_ledit.text()).strip() - - def retrieve_key(self): - from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key - fetched_key = fetch_bandn_key(self.user_name,self.cc_number) - if fetched_key == "": - errmsg = u"Could not retrieve key. Check username, password and intenet connectivity and try again." - error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - else: - self.key_display.setText(fetched_key) - - def accept(self): - if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace(): - errmsg = u"All fields are required!" - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - if len(self.key_name) < 4: - errmsg = u"Key name must be at least 4 characters long!" - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - if len(self.key_value) == 0: - self.retrieve_key() - if len(self.key_value) == 0: - return - QDialog.accept(self) - -class AddEReaderDialog(QDialog): - def __init__(self, parent=None,): - QDialog.__init__(self, parent) - self.parent = parent - self.setWindowTitle(u"{0} {1}: Create New eReader Key".format(PLUGIN_NAME, PLUGIN_VERSION)) - layout = QVBoxLayout(self) - self.setLayout(layout) - - data_group_box = QGroupBox(u"", self) - 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(u"Unique Key Name:", self)) - self.key_ledit = QLineEdit("", self) - self.key_ledit.setToolTip(u"

Enter an identifying name for this new key.\nIt should be something that will help you remember what personal information was used to create it.") - key_group.addWidget(self.key_ledit) - - name_group = QHBoxLayout() - data_group_box_layout.addLayout(name_group) - name_group.addWidget(QLabel(u"Your Name:", self)) - self.name_ledit = QLineEdit(u"", self) - self.name_ledit.setToolTip(u"Enter the name for this eReader key, usually the name on your credit card.\nIt will only be used to generate this one-time key and won\'t be stored anywhere in calibre or on your computer.\n(ex: Mr Jonathan Q Smith)") - name_group.addWidget(self.name_ledit) - name_disclaimer_label = QLabel(_(u"(Will not be saved in configuration data)"), self) - name_disclaimer_label.setAlignment(Qt.AlignHCenter) - data_group_box_layout.addWidget(name_disclaimer_label) - - ccn_group = QHBoxLayout() - data_group_box_layout.addLayout(ccn_group) - ccn_group.addWidget(QLabel(u"Credit Card#:", self)) - self.cc_ledit = QLineEdit(u"", self) - self.cc_ledit.setToolTip(u"

Enter the last 8 digits of credit card number for this eReader key.\nThey will only be used to generate this one-time key and won\'t be stored anywhere in calibre or on your computer.") - ccn_group.addWidget(self.cc_ledit) - ccn_disclaimer_label = QLabel(_('(Will not be saved in configuration data)'), self) - ccn_disclaimer_label.setAlignment(Qt.AlignHCenter) - data_group_box_layout.addWidget(ccn_disclaimer_label) - layout.addSpacing(10) - - self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - self.button_box.accepted.connect(self.accept) - self.button_box.rejected.connect(self.reject) - layout.addWidget(self.button_box) - - self.resize(self.sizeHint()) - - @property - def key_name(self): - return unicode(self.key_ledit.text()).strip() - - @property - def key_value(self): - from calibre_plugins.dedrm.erdr2pml import getuser_key as generate_ereader_key - return generate_ereader_key(self.user_name,self.cc_number).encode('hex') - - @property - def user_name(self): - return unicode(self.name_ledit.text()).strip().lower().replace(' ','') - - @property - def cc_number(self): - return unicode(self.cc_ledit.text()).strip().replace(' ', '').replace('-','') - - - def accept(self): - if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace(): - errmsg = u"All fields are required!" - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - if not self.cc_number.isdigit(): - errmsg = u"Numbers only in the credit card number field!" - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - if len(self.key_name) < 4: - errmsg = u"Key name must be at least 4 characters long!" - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - QDialog.accept(self) - - -class AddAdeptDialog(QDialog): - def __init__(self, parent=None,): - QDialog.__init__(self, parent) - self.parent = parent - self.setWindowTitle(u"{0} {1}: Getting Default Adobe Digital Editions Key".format(PLUGIN_NAME, PLUGIN_VERSION)) - layout = QVBoxLayout(self) - self.setLayout(layout) - - try: - if iswindows or isosx: - from calibre_plugins.dedrm.adobekey import adeptkeys - - defaultkeys = adeptkeys() - else: # linux - from wineutils import WineGetKeys - - scriptpath = os.path.join(parent.parent.alfdir,u"adobekey.py") - defaultkeys = WineGetKeys(scriptpath, u".der",parent.getwineprefix()) - - self.default_key = defaultkeys[0] - except: - traceback.print_exc() - self.default_key = u"" - - self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - - if len(self.default_key)>0: - data_group_box = QGroupBox(u"", self) - 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(u"Unique Key Name:", self)) - self.key_ledit = QLineEdit(u"default_key", self) - self.key_ledit.setToolTip(u"

Enter an identifying name for the current default Adobe Digital Editions key.") - key_group.addWidget(self.key_ledit) - - self.button_box.accepted.connect(self.accept) - else: - default_key_error = QLabel(u"The default encryption key for Adobe Digital Editions could not be found.", self) - default_key_error.setAlignment(Qt.AlignHCenter) - layout.addWidget(default_key_error) - # if no default, bot buttons do the same - self.button_box.accepted.connect(self.reject) - - self.button_box.rejected.connect(self.reject) - layout.addWidget(self.button_box) - - self.resize(self.sizeHint()) - - @property - def key_name(self): - return unicode(self.key_ledit.text()).strip() - - @property - def key_value(self): - return self.default_key.encode('hex') - - - def accept(self): - if len(self.key_name) == 0 or self.key_name.isspace(): - errmsg = u"All fields are required!" - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - if len(self.key_name) < 4: - errmsg = u"Key name must be at least 4 characters long!" - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - QDialog.accept(self) - - -class AddKindleDialog(QDialog): - def __init__(self, parent=None,): - QDialog.__init__(self, parent) - self.parent = parent - self.setWindowTitle(u"{0} {1}: Getting Default Kindle for Mac/PC Key".format(PLUGIN_NAME, PLUGIN_VERSION)) - layout = QVBoxLayout(self) - self.setLayout(layout) - - try: - if iswindows or isosx: - from calibre_plugins.dedrm.kindlekey import kindlekeys - - defaultkeys = kindlekeys() - else: # linux - from wineutils import WineGetKeys - - scriptpath = os.path.join(parent.parent.alfdir,u"kindlekey.py") - defaultkeys = WineGetKeys(scriptpath, u".k4i",parent.getwineprefix()) - - self.default_key = defaultkeys[0] - except: - traceback.print_exc() - self.default_key = u"" - - self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - - if len(self.default_key)>0: - data_group_box = QGroupBox(u"", self) - 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(u"Unique Key Name:", self)) - self.key_ledit = QLineEdit(u"default_key", self) - self.key_ledit.setToolTip(u"

Enter an identifying name for the current default Kindle for Mac/PC key.") - key_group.addWidget(self.key_ledit) - - self.button_box.accepted.connect(self.accept) - else: - default_key_error = QLabel(u"The default encryption key for Kindle for Mac/PC could not be found.", self) - default_key_error.setAlignment(Qt.AlignHCenter) - layout.addWidget(default_key_error) - - # if no default, both buttons do the same - self.button_box.accepted.connect(self.reject) - - self.button_box.rejected.connect(self.reject) - layout.addWidget(self.button_box) - - self.resize(self.sizeHint()) - - @property - def key_name(self): - return unicode(self.key_ledit.text()).strip() - - @property - def key_value(self): - return self.default_key - - - def accept(self): - if len(self.key_name) == 0 or self.key_name.isspace(): - errmsg = u"All fields are required!" - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - if len(self.key_name) < 4: - errmsg = u"Key name must be at least 4 characters long!" - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - QDialog.accept(self) - - -class AddSerialDialog(QDialog): - def __init__(self, parent=None,): - QDialog.__init__(self, parent) - self.parent = parent - self.setWindowTitle(u"{0} {1}: Add New EInk Kindle Serial Number".format(PLUGIN_NAME, PLUGIN_VERSION)) - layout = QVBoxLayout(self) - self.setLayout(layout) - - data_group_box = QGroupBox(u"", self) - 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(u"EInk Kindle Serial Number:", self)) - self.key_ledit = QLineEdit("", self) - self.key_ledit.setToolTip(u"Enter an eInk Kindle serial number. EInk Kindle serial numbers are 16 characters long and usually start with a 'B' or a '9'. Kindle Serial Numbers are case-sensitive, so be sure to enter the upper and lower case letters unchanged.") - key_group.addWidget(self.key_ledit) - - self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - self.button_box.accepted.connect(self.accept) - self.button_box.rejected.connect(self.reject) - layout.addWidget(self.button_box) - - self.resize(self.sizeHint()) - - @property - def key_name(self): - return unicode(self.key_ledit.text()).strip() - - @property - def key_value(self): - return unicode(self.key_ledit.text()).strip() - - def accept(self): - if len(self.key_name) == 0 or self.key_name.isspace(): - errmsg = u"Please enter an eInk Kindle Serial Number or click Cancel in the dialog." - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - if len(self.key_name) != 16: - errmsg = u"EInk Kindle Serial Numbers must be 16 characters long. This is {0:d} characters long.".format(len(self.key_name)) - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - QDialog.accept(self) - - -class AddAndroidDialog(QDialog): - def __init__(self, parent=None,): - - QDialog.__init__(self, parent) - self.parent = parent - self.setWindowTitle(u"{0} {1}: Add new Kindle for Android Key".format(PLUGIN_NAME, PLUGIN_VERSION)) - layout = QVBoxLayout(self) - self.setLayout(layout) - self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - - data_group_box = QGroupBox(u"", self) - layout.addWidget(data_group_box) - data_group_box_layout = QVBoxLayout() - data_group_box.setLayout(data_group_box_layout) - - file_group = QHBoxLayout() - data_group_box_layout.addLayout(file_group) - add_btn = QPushButton(u"Choose Backup File", self) - add_btn.setToolTip(u"Import Kindle for Android backup file.") - add_btn.clicked.connect(self.get_android_file) - file_group.addWidget(add_btn) - self.selected_file_name = QLabel(u"",self) - self.selected_file_name.setAlignment(Qt.AlignHCenter) - file_group.addWidget(self.selected_file_name) - - key_group = QHBoxLayout() - data_group_box_layout.addLayout(key_group) - key_group.addWidget(QLabel(u"Unique Key Name:", self)) - self.key_ledit = QLineEdit(u"", self) - self.key_ledit.setToolTip(u"

Enter an identifying name for the Android for Kindle key.") - key_group.addWidget(self.key_ledit) - #key_label = QLabel(_(''), self) - #key_label.setAlignment(Qt.AlignHCenter) - #data_group_box_layout.addWidget(key_label) - - self.button_box.accepted.connect(self.accept) - self.button_box.rejected.connect(self.reject) - layout.addWidget(self.button_box) - self.resize(self.sizeHint()) - - @property - def key_name(self): - return unicode(self.key_ledit.text()).strip() - - @property - def file_name(self): - return unicode(self.selected_file_name.text()).strip() - - @property - def key_value(self): - return self.serials_from_file - - def get_android_file(self): - unique_dlg_name = PLUGIN_NAME + u"Import Kindle for Android backup file" #takes care of automatically remembering last directory - caption = u"Select Kindle for Android backup file to add" - filters = [(u"Kindle for Android backup files", ['db','ab','xml'])] - files = choose_files(self, unique_dlg_name, caption, filters, all_files=False) - self.serials_from_file = [] - file_name = u"" - if files: - # find the first selected file that yields some serial numbers - for filename in files: - fpath = os.path.join(config_dir, filename) - self.filename = os.path.basename(filename) - file_serials = androidkindlekey.get_serials(fpath) - if len(file_serials)>0: - file_name = os.path.basename(self.filename) - self.serials_from_file.extend(file_serials) - self.selected_file_name.setText(file_name) - - - def accept(self): - if len(self.file_name) == 0 or len(self.key_value) == 0: - errmsg = u"Please choose a Kindle for Android backup file." - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - if len(self.key_name) == 0 or self.key_name.isspace(): - errmsg = u"Please enter a key name." - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - if len(self.key_name) < 4: - errmsg = u"Key name must be at least 4 characters long!" - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - QDialog.accept(self) - -class AddPIDDialog(QDialog): - def __init__(self, parent=None,): - QDialog.__init__(self, parent) - self.parent = parent - self.setWindowTitle(u"{0} {1}: Add New Mobipocket PID".format(PLUGIN_NAME, PLUGIN_VERSION)) - layout = QVBoxLayout(self) - self.setLayout(layout) - - data_group_box = QGroupBox(u"", self) - 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(u"PID:", self)) - self.key_ledit = QLineEdit("", self) - self.key_ledit.setToolTip(u"Enter a Mobipocket PID. Mobipocket PIDs are 8 or 10 characters long. Mobipocket PIDs are case-sensitive, so be sure to enter the upper and lower case letters unchanged.") - key_group.addWidget(self.key_ledit) - - self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - self.button_box.accepted.connect(self.accept) - self.button_box.rejected.connect(self.reject) - layout.addWidget(self.button_box) - - self.resize(self.sizeHint()) - - @property - def key_name(self): - return unicode(self.key_ledit.text()).strip() - - @property - def key_value(self): - return unicode(self.key_ledit.text()).strip() - - def accept(self): - if len(self.key_name) == 0 or self.key_name.isspace(): - errmsg = u"Please enter a Mobipocket PID or click Cancel in the dialog." - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - if len(self.key_name) != 8 and len(self.key_name) != 10: - errmsg = u"Mobipocket PIDs must be 8 or 10 characters long. This is {0:d} characters long.".format(len(self.key_name)) - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - QDialog.accept(self) - - diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/convert2xml.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/convert2xml.py deleted file mode 100644 index 8c2d0f3..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/convert2xml.py +++ /dev/null @@ -1,884 +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.first_unknown = True - 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.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), - '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.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), - '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), - 'img.gridSize' : (1, 'scalar_number', 0, 0), - 'img.gridBottomCenter' : (1, 'scalar_number', 0, 0), - 'img.gridTopCenter' : (1, 'scalar_number', 0, 0), - 'img.gridBeginCenter' : (1, 'scalar_number', 0, 0), - 'img.gridEndCenter' : (1, 'scalar_number', 0, 0), - 'img.image_type' : (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.class' : (1, 'scalar_text', 0, 0), - 'word_semantic.firstWord' : (1, 'scalar_number', 0, 0), - 'word_semantic.lastWord' : (1, 'scalar_number', 0, 0), - 'word_semantic.gridBottomCenter' : (1, 'scalar_number', 0, 0), - 'word_semantic.gridTopCenter' : (1, 'scalar_number', 0, 0), - 'word_semantic.gridBeginCenter' : (1, 'scalar_number', 0, 0), - 'word_semantic.gridEndCenter' : (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.class' : (1, 'scalar_text', 0, 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.class' : (1, 'scalar_text', 0, 0), - 'extratokens.type' : (1, 'scalar_text', 0, 0), - 'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0), - 'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0), - 'extratokens.gridSize' : (1, 'scalar_number', 0, 0), - 'extratokens.gridBottomCenter' : (1, 'scalar_number', 0, 0), - 'extratokens.gridTopCenter' : (1, 'scalar_number', 0, 0), - 'extratokens.gridBeginCenter' : (1, 'scalar_number', 0, 0), - 'extratokens.gridEndCenter' : (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._after_type' : (1, 'scalar_text', 0, 0), - 'style._parent_type' : (1, 'scalar_text', 0, 0), - 'style._after_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), - - 'median_d' : (1, 'number', 0, 0), - 'median_h' : (1, 'number', 0, 0), - 'median_firsty' : (1, 'number', 0, 0), - 'median_lasty' : (1, 'number', 0, 0), - - 'num_footers_maybe' : (1, 'number', 0, 0), - 'num_footers_yes' : (1, 'number', 0, 0), - 'num_headers_maybe' : (1, 'number', 0, 0), - 'num_headers_yes' : (1, 'number', 0, 0), - - 'tracking' : (1, 'number', 0, 0), - 'src' : (1, 'text', 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 or self.first_unknown): - print 'Unknown Token:', token - self.first_unknown = False - 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/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/encodebase64.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/encodebase64.py deleted file mode 100644 index 6bb8c37..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/encodebase64.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/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 deleted file mode 100644 index 11f1427..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/epubtest.py +++ /dev/null @@ -1,208 +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 drmcheck -# 1.00 - Initial version, with code from various other scripts -# 1.01 - Moved authorship announcement to usage section. -# -# Changelog epubtest -# 1.00 - Cut to epubtest.py, testing ePub files only by Apprentice Alf -# 1.01 - Added routine for use by Windows DeDRM -# -# 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.01' - -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) - -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 '?'. So use shell32.GetCommandLineArgvW to get sys.argv - # as a list of Unicode strings and encode them as utf-8 - - from ctypes import POINTER, byref, cdll, c_int, windll - from ctypes.wintypes import LPCWSTR, LPWSTR - - GetCommandLineW = cdll.kernel32.GetCommandLineW - GetCommandLineW.argtypes = [] - GetCommandLineW.restype = LPCWSTR - - CommandLineToArgvW = windll.shell32.CommandLineToArgvW - CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] - CommandLineToArgvW.restype = POINTER(LPWSTR) - - cmd = GetCommandLineW() - argc = c_int(0) - argv = CommandLineToArgvW(cmd, byref(argc)) - if argc.value > 0: - # Remove Python executable and commands if present - start = argc.value - len(sys.argv) - return [argv[i] 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"epubtest.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] - -_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 iswindows: - # first try with pycrypto - if inCalibre: - from calibre_plugins.dedrm import pycrypto_des - else: - import pycrypto_des - Des = pycrypto_des.load_pycrypto() - if Des == None: - # they try with openssl - if inCalibre: - from calibre_plugins.dedrm import openssl_des - else: - import openssl_des - Des = openssl_des.load_libcrypto() -else: - # first try with openssl - if inCalibre: - from calibre_plugins.dedrm import openssl_des - else: - import openssl_des - Des = openssl_des.load_libcrypto() - if Des == None: - # then try with pycrypto - if inCalibre: - from calibre_plugins.dedrm import pycrypto_des - else: - import pycrypto_des - Des = pycrypto_des.load_pycrypto() - -# if that did not work then use pure python implementation -# of DES and try to speed it up with Psycho -if Des == None: - if inCalibre: - from calibre_plugins.dedrm import python_des - else: - import python_des - Des = python_des.Des - # Import Psyco if available - try: - # http://psyco.sourceforge.net - import psyco - psyco.full() - except ImportError: - pass - -try: - from hashlib import sha1 -except ImportError: - # older Python release - import sha - sha1 = lambda s: sha.new(s) - -import cgi -import logging - -logging.basicConfig() -#logging.basicConfig(level=logging.DEBUG) - - -class Sectionizer(object): - bkType = "Book" - - 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]) - # Dictionary or normal content (TODO: Not hard-coded) - if self.header[0x3C:0x3C+8] != ident: - if self.header[0x3C:0x3C+8] == "PDctPPrs": - self.bkType = "Dict" - else: - 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] - -# 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]) - -def deXOR(text, sp, table): - r='' - j = sp - for i in xrange(len(text)): - r += chr(ord(table[j]) ^ ord(text[i])) - j = j + 1 - if j == len(table): - j = 0 - return r - -class EreaderProcessor(object): - def __init__(self, sect, user_key): - self.section_reader = sect.loadSection - data = self.section_reader(0) - version, = struct.unpack('>H', data[0:2]) - self.version = version - logging.info('eReader file format version %s', version) - if version != 272 and version != 260 and version != 259: - raise ValueError('incorrect eReader version %d (error 1)' % version) - data = self.section_reader(1) - self.data = data - des = Des(fixKey(data[0:8])) - cookie_shuf, cookie_size = struct.unpack('>LL', des.decrypt(data[-8:])) - if cookie_shuf < 3 or cookie_shuf > 0x14 or cookie_size < 0xf0 or cookie_size > 0x200: - raise ValueError('incorrect eReader version (error 2)') - input = des.decrypt(data[-cookie_size:]) - def unshuff(data, shuf): - r = [''] * len(data) - j = 0 - for i in xrange(len(data)): - j = (j + shuf) % len(data) - r[j] = data[i] - assert len("".join(r)) == len(data) - return "".join(r) - r = unshuff(input[0:-8], cookie_shuf) - - 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] - self.first_image_page = struct.unpack('>H', r[24:24+2])[0] - # Default values - self.num_footnote_pages = 0 - self.num_sidebar_pages = 0 - self.first_footnote_page = -1 - self.first_sidebar_page = -1 - if self.version == 272: - self.num_footnote_pages = struct.unpack('>H', r[46:46+2])[0] - self.first_footnote_page = struct.unpack('>H', r[44:44+2])[0] - if (sect.bkType == "Book"): - self.num_sidebar_pages = struct.unpack('>H', r[38:38+2])[0] - self.first_sidebar_page = struct.unpack('>H', r[36:36+2])[0] - # self.num_bookinfo_pages = struct.unpack('>H', r[34:34+2])[0] - # self.first_bookinfo_page = struct.unpack('>H', r[32:32+2])[0] - # self.num_chapter_pages = struct.unpack('>H', r[22:22+2])[0] - # self.first_chapter_page = struct.unpack('>H', r[20:20+2])[0] - # self.num_link_pages = struct.unpack('>H', r[30:30+2])[0] - # self.first_link_page = struct.unpack('>H', r[28:28+2])[0] - # self.num_xtextsize_pages = struct.unpack('>H', r[54:54+2])[0] - # self.first_xtextsize_page = struct.unpack('>H', r[52:52+2])[0] - - # **before** data record 1 was decrypted and unshuffled, it contained data - # to create an XOR table and which is used to fix footnote record 0, link records, chapter records, etc - self.xortable_offset = struct.unpack('>H', r[40:40+2])[0] - self.xortable_size = struct.unpack('>H', r[42:42+2])[0] - self.xortable = self.data[self.xortable_offset:self.xortable_offset + self.xortable_size] - else: - # Nothing needs to be done - pass - # self.num_bookinfo_pages = 0 - # self.num_chapter_pages = 0 - # self.num_link_pages = 0 - # self.num_xtextsize_pages = 0 - # self.first_bookinfo_page = -1 - # self.first_chapter_page = -1 - # self.first_link_page = -1 - # self.first_xtextsize_page = -1 - - logging.debug('self.num_text_pages %d', self.num_text_pages) - logging.debug('self.num_footnote_pages %d, self.first_footnote_page %d', self.num_footnote_pages , self.first_footnote_page) - logging.debug('self.num_sidebar_pages %d, self.first_sidebar_page %d', self.num_sidebar_pages , self.first_sidebar_page) - self.flags = struct.unpack('>L', r[4:8])[0] - reqd_flags = (1<<9) | (1<<7) | (1<<10) - if (self.flags & reqd_flags) != reqd_flags: - print "Flags: 0x%X" % self.flags - raise ValueError('incompatible eReader file') - des = Des(fixKey(user_key)) - if version == 259: - if drm_sub_version != 7: - raise ValueError('incorrect eReader version %d (error 3)' % drm_sub_version) - encrypted_key_sha = r[44:44+20] - encrypted_key = r[64:64+8] - elif version == 260: - if drm_sub_version != 13 and drm_sub_version != 11: - raise ValueError('incorrect eReader version %d (error 3)' % drm_sub_version) - if drm_sub_version == 13: - encrypted_key = r[44:44+8] - encrypted_key_sha = r[52:52+20] - else: - encrypted_key = r[64:64+8] - encrypted_key_sha = r[44:44+20] - elif version == 272: - encrypted_key = r[172:172+8] - encrypted_key_sha = r[56:56+20] - self.content_key = des.decrypt(encrypted_key) - if sha1(self.content_key).digest() != encrypted_key_sha: - raise ValueError('Incorrect Name and/or Credit Card') - - def getNumImages(self): - return self.num_image_pages - - def getImage(self, i): - sect = self.section_reader(self.first_image_page + i) - name = sect[4:4+32].strip('\0') - data = sect[62:] - return sanitizeFileName(unicode(name,'windows-1252')), data - - - # def getChapterNamePMLOffsetData(self): - # cv = '' - # if self.num_chapter_pages > 0: - # for i in xrange(self.num_chapter_pages): - # chaps = self.section_reader(self.first_chapter_page + i) - # j = i % self.xortable_size - # 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) - # return cv - - # def getLinkNamePMLOffsetData(self): - # lv = '' - # if self.num_link_pages > 0: - # for i in xrange(self.num_link_pages): - # links = self.section_reader(self.first_link_page + i) - # j = i % self.xortable_size - # 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) - # return lv - - # def getExpandedTextSizesData(self): - # ts = '' - # if self.num_xtextsize_pages > 0: - # tsize = deXOR(self.section_reader(self.first_xtextsize_page), 0, self.xortable) - # for i in xrange(self.num_text_pages): - # xsize = struct.unpack('>H', tsize[0:2])[0] - # ts += "%d\n" % xsize - # tsize = tsize[2:] - # return ts - - # def getBookInfo(self): - # bkinfo = '' - # if self.num_bookinfo_pages > 0: - # info = self.section_reader(self.first_bookinfo_page) - # bkinfo = deXOR(info, 0, self.xortable) - # bkinfo = bkinfo.replace('\0','|') - # bkinfo += '\n' - # return bkinfo - - def getText(self): - des = Des(fixKey(self.content_key)) - r = '' - 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' - # the record 0 of the footnote section must pass through the Xor Table to make it useful - sect = self.section_reader(self.first_footnote_page) - fnote_ids = deXOR(sect, 0, self.xortable) - # the remaining records of the footnote sections need to be decoded with the content_key and zlib inflated - des = Des(fixKey(self.content_key)) - for i in xrange(1,self.num_footnote_pages): - logging.debug('get footnotepage %d', i) - id_len = ord(fnote_ids[2]) - id = fnote_ids[3:3+id_len] - fmarker = '\n' % id - fmarker += zlib.decompress(des.decrypt(self.section_reader(self.first_footnote_page + i))) - fmarker += '\n\n' - r += fmarker - fnote_ids = fnote_ids[id_len+4:] - - # TODO: Handle dictionary index (?) pages - which are also marked as - # sidebar_pages (?). For now dictionary sidebars are ignored - # For dictionaries - record 0 is null terminated strings, followed by - # blocks of around 62000 bytes and a final block. Not sure of the - # encoding - - # now handle sidebar pages - if self.num_sidebar_pages > 0: - r += '\n' - # the record 0 of the sidebar section must pass through the Xor Table to make it useful - sect = self.section_reader(self.first_sidebar_page) - sbar_ids = deXOR(sect, 0, self.xortable) - # the remaining records of the sidebar sections need to be decoded with the content_key and zlib inflated - des = Des(fixKey(self.content_key)) - for i in xrange(1,self.num_sidebar_pages): - id_len = ord(sbar_ids[2]) - id = sbar_ids[3:3+id_len] - smarker = '\n' % id - smarker += zlib.decompress(des.decrypt(self.section_reader(self.first_sidebar_page + i))) - smarker += '\n\n' - r += smarker - sbar_ids = sbar_ids[id_len+4:] - - 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 - -def decryptBook(infile, outpath, make_pmlz, user_key): - bookname = os.path.splitext(os.path.basename(infile))[0] - 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: - 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 u"Creating PMLZ file {0}".format(os.path.basename(pmlzname)) - myZipFile = zipfile.ZipFile(pmlzname,'w',zipfile.ZIP_STORED, False) - list = os.listdir(outdir) - 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): - 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() - # remove temporary directory - shutil.rmtree(outdir, True) - print u"Output is {0}".format(pmlzname) - else : - print u"Output is in {0}".format(outdir) - print "done" - except ValueError, e: - print u"Error: {0}".format(e) - traceback.print_exc() - return 1 - return 0 - - -def usage(): - 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(): - print u"eRdr2Pml v{0}. Copyright © 2009–2012 The Dark Reverser et al.".format(__version__) - - argv=unicode_argv() - try: - opts, args = getopt.getopt(argv[1:], "hp", ["make-pmlz"]) - except getopt.GetoptError, err: - print err.args[0] - usage() - return 1 - make_pmlz = False - for o, a in opts: - if o == "-h": - usage() - return 0 - elif o == "-p": - make_pmlz = True - elif o == "--make-pmlz": - make_pmlz = True - - if len(args)!=3 and len(args)!=4: - usage() - return 1 - - if len(args)==3: - 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, outpath, name, cc = args - - print getuser_key(name,cc).encode('hex') - - return decryptBook(infile, outpath, make_pmlz, getuser_key(name,cc)) - - -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/flatxml2html.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/flatxml2html.py deleted file mode 100644 index 991591b..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/flatxml2html.py +++ /dev/null @@ -1,801 +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'): - # we only handle spaceafter word class - try: - (cname, space) = argres.split('-',1) - if space == '' : space = '0' - if (cname == 'spaceafter') and (int(space) > 0) : - word_class = 'sa' - except: - pass - - elif name.endswith('word.img.src'): - result.append(('img' + word_class, int(argres))) - 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' : - try: - word = self.ocrtext[num] - except: - word = "" - - 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/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/flatxml2svg.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/flatxml2svg.py deleted file mode 100644 index 4dfd6c7..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/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/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/genbook.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/genbook.py deleted file mode 100644 index 4036896..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/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.dedrm import convert2xml - from calibre_plugins.dedrm import flatxml2html - from calibre_plugins.dedrm import flatxml2svg - from calibre_plugins.dedrm 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/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignobleepub.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignobleepub.py deleted file mode 100644 index 1dda116..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignobleepub.py +++ /dev/null @@ -1,455 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# ignobleepub.pyw, version 4.1 -# Copyright © 2009-2010 by i♥cabbages - -# Released under the terms of the GNU General Public Licence, version 3 -# - -# Modified 2010–2013 by some_updates, DiapDealer and Apprentice Alf -# Modified 2015–2017 by Apprentice Harper - -# 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 decrypt from encryption.xml -# 3.3 - On Windows try PyCrypto first, OpenSSL next -# 3.4 - Modify interface to allow use with import -# 3.5 - Fix for potential problem with PyCrypto -# 3.6 - Revised to allow use in calibre plugins to eliminate need for duplicate code -# 3.7 - Tweaked to match ineptepub more closely -# 3.8 - Fixed to retain zip file metadata (e.g. file modification date) -# 3.9 - moved unicode_argv call inside main for Windows DeDRM compatibility -# 4.0 - Work if TkInter is missing -# 4.1 - Import tkFileDialog, don't assume something else will import it. - -""" -Decrypt Barnes & Noble encrypted ePub books. -""" - -__license__ = 'GPL v3' -__version__ = "4.1" - -import sys -import os -import traceback -import zlib -import zipfile -from zipfile import ZipInfo, 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 Decryptor(object): - def __init__(self, bookkey, encryption): - enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag) - self._aes = AES(bookkey) - encryption = etree.fromstring(encryption) - self._encrypted = encrypted = set() - expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'), - enc('CipherReference')) - for elem in encryption.findall(expr): - path = elem.get('URI', None) - if path is not None: - path = path.encode('utf-8') - encrypted.add(path) - - def decompress(self, bytes): - dc = zlib.decompressobj(-15) - bytes = dc.decompress(bytes) - ex = dc.decompress('Z') + dc.flush() - if ex: - bytes = bytes + ex - return bytes - - def decrypt(self, path, data): - if path in self._encrypted: - data = self._aes.decrypt(data)[16:] - data = data[:-ord(data[-1])] - data = self.decompress(data) - return data - -# check file to make check whether it's probably an Adobe Adept encrypted ePub -def ignobleBook(inpath): - with closing(ZipFile(open(inpath, 'rb'))) as inf: - namelist = set(inf.namelist()) - if 'META-INF/rights.xml' not in namelist or \ - 'META-INF/encryption.xml' not in namelist: - return False - try: - rights = etree.fromstring(inf.read('META-INF/rights.xml')) - adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) - expr = './/%s' % (adept('encryptedKey'),) - bookkey = ''.join(rights.findtext(expr)) - if len(bookkey) == 64: - return True - except: - # if we couldn't check, assume it is - return True - return False - -def decryptBook(keyb64, inpath, outpath): - if AES is None: - raise IGNOBLEError(u"PyCrypto or OpenSSL must be installed.") - key = keyb64.decode('base64')[:16] - aes = AES(key) - with closing(ZipFile(open(inpath, 'rb'))) as inf: - namelist = set(inf.namelist()) - if 'META-INF/rights.xml' not in namelist or \ - 'META-INF/encryption.xml' not in namelist: - print u"{0:s} is DRM-free.".format(os.path.basename(inpath)) - return 1 - for name in META_NAMES: - namelist.remove(name) - try: - rights = etree.fromstring(inf.read('META-INF/rights.xml')) - adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) - expr = './/%s' % (adept('encryptedKey'),) - bookkey = ''.join(rights.findtext(expr)) - if len(bookkey) != 64: - print u"{0:s} is not a secure Barnes & Noble ePub.".format(os.path.basename(inpath)) - return 1 - bookkey = aes.decrypt(bookkey.decode('base64')) - bookkey = bookkey[:-ord(bookkey[-1])] - encryption = inf.read('META-INF/encryption.xml') - decryptor = Decryptor(bookkey[-16:], encryption) - kwds = dict(compression=ZIP_DEFLATED, allowZip64=False) - with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf: - zi = ZipInfo('mimetype') - zi.compress_type=ZIP_STORED - try: - # if the mimetype is present, get its info, including time-stamp - oldzi = inf.getinfo('mimetype') - # copy across fields to be preserved - zi.date_time = oldzi.date_time - zi.comment = oldzi.comment - zi.extra = oldzi.extra - zi.internal_attr = oldzi.internal_attr - # external attributes are dependent on the create system, so copy both. - zi.external_attr = oldzi.external_attr - zi.create_system = oldzi.create_system - except: - pass - outf.writestr(zi, inf.read('mimetype')) - for path in namelist: - data = inf.read(path) - zi = ZipInfo(path) - zi.compress_type=ZIP_DEFLATED - try: - # get the file info, including time-stamp - oldzi = inf.getinfo(path) - # copy across useful fields - zi.date_time = oldzi.date_time - zi.comment = oldzi.comment - zi.extra = oldzi.extra - zi.internal_attr = oldzi.internal_attr - # external attributes are dependent on the create system, so copy both. - zi.external_attr = oldzi.external_attr - zi.create_system = oldzi.create_system - except: - pass - outf.writestr(zi, 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(): - sys.stdout=SafeUnbuffered(sys.stdout) - sys.stderr=SafeUnbuffered(sys.stderr) - 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) - 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(): - try: - import Tkinter - import Tkconstants - import tkFileDialog - import tkMessageBox - import traceback - except: - return cli_main() - - class DecryptionDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - self.status = Tkinter.Label(self, text=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: - self.status['text'] = u"File successfully decrypted" - else: - self.status['text'] = u"The was an error decrypting the file." - - root = Tkinter.Tk() - root.title(u"Barnes & Noble ePub Decrypter v.{0}".format(__version__)) - 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/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignoblekey.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignoblekey.py deleted file mode 100644 index dbadc5d..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignoblekey.py +++ /dev/null @@ -1,336 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# ignoblekey.py -# Copyright © 2015 Apprentice Alf and Apprentice Harper - -# Based on kindlekey.py, Copyright © 2010-2013 by some_updates and Apprentice Alf - -# Released under the terms of the GNU General Public Licence, version 3 -# - -# Revision history: -# 1.0 - Initial release -# 1.1 - remove duplicates and return last key as single key - -""" -Get Barnes & Noble EPUB user key from nook Studio log file -""" - -__license__ = 'GPL v3' -__version__ = "1.1" - -import sys -import os -import hashlib -import getopt -import re - -# 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 '?'. So use shell32.GetCommandLineArgvW to get sys.argv - # as a list of Unicode strings and encode them as utf-8 - - from ctypes import POINTER, byref, cdll, c_int, windll - from ctypes.wintypes import LPCWSTR, LPWSTR - - GetCommandLineW = cdll.kernel32.GetCommandLineW - GetCommandLineW.argtypes = [] - GetCommandLineW.restype = LPCWSTR - - CommandLineToArgvW = windll.shell32.CommandLineToArgvW - CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] - CommandLineToArgvW.restype = POINTER(LPWSTR) - - cmd = GetCommandLineW() - argc = c_int(0) - argv = CommandLineToArgvW(cmd, byref(argc)) - if argc.value > 0: - # Remove Python executable and commands if present - start = argc.value - len(sys.argv) - return [argv[i] 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"ignoblekey.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 - -# Locate all of the nookStudy/nook for PC/Mac log file and return as list -def getNookLogFiles(): - logFiles = [] - found = False - if iswindows: - import _winreg as winreg - - # some 64 bit machines do not have the proper registry key for some reason - # or the python interface to the 32 vs 64 bit registry is broken - paths = set() - if 'LOCALAPPDATA' in os.environ.keys(): - # Python 2.x does not return unicode env. Use Python 3.x - path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%") - if os.path.isdir(path): - paths.add(path) - if 'USERPROFILE' in os.environ.keys(): - # Python 2.x does not return unicode env. Use Python 3.x - path = winreg.ExpandEnvironmentStrings(u"%USERPROFILE%")+u"\\AppData\\Local" - if os.path.isdir(path): - paths.add(path) - path = winreg.ExpandEnvironmentStrings(u"%USERPROFILE%")+u"\\AppData\\Roaming" - if os.path.isdir(path): - paths.add(path) - # User Shell Folders show take precedent over Shell Folders if present - try: - regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\") - path = winreg.QueryValueEx(regkey, 'Local AppData')[0] - if os.path.isdir(path): - paths.add(path) - except WindowsError: - pass - try: - regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\") - path = winreg.QueryValueEx(regkey, 'AppData')[0] - if os.path.isdir(path): - paths.add(path) - except WindowsError: - pass - try: - regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\") - path = winreg.QueryValueEx(regkey, 'Local AppData')[0] - if os.path.isdir(path): - paths.add(path) - except WindowsError: - pass - try: - regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\") - path = winreg.QueryValueEx(regkey, 'AppData')[0] - if os.path.isdir(path): - paths.add(path) - except WindowsError: - pass - - for path in paths: - # look for nookStudy log file - logpath = path +'\\Barnes & Noble\\NOOKstudy\\logs\\BNClientLog.txt' - if os.path.isfile(logpath): - found = True - print('Found nookStudy log file: ' + logpath.encode('ascii','ignore')) - logFiles.append(logpath) - else: - home = os.getenv('HOME') - # check for BNClientLog.txt in various locations - testpath = home + '/Library/Application Support/Barnes & Noble/DesktopReader/logs/BNClientLog.txt' - if os.path.isfile(testpath): - logFiles.append(testpath) - print('Found nookStudy log file: ' + testpath) - found = True - testpath = home + '/Library/Application Support/Barnes & Noble/DesktopReader/indices/BNClientLog.txt' - if os.path.isfile(testpath): - logFiles.append(testpath) - print('Found nookStudy log file: ' + testpath) - found = True - testpath = home + '/Library/Application Support/Barnes & Noble/BNDesktopReader/logs/BNClientLog.txt' - if os.path.isfile(testpath): - logFiles.append(testpath) - print('Found nookStudy log file: ' + testpath) - found = True - testpath = home + '/Library/Application Support/Barnes & Noble/BNDesktopReader/indices/BNClientLog.txt' - if os.path.isfile(testpath): - logFiles.append(testpath) - print('Found nookStudy log file: ' + testpath) - found = True - - if not found: - print('No nook Study log files have been found.') - return logFiles - - -# Extract CCHash key(s) from log file -def getKeysFromLog(kLogFile): - keys = [] - regex = re.compile("ccHash: \"(.{28})\""); - for line in open(kLogFile): - for m in regex.findall(line): - keys.append(m) - return keys - -# interface for calibre plugin -def nookkeys(files = []): - keys = [] - if files == []: - files = getNookLogFiles() - for file in files: - fileKeys = getKeysFromLog(file) - if fileKeys: - print u"Found {0} keys in the Nook Study log files".format(len(fileKeys)) - keys.extend(fileKeys) - return list(set(keys)) - -# interface for Python DeDRM -# returns single key or multiple keys, depending on path or file passed in -def getkey(outpath, files=[]): - keys = nookkeys(files) - if len(keys) > 0: - if not os.path.isdir(outpath): - outfile = outpath - with file(outfile, 'w') as keyfileout: - keyfileout.write(keys[-1]) - print u"Saved a key to {0}".format(outfile) - else: - keycount = 0 - for key in keys: - while True: - keycount += 1 - outfile = os.path.join(outpath,u"nookkey{0:d}.b64".format(keycount)) - if not os.path.exists(outfile): - break - with file(outfile, 'w') as keyfileout: - keyfileout.write(key) - print u"Saved a key to {0}".format(outfile) - return True - return False - -def usage(progname): - print u"Finds the nook Study encryption keys." - print u"Keys are saved to the current directory, or a specified output directory." - print u"If a file name is passed instead of a directory, only the first key is saved, in that file." - print u"Usage:" - print u" {0:s} [-h] [-k ] []".format(progname) - - -def cli_main(): - sys.stdout=SafeUnbuffered(sys.stdout) - sys.stderr=SafeUnbuffered(sys.stderr) - argv=unicode_argv() - progname = os.path.basename(argv[0]) - print u"{0} v{1}\nCopyright © 2015 Apprentice Alf".format(progname,__version__) - - try: - opts, args = getopt.getopt(argv[1:], "hk:") - except getopt.GetoptError, err: - print u"Error in options or arguments: {0}".format(err.args[0]) - usage(progname) - sys.exit(2) - - files = [] - for o, a in opts: - if o == "-h": - usage(progname) - sys.exit(0) - if o == "-k": - files = [a] - - if len(args) > 1: - usage(progname) - sys.exit(2) - - if len(args) == 1: - # save to the specified file or directory - outpath = args[0] - if not os.path.isabs(outpath): - outpath = os.path.abspath(outpath) - else: - # save to the same directory as the script - outpath = os.path.dirname(argv[0]) - - # make sure the outpath is the - outpath = os.path.realpath(os.path.normpath(outpath)) - - if not getkey(outpath, files): - print u"Could not retrieve nook Study key." - return 0 - - -def gui_main(): - try: - import Tkinter - import Tkconstants - import tkMessageBox - import traceback - except: - return cli_main() - - class ExceptionDialog(Tkinter.Frame): - def __init__(self, root, text): - Tkinter.Frame.__init__(self, root, border=5) - 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) - - - argv=unicode_argv() - root = Tkinter.Tk() - root.withdraw() - progpath, progname = os.path.split(argv[0]) - success = False - try: - keys = nookkeys() - keycount = 0 - for key in keys: - print key - while True: - keycount += 1 - outfile = os.path.join(progpath,u"nookkey{0:d}.b64".format(keycount)) - if not os.path.exists(outfile): - break - - with file(outfile, 'w') as keyfileout: - keyfileout.write(key) - success = True - tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile)) - except DrmException, e: - tkMessageBox.showerror(progname, u"Error: {0}".format(str(e))) - except Exception: - root.wm_state('normal') - root.title(progname) - text = traceback.format_exc() - ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1) - root.mainloop() - if not success: - return 1 - return 0 - -if __name__ == '__main__': - if len(sys.argv) > 1: - sys.exit(cli_main()) - sys.exit(gui_main()) diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignoblekeyfetch.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignoblekeyfetch.py deleted file mode 100644 index e9637a1..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignoblekeyfetch.py +++ /dev/null @@ -1,258 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# ignoblekeyfetch.pyw, version 1.1 -# Copyright © 2015 Apprentice Harper - -# Released under the terms of the GNU General Public Licence, version 3 -# - -# Based on discoveries by "Nobody You Know" -# Code partly based on ignoblekeygen.py by several people. - -# Windows users: Before running this program, you must first install Python. -# We recommend ActiveState Python 2.7.X for Windows from -# http://www.activestate.com/activepython/downloads. -# Then save this script file as ignoblekeyfetch.pyw and double-click on it to run it. -# -# Mac OS X users: Save this script file as ignoblekeyfetch.pyw. You can run this -# program from the command line (python ignoblekeyfetch.pyw) or by double-clicking -# it when it has been associated with PythonLauncher. - -# Revision history: -# 1.0 - Initial version -# 1.1 - Try second URL if first one fails - -""" -Fetch Barnes & Noble EPUB user key from B&N servers using email and password -""" - -__license__ = 'GPL v3' -__version__ = "1.1" - -import sys -import os - -# 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 '?'. So use shell32.GetCommandLineArgvW to get sys.argv - # as a list of Unicode strings and encode them as utf-8 - - from ctypes import POINTER, byref, cdll, c_int, windll - from ctypes.wintypes import LPCWSTR, LPWSTR - - GetCommandLineW = cdll.kernel32.GetCommandLineW - GetCommandLineW.argtypes = [] - GetCommandLineW.restype = LPCWSTR - - CommandLineToArgvW = windll.shell32.CommandLineToArgvW - CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] - CommandLineToArgvW.restype = POINTER(LPWSTR) - - cmd = GetCommandLineW() - argc = c_int(0) - argv = CommandLineToArgvW(cmd, byref(argc)) - if argc.value > 0: - # Remove Python executable and commands if present - start = argc.value - len(sys.argv) - return [argv[i] 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"ignoblekeyfetch.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 fetch_key(email, password): - # change email and password to utf-8 if unicode - if type(email)==unicode: - email = email.encode('utf-8') - if type(password)==unicode: - password = password.encode('utf-8') - - import random - random = "%030x" % random.randrange(16**30) - - import urllib, urllib2, re - - # try the URL from nook for PC - fetch_url = "https://cart4.barnesandnoble.com/services/service.aspx?Version=2&acctPassword=" - fetch_url += urllib.quote(password,'')+"&devID=PC_BN_2.5.6.9575_"+random+"&emailAddress=" - fetch_url += urllib.quote(email,"")+"&outFormat=5&schema=1&service=1&stage=deviceHashB" - #print fetch_url - - found = '' - try: - req = urllib2.Request(fetch_url) - response = urllib2.urlopen(req) - the_page = response.read() - #print the_page - found = re.search('ccHash>(.+?)(.+?) ".format(progname) - return 1 - email, password, keypath = argv[1:] - userkey = fetch_key(email, password) - if len(userkey) == 28: - open(keypath,'wb').write(userkey) - return 0 - print u"Failed to fetch key." - return 1 - - -def gui_main(): - try: - import Tkinter - import tkFileDialog - import Tkconstants - import tkMessageBox - import traceback - except: - return cli_main() - - class DecryptionDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - self.status = Tkinter.Label(self, text=u"Enter parameters") - self.status.pack(fill=Tkconstants.X, expand=1) - body = Tkinter.Frame(self) - body.pack(fill=Tkconstants.X, expand=1) - sticky = Tkconstants.E + Tkconstants.W - body.grid_columnconfigure(1, weight=2) - Tkinter.Label(body, text=u"Account email address").grid(row=0) - self.name = Tkinter.Entry(body, width=40) - self.name.grid(row=0, column=1, sticky=sticky) - Tkinter.Label(body, text=u"Account password").grid(row=1) - self.ccn = Tkinter.Entry(body, width=40) - self.ccn.grid(row=1, column=1, sticky=sticky) - Tkinter.Label(body, text=u"Output file").grid(row=2) - self.keypath = Tkinter.Entry(body, width=40) - self.keypath.grid(row=2, column=1, sticky=sticky) - self.keypath.insert(2, u"bnepubkey.b64") - button = Tkinter.Button(body, text=u"...", command=self.get_keypath) - button.grid(row=2, column=2) - buttons = Tkinter.Frame(self) - buttons.pack() - botton = Tkinter.Button( - buttons, text=u"Fetch", width=10, command=self.generate) - botton.pack(side=Tkconstants.LEFT) - Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) - button = Tkinter.Button( - buttons, text=u"Quit", width=10, command=self.quit) - button.pack(side=Tkconstants.RIGHT) - - def get_keypath(self): - keypath = tkFileDialog.asksaveasfilename( - parent=None, title=u"Select B&N ePub key file to produce", - defaultextension=u".b64", - filetypes=[('base64-encoded files', '.b64'), - ('All Files', '.*')]) - if keypath: - keypath = os.path.normpath(keypath) - self.keypath.delete(0, Tkconstants.END) - self.keypath.insert(0, keypath) - return - - def generate(self): - email = self.name.get() - password = self.ccn.get() - keypath = self.keypath.get() - if not email: - self.status['text'] = u"Email address not given" - return - if not password: - self.status['text'] = u"Account password not given" - return - if not keypath: - self.status['text'] = u"Output keyfile path not set" - return - self.status['text'] = u"Fetching..." - try: - userkey = fetch_key(email, password) - except Exception, e: - self.status['text'] = u"Error: {0}".format(e.args[0]) - return - if len(userkey) == 28: - open(keypath,'wb').write(userkey) - self.status['text'] = u"Keyfile fetched successfully" - else: - self.status['text'] = u"Keyfile fetch failed." - - root = Tkinter.Tk() - root.title(u"Barnes & Noble ePub Keyfile Fetch 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.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 deleted file mode 100644 index d2917c7..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignoblekeygen.py +++ /dev/null @@ -1,332 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# ignoblekeygen.pyw, version 2.5 -# Copyright © 2009-2010 i♥cabbages - -# Released under the terms of the GNU General Public Licence, version 3 -# - -# Modified 2010–2013 by some_updates, DiapDealer and Apprentice Alf - -# Windows users: Before running this program, you must first install Python. -# We recommend ActiveState Python 2.7.X for Windows (x86) from -# http://www.activestate.com/activepython/downloads. -# You must also install PyCrypto from -# http://www.voidspace.org.uk/python/modules.shtml#pycrypto -# (make certain to install the version for Python 2.7). -# Then 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 (python ignoblekeygen.pyw) or by double-clicking -# it when it has been associated with PythonLauncher. - -# Revision history: -# 1 - Initial release -# 2 - Add OS X support by using OpenSSL when available (taken/modified from ineptepub v5) -# 2.1 - Allow Windows versions of libcrypto to be found -# 2.2 - On Windows try PyCrypto first and then OpenSSL next -# 2.3 - Modify interface to allow use of import -# 2.4 - Improvements to UI and now works in plugins -# 2.5 - Additional improvement for unicode and plugin support -# 2.6 - moved unicode_argv call inside main for Windows DeDRM compatibility -# 2.7 - Work if TkInter is missing -# 2.8 - Fix bug in stand-alone use (import tkFileDialog) - -""" -Generate Barnes & Noble EPUB user key from name and credit card number. -""" - -__license__ = 'GPL v3' -__version__ = "2.8" - -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) - -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 '?'. So use shell32.GetCommandLineArgvW to get sys.argv - # as a list of Unicode strings and encode them as utf-8 - - from ctypes import POINTER, byref, cdll, c_int, windll - from ctypes.wintypes import LPCWSTR, LPWSTR - - GetCommandLineW = cdll.kernel32.GetCommandLineW - GetCommandLineW.argtypes = [] - GetCommandLineW.restype = LPCWSTR - - CommandLineToArgvW = windll.shell32.CommandLineToArgvW - CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] - CommandLineToArgvW.restype = POINTER(LPWSTR) - - cmd = GetCommandLineW() - argc = c_int(0) - argv = CommandLineToArgvW(cmd, byref(argc)) - if argc.value > 0: - # Remove Python executable and commands if present - start = argc.value - len(sys.argv) - return [argv[i] for i in - xrange(start, argc.value)] - # if we don't have any arguments at all, just pass back script name - # this should never happen - return [u"ignoblekeygen.py"] - else: - argvencoding = sys.stdin.encoding - if argvencoding == None: - argvencoding = "utf-8" - return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv] - - -class IGNOBLEError(Exception): - pass - -def _load_crypto_libcrypto(): - from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \ - Structure, c_ulong, create_string_buffer, cast - from ctypes.util import find_library - - if iswindows: - libcrypto = find_library('libeay32') - else: - libcrypto = find_library('crypto') - - if libcrypto is None: - raise IGNOBLEError('libcrypto not found') - libcrypto = CDLL(libcrypto) - - AES_MAXNR = 14 - - c_char_pp = POINTER(c_char_p) - c_int_p = POINTER(c_int) - - class AES_KEY(Structure): - _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), - ('rounds', c_int)] - AES_KEY_p = POINTER(AES_KEY) - - def F(restype, name, argtypes): - func = getattr(libcrypto, name) - func.restype = restype - func.argtypes = argtypes - return func - - AES_set_encrypt_key = F(c_int, 'AES_set_encrypt_key', - [c_char_p, c_int, AES_KEY_p]) - AES_cbc_encrypt = F(None, 'AES_cbc_encrypt', - [c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p, - c_int]) - - class AES(object): - def __init__(self, userkey, iv): - self._blocksize = len(userkey) - self._iv = iv - key = self._key = AES_KEY() - rv = AES_set_encrypt_key(userkey, len(userkey) * 8, key) - if rv < 0: - raise IGNOBLEError('Failed to initialize AES Encrypt key') - - def encrypt(self, data): - out = create_string_buffer(len(data)) - rv = AES_cbc_encrypt(data, out, len(data), self._key, self._iv, 1) - if rv == 0: - raise IGNOBLEError('AES encryption failed') - return out.raw - - return AES - -def _load_crypto_pycrypto(): - from Crypto.Cipher import AES as _AES - - class AES(object): - def __init__(self, key, iv): - self._aes = _AES.new(key, _AES.MODE_CBC, iv) - - def encrypt(self, data): - return self._aes.encrypt(data) - - return AES - -def _load_crypto(): - AES = None - cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto) - if sys.platform.startswith('win'): - cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto) - for loader in cryptolist: - try: - AES = loader() - break - except (ImportError, IGNOBLEError): - pass - return AES - -AES = _load_crypto() - -def normalize_name(name): - return ''.join(x for x in name.lower() if x != ' ') - - -def generate_key(name, ccn): - # remove spaces and case from name and CC numbers. - if type(name)==unicode: - name = name.encode('utf-8') - if type(ccn)==unicode: - ccn = ccn.encode('utf-8') - - name = normalize_name(name) + '\x00' - ccn = normalize_name(ccn) + '\x00' - - name_sha = hashlib.sha1(name).digest()[:16] - ccn_sha = hashlib.sha1(ccn).digest()[:16] - both_sha = hashlib.sha1(name + ccn).digest() - aes = AES(ccn_sha, name_sha) - crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c)) - userkey = hashlib.sha1(crypt).digest() - return userkey.encode('base64') - - - - -def cli_main(): - sys.stdout=SafeUnbuffered(sys.stdout) - sys.stderr=SafeUnbuffered(sys.stderr) - argv=unicode_argv() - progname = os.path.basename(argv[0]) - if AES is None: - print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \ - "separately. Read the top-of-script comment for details." % \ - (progname,) - return 1 - if len(argv) != 4: - print u"usage: {0} ".format(progname) - return 1 - name, ccn, keypath = argv[1:] - userkey = generate_key(name, ccn) - open(keypath,'wb').write(userkey) - return 0 - - -def gui_main(): - try: - import Tkinter - import Tkconstants - import tkMessageBox - import tkFileDialog - import traceback - except: - return cli_main() - - class DecryptionDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - self.status = Tkinter.Label(self, text=u"Enter parameters") - self.status.pack(fill=Tkconstants.X, expand=1) - body = Tkinter.Frame(self) - body.pack(fill=Tkconstants.X, expand=1) - sticky = Tkconstants.E + Tkconstants.W - body.grid_columnconfigure(1, weight=2) - Tkinter.Label(body, text=u"Account Name").grid(row=0) - self.name = Tkinter.Entry(body, width=40) - self.name.grid(row=0, column=1, sticky=sticky) - Tkinter.Label(body, text=u"CC#").grid(row=1) - self.ccn = Tkinter.Entry(body, width=40) - self.ccn.grid(row=1, column=1, sticky=sticky) - Tkinter.Label(body, text=u"Output file").grid(row=2) - self.keypath = Tkinter.Entry(body, width=40) - self.keypath.grid(row=2, column=1, sticky=sticky) - self.keypath.insert(2, u"bnepubkey.b64") - button = Tkinter.Button(body, text=u"...", command=self.get_keypath) - button.grid(row=2, column=2) - buttons = Tkinter.Frame(self) - buttons.pack() - botton = Tkinter.Button( - buttons, text=u"Generate", width=10, command=self.generate) - botton.pack(side=Tkconstants.LEFT) - Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) - button = Tkinter.Button( - buttons, text=u"Quit", width=10, command=self.quit) - button.pack(side=Tkconstants.RIGHT) - - def get_keypath(self): - keypath = tkFileDialog.asksaveasfilename( - parent=None, title=u"Select B&N ePub key file to produce", - defaultextension=u".b64", - filetypes=[('base64-encoded files', '.b64'), - ('All Files', '.*')]) - if keypath: - keypath = os.path.normpath(keypath) - self.keypath.delete(0, Tkconstants.END) - self.keypath.insert(0, keypath) - return - - def generate(self): - name = self.name.get() - ccn = self.ccn.get() - keypath = self.keypath.get() - if not name: - self.status['text'] = u"Name not specified" - return - if not ccn: - self.status['text'] = u"Credit card number not specified" - return - if not keypath: - self.status['text'] = u"Output keyfile path not specified" - return - self.status['text'] = u"Generating..." - try: - userkey = generate_key(name, ccn) - except Exception, e: - self.status['text'] = u"Error: (0}".format(e.args[0]) - return - open(keypath,'wb').write(userkey) - self.status['text'] = u"Keyfile successfully generated" - - root = Tkinter.Tk() - if AES is None: - root.withdraw() - tkMessageBox.showerror( - "Ignoble EPUB Keyfile Generator", - "This script requires OpenSSL or PyCrypto, which must be installed " - "separately. Read the top-of-script comment for details.") - return 1 - root.title(u"Barnes & Noble ePub Keyfile Generator v.{0}".format(__version__)) - root.resizable(True, False) - root.minsize(300, 0) - DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1) - root.mainloop() - return 0 - -if __name__ == '__main__': - if len(sys.argv) > 1: - sys.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 deleted file mode 100644 index ae30c04..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ineptepub.py +++ /dev/null @@ -1,601 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# ineptepub.pyw, version 6.6 -# Copyright © 2009-2010 by i♥cabbages - -# Released under the terms of the GNU General Public Licence, version 3 -# - -# Modified 2010–2013 by some_updates, DiapDealer and Apprentice Alf -# Modified 2015–2017 by Apprentice Harper - -# Windows users: Before running this program, you must first install Python 2.7 -# from and PyCrypto from -# (make sure to -# install the version for Python 2.7). 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 - Rename to INEPT, fix exit code -# 5 - Version bump to avoid (?) confusion; -# Improve OS X support by using OpenSSL when available -# 5.1 - Improve OpenSSL error checking -# 5.2 - Fix ctypes error causing segfaults on some systems -# 5.3 - add support for OpenSSL on Windows, fix bug with some versions of libcrypto 0.9.8 prior to path level o -# 5.4 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml -# 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 -# 5.9 - Fixed to retain zip file metadata (e.g. file modification date) -# 6.0 - moved unicode_argv call inside main for Windows DeDRM compatibility -# 6.1 - Work if TkInter is missing -# 6.2 - Handle UTF-8 file names inside an ePub, fix by Jose Luis -# 6.3 - Add additional check on DER file sanity -# 6.4 - Remove erroneous check on DER file sanity -# 6.5 - Completely remove erroneous check on DER file sanity -# 6.6 - Import tkFileDialog, don't assume something else will import it. - -""" -Decrypt Adobe Digital Editions encrypted ePub books. -""" - -__license__ = 'GPL v3' -__version__ = "6.6" - -import sys -import os -import traceback -import zlib -import zipfile -from zipfile import ZipInfo, 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 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 iswindows: - libcrypto = find_library('libeay32') - else: - libcrypto = find_library('crypto') - - if libcrypto is None: - raise ADEPTError('libcrypto not found') - 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 - - 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) - - 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) - -AES, RSA = _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 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.encode('utf-8') 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 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: - 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) - 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') - zi.compress_type=ZIP_STORED - try: - # if the mimetype is present, get its info, including time-stamp - oldzi = inf.getinfo('mimetype') - # copy across fields to be preserved - zi.date_time = oldzi.date_time - zi.comment = oldzi.comment - zi.extra = oldzi.extra - zi.internal_attr = oldzi.internal_attr - # external attributes are dependent on the create system, so copy both. - zi.external_attr = oldzi.external_attr - zi.create_system = oldzi.create_system - except: - pass - outf.writestr(zi, inf.read('mimetype')) - for path in namelist: - data = inf.read(path) - zi = ZipInfo(path) - zi.compress_type=ZIP_DEFLATED - try: - # get the file info, including time-stamp - oldzi = inf.getinfo(path) - # copy across useful fields - zi.date_time = oldzi.date_time - zi.comment = oldzi.comment - zi.extra = oldzi.extra - zi.internal_attr = oldzi.internal_attr - # external attributes are dependent on the create system, so copy both. - zi.external_attr = oldzi.external_attr - zi.create_system = oldzi.create_system - except: - pass - outf.writestr(zi, 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(): - sys.stdout=SafeUnbuffered(sys.stdout) - sys.stderr=SafeUnbuffered(sys.stderr) - 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) - 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(): - try: - import Tkinter - import Tkconstants - import tkFileDialog - import tkMessageBox - import traceback - except: - return cli_main() - - class DecryptionDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - self.status = Tkinter.Label(self, text=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.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() - 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) - root.mainloop() - return 0 - -if __name__ == '__main__': - if len(sys.argv) > 1: - sys.exit(cli_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 deleted file mode 100644 index 0da2993..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ineptpdf.py +++ /dev/null @@ -1,2350 +0,0 @@ -#! /usr/bin/python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# ineptpdf.pyw, version 8.0.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 -# Modified 2015-2017 by Apprentice Harper - -# Windows users: Before running this program, you must first install Python 2.7 -# from and PyCrypto from -# (make sure to -# install the version for Python 2.7). Save this script file as -# ineptpdf.pyw and double-click on it to run it. -# -# Mac OS X users: Save this script file as ineptpdf.pyw. You can run this -# program from the command line (pythonw ineptpdf.pyw) or by double-clicking -# it when it has been associated with PythonLauncher. - -# 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 -# 5 - Automated decryption of a complete directory -# 6.1 - backward compatibility for 1.7.1 and old adeptkey.der -# 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 -# 7.2 - Correct malformed Mac OS resource forks for Stanza (anon2) -# - Support for cross ref streams on output (decreases file size) -# 7.3 - Correct bug in trailer with cross ref stream that caused the error -# "The root object is missing or invalid" in Adobe Reader. (anon2) -# 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) -# 7.5 - allow support for OpenSSL to replace pycrypto on all platforms -# implemented ARC4 interface to OpenSSL -# fixed minor typos -# 7.6 - backported AES and other fixes from version 8.4.48 -# 7.7 - On Windows try PyCrypto first and OpenSSL next -# 7.8 - Modify interface to allow use of import -# 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 -# 7.13 - Fixed erroneous mentions of ineptepub -# 7.14 - moved unicode_argv call inside main for Windows DeDRM compatibility -# 8.0 - Work if TkInter is missing -# 8.0.1 - Broken Metadata fix. -# 8.0.2 - Add additional check on DER file sanity -# 8.0.3 - Remove erroneous check on DER file sanity -# 8.0.4 - Completely remove erroneous check on DER file sanity -# 8.0.5 - Do not process DRM-free documents -# 8.0.6 - Replace use of float by Decimal for greater precision, and import tkFileDialog - - -""" -Decrypts Adobe ADEPT-encrypted PDF files. -""" - -__license__ = 'GPL v3' -__version__ = "8.0.6" - -import sys -import os -import re -import zlib -import struct -import hashlib -from decimal import * -from itertools import chain, islice -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) - -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"ineptpdf.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 - - -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) -ARC4, RSA, AES = _load_crypto() - - -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_decimal, 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_decimal, j+1) - try: - self.add_token(int(self.token)) - except ValueError: - pass - return (self.parse_main, j) - - def parse_decimal(self, s, i): - m = END_NUMBER.search(s, i) - if not m: - self.token += s[i:] - return (self.parse_decimal, len(s)) - j = m.start(0) - self.token += s[i:j] - self.add_token(Decimal(self.token)) - 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, Decimal) 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: - print "Incomplete dictionary construct" - objs.append("") # this isn't necessary. - # temporary fix. is this due to rental books? - # raise PSSyntaxError( - # 'Invalid dictionary construct: %r' % objs) - d = dict((literal_name(k), v) \ - for (k,v) in choplist(2, objs)) - self.push((pos, d)) - except PSTypeError: - if STRICT: raise - else: - self.do_keyword(pos, token) - if self.context: - continue - else: - if direct: - return self.pop(1)[0] - self.flush() - obj = self.results.pop(0) - return obj - - -LITERAL_CRYPT = PSLiteralTable.intern('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 decimal_value(x): - x = resolve1(x) - if not isinstance(x, Decimal): - if STRICT: - raise PDFTypeError('Decimal required: %r' % x) - return 0.0 - return x - -def num_value(x): - x = resolve1(x) - if not (isinstance(x, int) or isinstance(x, Decimal)): - if STRICT: - raise PDFTypeError('Int or Float required: %r' % x) - return 0 - return x - -def str_value(x): - x = resolve1(x) - if not isinstance(x, 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 - raise PDFEncryptionError('Document is not encrypted.') - return - (docid, param) = self.encryption - type = literal_name(param['Filter']) - if type == 'Adobe.APS': - return self.initialize_adobe_ps(password, docid, param) - if type == 'Standard': - return self.initialize_standard(password, docid, param) - if type == 'EBX_HANDLER': - return self.initialize_ebx(password, docid, param) - raise PDFEncryptionError('Unknown filter: param=%r' % param) - - def initialize_adobe_ps(self, password, docid, param): - global KEYFILEPATH - self.decrypt_key = self.genkey_adobe_ps(param) - self.genkey = self.genkey_v4 - self.decipher = self.decrypt_aes - self.ready = True - return - - def genkey_adobe_ps(self, param): - # nice little offline principal keys dictionary - # global static principal key for German Onleihe / Bibliothek Digital - principalkeys = { '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 - 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) - 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, userkey): - global GEN_XREF_STM, gen_xref_stm - gen_xref_stm = GEN_XREF_STM > 1 - self.version = inf.read(8) - inf.seek(0) - self.doc = doc = PDFDocument() - parser = PDFParser(doc, inf) - doc.initialize(userkey) - self.objids = objids = set() - for xref in reversed(doc.xrefs): - trailer = xref.trailer - for objid in xref.objids(): - objids.add(objid) - trailer = dict(trailer) - trailer.pop('Prev', None) - trailer.pop('XRefStm', None) - if 'Encrypt' in trailer: - objids.remove(trailer.pop('Encrypt').objid) - self.trailer = trailer - - def dump(self, outf): - self.outf = outf - self.write(self.version) - self.write('\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)): - if self.last.isalnum(): - self.write(' ') - self.write(str(obj)) - elif isinstance(obj, Decimal): - 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 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, userkey) - #except: - # 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 - try: - serializer.dump(outf) - except Exception, e: - print u"error writing pdf: {0}".format(e.args[0]) - return 2 - return 0 - - -def cli_main(): - sys.stdout=SafeUnbuffered(sys.stdout) - sys.stderr=SafeUnbuffered(sys.stderr) - argv=unicode_argv() - progname = os.path.basename(argv[0]) - if len(argv) != 4: - print u"usage: {0} ".format(progname) - return 1 - keypath, inpath, outpath = argv[1:] - userkey = open(keypath,'rb').read() - result = decryptBook(userkey, inpath, outpath) - if result == 0: - print u"Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath)) - return result - - -def gui_main(): - try: - import Tkinter - import Tkconstants - import tkFileDialog - import tkMessageBox - import traceback - except: - return cli_main() - - class DecryptionDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - self.status = Tkinter.Label(self, text=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() - tkMessageBox.showerror( - "INEPT PDF", - "This script requires OpenSSL or PyCrypto, which must be installed " - "separately. Read the top-of-script comment for details.") - return 1 - 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) - root.mainloop() - return 0 - - -if __name__ == '__main__': - if len(sys.argv) > 1: - sys.exit(cli_main()) - sys.exit(gui_main()) diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ion.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ion.py deleted file mode 100644 index 40433ca..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ion.py +++ /dev/null @@ -1,985 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# Pascal implementation by lulzkabulz. Python translation by apprenticenaomi. DeDRM integration by anon. -# BinaryIon.pas + DrmIon.pas + IonSymbols.pas - -from __future__ import with_statement - -import collections -import hashlib -import hmac -import os -import os.path -import struct - -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO - -from Crypto.Cipher import AES -from Crypto.Util.py3compat import bchr, bord - -try: - # lzma library from calibre 2.35.0 or later - import lzma.lzma1 as calibre_lzma -except ImportError: - calibre_lzma = None - try: - import lzma - except ImportError: - # Need pip backports.lzma on Python <3.3 - try: - from backports import lzma - except ImportError: - # Windows-friendly choice: pylzma wheels - import pylzma as lzma - - -TID_NULL = 0 -TID_BOOLEAN = 1 -TID_POSINT = 2 -TID_NEGINT = 3 -TID_FLOAT = 4 -TID_DECIMAL = 5 -TID_TIMESTAMP = 6 -TID_SYMBOL = 7 -TID_STRING = 8 -TID_CLOB = 9 -TID_BLOB = 0xA -TID_LIST = 0xB -TID_SEXP = 0xC -TID_STRUCT = 0xD -TID_TYPEDECL = 0xE -TID_UNUSED = 0xF - - -SID_UNKNOWN = -1 -SID_ION = 1 -SID_ION_1_0 = 2 -SID_ION_SYMBOL_TABLE = 3 -SID_NAME = 4 -SID_VERSION = 5 -SID_IMPORTS = 6 -SID_SYMBOLS = 7 -SID_MAX_ID = 8 -SID_ION_SHARED_SYMBOL_TABLE = 9 -SID_ION_1_0_MAX = 10 - - -LEN_IS_VAR_LEN = 0xE -LEN_IS_NULL = 0xF - - -VERSION_MARKER = b"\x01\x00\xEA" - - -# asserts must always raise exceptions for proper functioning -def _assert(test, msg="Exception"): - if not test: - raise Exception(msg) - - -class SystemSymbols(object): - ION = '$ion' - ION_1_0 = '$ion_1_0' - ION_SYMBOL_TABLE = '$ion_symbol_table' - NAME = 'name' - VERSION = 'version' - IMPORTS = 'imports' - SYMBOLS = 'symbols' - MAX_ID = 'max_id' - ION_SHARED_SYMBOL_TABLE = '$ion_shared_symbol_table' - - -class IonCatalogItem(object): - name = "" - version = 0 - symnames = [] - - def __init__(self, name, version, symnames): - self.name = name - self.version = version - self.symnames = symnames - - -class SymbolToken(object): - text = "" - sid = 0 - - def __init__(self, text, sid): - if text == "" and sid == 0: - raise ValueError("Symbol token must have Text or SID") - - self.text = text - self.sid = sid - - -class SymbolTable(object): - table = None - - def __init__(self): - self.table = [None] * SID_ION_1_0_MAX - self.table[SID_ION] = SystemSymbols.ION - self.table[SID_ION_1_0] = SystemSymbols.ION_1_0 - self.table[SID_ION_SYMBOL_TABLE] = SystemSymbols.ION_SYMBOL_TABLE - self.table[SID_NAME] = SystemSymbols.NAME - self.table[SID_VERSION] = SystemSymbols.VERSION - self.table[SID_IMPORTS] = SystemSymbols.IMPORTS - self.table[SID_SYMBOLS] = SystemSymbols.SYMBOLS - self.table[SID_MAX_ID] = SystemSymbols.MAX_ID - self.table[SID_ION_SHARED_SYMBOL_TABLE] = SystemSymbols.ION_SHARED_SYMBOL_TABLE - - def findbyid(self, sid): - if sid < 1: - raise ValueError("Invalid symbol id") - - if sid < len(self.table): - return self.table[sid] - else: - return "" - - def import_(self, table, maxid): - for i in range(maxid): - self.table.append(table.symnames[i]) - - def importunknown(self, name, maxid): - for i in range(maxid): - self.table.append("%s#%d" % (name, i + 1)) - - -class ParserState: - Invalid,BeforeField,BeforeTID,BeforeValue,AfterValue,EOF = 1,2,3,4,5,6 - -ContainerRec = collections.namedtuple("ContainerRec", "nextpos, tid, remaining") - - -class BinaryIonParser(object): - eof = False - state = None - localremaining = 0 - needhasnext = False - isinstruct = False - valuetid = 0 - valuefieldid = 0 - parenttid = 0 - valuelen = 0 - valueisnull = False - valueistrue = False - value = None - didimports = False - - def __init__(self, stream): - self.annotations = [] - self.catalog = [] - - self.stream = stream - self.initpos = stream.tell() - self.reset() - self.symbols = SymbolTable() - - def reset(self): - self.state = ParserState.BeforeTID - self.needhasnext = True - self.localremaining = -1 - self.eof = False - self.isinstruct = False - self.containerstack = [] - self.stream.seek(self.initpos) - - def addtocatalog(self, name, version, symbols): - self.catalog.append(IonCatalogItem(name, version, symbols)) - - def hasnext(self): - while self.needhasnext and not self.eof: - self.hasnextraw() - if len(self.containerstack) == 0 and not self.valueisnull: - if self.valuetid == TID_SYMBOL: - if self.value == SID_ION_1_0: - self.needhasnext = True - elif self.valuetid == TID_STRUCT: - for a in self.annotations: - if a == SID_ION_SYMBOL_TABLE: - self.parsesymboltable() - self.needhasnext = True - break - return not self.eof - - def hasnextraw(self): - self.clearvalue() - while self.valuetid == -1 and not self.eof: - self.needhasnext = False - if self.state == ParserState.BeforeField: - _assert(self.valuefieldid == SID_UNKNOWN) - - self.valuefieldid = self.readfieldid() - if self.valuefieldid != SID_UNKNOWN: - self.state = ParserState.BeforeTID - else: - self.eof = True - - elif self.state == ParserState.BeforeTID: - self.state = ParserState.BeforeValue - self.valuetid = self.readtypeid() - if self.valuetid == -1: - self.state = ParserState.EOF - self.eof = True - break - - if self.valuetid == TID_TYPEDECL: - if self.valuelen == 0: - self.checkversionmarker() - else: - self.loadannotations() - - elif self.state == ParserState.BeforeValue: - self.skip(self.valuelen) - self.state = ParserState.AfterValue - - elif self.state == ParserState.AfterValue: - if self.isinstruct: - self.state = ParserState.BeforeField - else: - self.state = ParserState.BeforeTID - - else: - _assert(self.state == ParserState.EOF) - - def next(self): - if self.hasnext(): - self.needhasnext = True - return self.valuetid - else: - return -1 - - def push(self, typeid, nextposition, nextremaining): - self.containerstack.append(ContainerRec(nextpos=nextposition, tid=typeid, remaining=nextremaining)) - - def stepin(self): - _assert(self.valuetid in [TID_STRUCT, TID_LIST, TID_SEXP] and not self.eof, - "valuetid=%s eof=%s" % (self.valuetid, self.eof)) - _assert((not self.valueisnull or self.state == ParserState.AfterValue) and - (self.valueisnull or self.state == ParserState.BeforeValue)) - - nextrem = self.localremaining - if nextrem != -1: - nextrem -= self.valuelen - if nextrem < 0: - nextrem = 0 - self.push(self.parenttid, self.stream.tell() + self.valuelen, nextrem) - - self.isinstruct = (self.valuetid == TID_STRUCT) - if self.isinstruct: - self.state = ParserState.BeforeField - else: - self.state = ParserState.BeforeTID - - self.localremaining = self.valuelen - self.parenttid = self.valuetid - self.clearvalue() - self.needhasnext = True - - def stepout(self): - rec = self.containerstack.pop() - - self.eof = False - self.parenttid = rec.tid - if self.parenttid == TID_STRUCT: - self.isinstruct = True - self.state = ParserState.BeforeField - else: - self.isinstruct = False - self.state = ParserState.BeforeTID - self.needhasnext = True - - self.clearvalue() - curpos = self.stream.tell() - if rec.nextpos > curpos: - self.skip(rec.nextpos - curpos) - else: - _assert(rec.nextpos == curpos) - - self.localremaining = rec.remaining - - def read(self, count=1): - if self.localremaining != -1: - self.localremaining -= count - _assert(self.localremaining >= 0) - - result = self.stream.read(count) - if len(result) == 0: - raise EOFError() - return result - - def readfieldid(self): - if self.localremaining != -1 and self.localremaining < 1: - return -1 - - try: - return self.readvaruint() - except EOFError: - return -1 - - def readtypeid(self): - if self.localremaining != -1: - if self.localremaining < 1: - return -1 - self.localremaining -= 1 - - b = self.stream.read(1) - if len(b) < 1: - return -1 - b = bord(b) - result = b >> 4 - ln = b & 0xF - - if ln == LEN_IS_VAR_LEN: - ln = self.readvaruint() - elif ln == LEN_IS_NULL: - ln = 0 - self.state = ParserState.AfterValue - elif result == TID_NULL: - # Must have LEN_IS_NULL - _assert(False) - elif result == TID_BOOLEAN: - _assert(ln <= 1) - self.valueistrue = (ln == 1) - ln = 0 - self.state = ParserState.AfterValue - elif result == TID_STRUCT: - if ln == 1: - ln = self.readvaruint() - - self.valuelen = ln - return result - - def readvarint(self): - b = bord(self.read()) - negative = ((b & 0x40) != 0) - result = (b & 0x3F) - - i = 0 - while (b & 0x80) == 0 and i < 4: - b = bord(self.read()) - result = (result << 7) | (b & 0x7F) - i += 1 - - _assert(i < 4 or (b & 0x80) != 0, "int overflow") - - if negative: - return -result - return result - - def readvaruint(self): - b = bord(self.read()) - result = (b & 0x7F) - - i = 0 - while (b & 0x80) == 0 and i < 4: - b = bord(self.read()) - result = (result << 7) | (b & 0x7F) - i += 1 - - _assert(i < 4 or (b & 0x80) != 0, "int overflow") - - return result - - def readdecimal(self): - if self.valuelen == 0: - return 0. - - rem = self.localremaining - self.valuelen - self.localremaining = self.valuelen - exponent = self.readvarint() - - _assert(self.localremaining > 0, "Only exponent in ReadDecimal") - _assert(self.localremaining <= 8, "Decimal overflow") - - signed = False - b = [bord(x) for x in self.read(self.localremaining)] - if (b[0] & 0x80) != 0: - b[0] = b[0] & 0x7F - signed = True - - # Convert variably sized network order integer into 64-bit little endian - j = 0 - vb = [0] * 8 - for i in range(len(b), -1, -1): - vb[i] = b[j] - j += 1 - - v = struct.unpack(" 0: - result = result[:-1] - return result - - def ionwalk(self, supert, indent, lst): - while self.hasnext(): - if supert == TID_STRUCT: - L = self.getfieldname() + ":" - else: - L = "" - - t = self.next() - if t in [TID_STRUCT, TID_LIST]: - if L != "": - lst.append(indent + L) - L = self.gettypename() - if L != "": - lst.append(indent + L + "::") - if t == TID_STRUCT: - lst.append(indent + "{") - else: - lst.append(indent + "[") - - self.stepin() - self.ionwalk(t, indent + " ", lst) - self.stepout() - - if t == TID_STRUCT: - lst.append(indent + "}") - else: - lst.append(indent + "]") - - else: - if t == TID_STRING: - L += ('"%s"' % self.stringvalue()) - elif t in [TID_CLOB, TID_BLOB]: - L += ("{%s}" % self.printlob(self.lobvalue())) - elif t == TID_POSINT: - L += str(self.intvalue()) - elif t == TID_SYMBOL: - tn = self.gettypename() - if tn != "": - tn += "::" - L += tn + self.symbolvalue() - elif t == TID_DECIMAL: - L += str(self.decimalvalue()) - else: - L += ("TID %d" % t) - lst.append(indent + L) - - def print_(self, lst): - self.reset() - self.ionwalk(-1, "", lst) - - -SYM_NAMES = [ 'com.amazon.drm.Envelope@1.0', - 'com.amazon.drm.EnvelopeMetadata@1.0', 'size', 'page_size', - 'encryption_key', 'encryption_transformation', - 'encryption_voucher', 'signing_key', 'signing_algorithm', - 'signing_voucher', 'com.amazon.drm.EncryptedPage@1.0', - 'cipher_text', 'cipher_iv', 'com.amazon.drm.Signature@1.0', - 'data', 'com.amazon.drm.EnvelopeIndexTable@1.0', 'length', - 'offset', 'algorithm', 'encoded', 'encryption_algorithm', - 'hashing_algorithm', 'expires', 'format', 'id', - 'lock_parameters', 'strategy', 'com.amazon.drm.Key@1.0', - 'com.amazon.drm.KeySet@1.0', 'com.amazon.drm.PIDv3@1.0', - 'com.amazon.drm.PlainTextPage@1.0', - 'com.amazon.drm.PlainText@1.0', 'com.amazon.drm.PrivateKey@1.0', - 'com.amazon.drm.PublicKey@1.0', 'com.amazon.drm.SecretKey@1.0', - 'com.amazon.drm.Voucher@1.0', 'public_key', 'private_key', - 'com.amazon.drm.KeyPair@1.0', 'com.amazon.drm.ProtectedData@1.0', - 'doctype', 'com.amazon.drm.EnvelopeIndexTableOffset@1.0', - 'enddoc', 'license_type', 'license', 'watermark', 'key', 'value', - 'com.amazon.drm.License@1.0', 'category', 'metadata', - 'categorized_metadata', 'com.amazon.drm.CategorizedMetadata@1.0', - 'com.amazon.drm.VoucherEnvelope@1.0', 'mac', 'voucher', - 'com.amazon.drm.ProtectedData@2.0', - 'com.amazon.drm.Envelope@2.0', - 'com.amazon.drm.EnvelopeMetadata@2.0', - 'com.amazon.drm.EncryptedPage@2.0', - 'com.amazon.drm.PlainText@2.0', 'compression_algorithm', - 'com.amazon.drm.Compressed@1.0', 'priority', 'refines'] - -def addprottable(ion): - ion.addtocatalog("ProtectedData", 1, SYM_NAMES) - - -def pkcs7pad(msg, blocklen): - paddinglen = blocklen - len(msg) % blocklen - padding = bchr(paddinglen) * paddinglen - return msg + padding - - -def pkcs7unpad(msg, blocklen): - _assert(len(msg) % blocklen == 0) - - paddinglen = bord(msg[-1]) - _assert(paddinglen > 0 and paddinglen <= blocklen, "Incorrect padding - Wrong key") - _assert(msg[-paddinglen:] == bchr(paddinglen) * paddinglen, "Incorrect padding - Wrong key") - - return msg[:-paddinglen] - - -class DrmIonVoucher(object): - envelope = None - voucher = None - drmkey = None - license_type = "Unknown" - - encalgorithm = "" - enctransformation = "" - hashalgorithm = "" - - lockparams = None - - ciphertext = b"" - cipheriv = b"" - secretkey = b"" - - def __init__(self, voucherenv, dsn, secret): - self.dsn,self.secret = dsn,secret - - self.lockparams = [] - - self.envelope = BinaryIonParser(voucherenv) - addprottable(self.envelope) - - def decryptvoucher(self): - shared = "PIDv3" + self.encalgorithm + self.enctransformation + self.hashalgorithm - - self.lockparams.sort() - for param in self.lockparams: - if param == "ACCOUNT_SECRET": - shared += param + self.secret - elif param == "CLIENT_ID": - shared += param + self.dsn - else: - _assert(False, "Unknown lock parameter: %s" % param) - - sharedsecret = shared.encode("UTF-8") - - key = hmac.new(sharedsecret, sharedsecret[:5], digestmod=hashlib.sha256).digest() - aes = AES.new(key[:32], AES.MODE_CBC, self.cipheriv[:16]) - b = aes.decrypt(self.ciphertext) - b = pkcs7unpad(b, 16) - - self.drmkey = BinaryIonParser(StringIO(b)) - addprottable(self.drmkey) - - _assert(self.drmkey.hasnext() and self.drmkey.next() == TID_LIST and self.drmkey.gettypename() == "com.amazon.drm.KeySet@1.0", - "Expected KeySet, got %s" % self.drmkey.gettypename()) - - self.drmkey.stepin() - while self.drmkey.hasnext(): - self.drmkey.next() - if self.drmkey.gettypename() != "com.amazon.drm.SecretKey@1.0": - continue - - self.drmkey.stepin() - while self.drmkey.hasnext(): - self.drmkey.next() - if self.drmkey.getfieldname() == "algorithm": - _assert(self.drmkey.stringvalue() == "AES", "Unknown cipher algorithm: %s" % self.drmkey.stringvalue()) - elif self.drmkey.getfieldname() == "format": - _assert(self.drmkey.stringvalue() == "RAW", "Unknown key format: %s" % self.drmkey.stringvalue()) - elif self.drmkey.getfieldname() == "encoded": - self.secretkey = self.drmkey.lobvalue() - - self.drmkey.stepout() - break - - self.drmkey.stepout() - - def parse(self): - self.envelope.reset() - _assert(self.envelope.hasnext(), "Envelope is empty") - _assert(self.envelope.next() == TID_STRUCT and self.envelope.gettypename() == "com.amazon.drm.VoucherEnvelope@1.0", - "Unknown type encountered in envelope, expected VoucherEnvelope") - - self.envelope.stepin() - while self.envelope.hasnext(): - self.envelope.next() - field = self.envelope.getfieldname() - if field == "voucher": - self.voucher = BinaryIonParser(StringIO(self.envelope.lobvalue())) - addprottable(self.voucher) - continue - elif field != "strategy": - continue - - _assert(self.envelope.gettypename() == "com.amazon.drm.PIDv3@1.0", "Unknown strategy: %s" % self.envelope.gettypename()) - - self.envelope.stepin() - while self.envelope.hasnext(): - self.envelope.next() - field = self.envelope.getfieldname() - if field == "encryption_algorithm": - self.encalgorithm = self.envelope.stringvalue() - elif field == "encryption_transformation": - self.enctransformation = self.envelope.stringvalue() - elif field == "hashing_algorithm": - self.hashalgorithm = self.envelope.stringvalue() - elif field == "lock_parameters": - self.envelope.stepin() - while self.envelope.hasnext(): - _assert(self.envelope.next() == TID_STRING, "Expected string list for lock_parameters") - self.lockparams.append(self.envelope.stringvalue()) - self.envelope.stepout() - - self.envelope.stepout() - - self.parsevoucher() - - def parsevoucher(self): - _assert(self.voucher.hasnext(), "Voucher is empty") - _assert(self.voucher.next() == TID_STRUCT and self.voucher.gettypename() == "com.amazon.drm.Voucher@1.0", - "Unknown type, expected Voucher") - - self.voucher.stepin() - while self.voucher.hasnext(): - self.voucher.next() - - if self.voucher.getfieldname() == "cipher_iv": - self.cipheriv = self.voucher.lobvalue() - elif self.voucher.getfieldname() == "cipher_text": - self.ciphertext = self.voucher.lobvalue() - elif self.voucher.getfieldname() == "license": - _assert(self.voucher.gettypename() == "com.amazon.drm.License@1.0", - "Unknown license: %s" % self.voucher.gettypename()) - self.voucher.stepin() - while self.voucher.hasnext(): - self.voucher.next() - if self.voucher.getfieldname() == "license_type": - self.license_type = self.voucher.stringvalue() - self.voucher.stepout() - - def printenvelope(self, lst): - self.envelope.print_(lst) - - def printkey(self, lst): - if self.voucher is None: - self.parse() - if self.drmkey is None: - self.decryptvoucher() - - self.drmkey.print_(lst) - - def printvoucher(self, lst): - if self.voucher is None: - self.parse() - - self.voucher.print_(lst) - - def getlicensetype(self): - return self.license_type - - -class DrmIon(object): - ion = None - voucher = None - vouchername = "" - key = b"" - onvoucherrequired = None - - def __init__(self, ionstream, onvoucherrequired): - self.ion = BinaryIonParser(ionstream) - addprottable(self.ion) - self.onvoucherrequired = onvoucherrequired - - def parse(self, outpages): - self.ion.reset() - - _assert(self.ion.hasnext(), "DRMION envelope is empty") - _assert(self.ion.next() == TID_SYMBOL and self.ion.gettypename() == "doctype", "Expected doctype symbol") - _assert(self.ion.next() == TID_LIST and self.ion.gettypename() in ["com.amazon.drm.Envelope@1.0", "com.amazon.drm.Envelope@2.0"], - "Unknown type encountered in DRMION envelope, expected Envelope, got %s" % self.ion.gettypename()) - - while True: - if self.ion.gettypename() == "enddoc": - break - - self.ion.stepin() - while self.ion.hasnext(): - self.ion.next() - - if self.ion.gettypename() in ["com.amazon.drm.EnvelopeMetadata@1.0", "com.amazon.drm.EnvelopeMetadata@2.0"]: - self.ion.stepin() - while self.ion.hasnext(): - self.ion.next() - if self.ion.getfieldname() != "encryption_voucher": - continue - - if self.vouchername == "": - self.vouchername = self.ion.stringvalue() - self.voucher = self.onvoucherrequired(self.vouchername) - self.key = self.voucher.secretkey - _assert(self.key is not None, "Unable to obtain secret key from voucher") - else: - _assert(self.vouchername == self.ion.stringvalue(), - "Unexpected: Different vouchers required for same file?") - - self.ion.stepout() - - elif self.ion.gettypename() in ["com.amazon.drm.EncryptedPage@1.0", "com.amazon.drm.EncryptedPage@2.0"]: - decompress = False - ct = None - civ = None - self.ion.stepin() - while self.ion.hasnext(): - self.ion.next() - if self.ion.gettypename() == "com.amazon.drm.Compressed@1.0": - decompress = True - if self.ion.getfieldname() == "cipher_text": - ct = self.ion.lobvalue() - elif self.ion.getfieldname() == "cipher_iv": - civ = self.ion.lobvalue() - - if ct is not None and civ is not None: - self.processpage(ct, civ, outpages, decompress) - self.ion.stepout() - - self.ion.stepout() - if not self.ion.hasnext(): - break - self.ion.next() - - def print_(self, lst): - self.ion.print_(lst) - - def processpage(self, ct, civ, outpages, decompress): - aes = AES.new(self.key[:16], AES.MODE_CBC, civ[:16]) - msg = pkcs7unpad(aes.decrypt(ct), 16) - - if not decompress: - outpages.write(msg) - return - - _assert(msg[0] == b"\x00", "LZMA UseFilter not supported") - - if calibre_lzma is not None: - with calibre_lzma.decompress(msg[1:], bufsize=0x1000000) as f: - f.seek(0) - outpages.write(f.read()) - return - - decomp = lzma.LZMADecompressor(format=lzma.FORMAT_ALONE) - while not decomp.eof: - segment = decomp.decompress(msg[1:]) - msg = b"" # Contents were internally buffered after the first call - outpages.write(segment) diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/k4mobidedrm.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/k4mobidedrm.py deleted file mode 100644 index 1ce1f35..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/k4mobidedrm.py +++ /dev/null @@ -1,353 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# k4mobidedrm.py -# Copyright © 2008-2017 by Apprentice Harper et al. - -__license__ = 'GPL v3' -__version__ = '5.5' - -# Engine to remove drm from Kindle and Mobipocket ebooks -# 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, 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. -# 4.9 - Missed some invalid characters in cleanup_name -# 5.0 - Extraction of info from Kindle for PC/Mac moved into kindlekey.py -# - tweaked GetDecryptedBook interface to leave passed parameters unchanged -# 5.1 - moved unicode_argv call inside main for Windows DeDRM compatibility -# 5.2 - Fixed error in command line processing of unicode arguments -# 5.3 - Changed Android support to allow passing of backup .ab files -# 5.4 - Recognise KFX files masquerading as azw, even if we can't decrypt them yet. -# 5.5 - Added GPL v3 licence explicitly. -# 5.x - Invoke KFXZipBook to handle zipped KFX files - -import sys, os, re -import csv -import getopt -import re -import traceback -import time -import htmlentitydefs -import json - -class DrmException(Exception): - pass - -if 'calibre' in sys.modules: - inCalibre = True -else: - inCalibre = False - -if inCalibre: - from calibre_plugins.dedrm import mobidedrm - from calibre_plugins.dedrm import topazextract - from calibre_plugins.dedrm import kgenpids - from calibre_plugins.dedrm import androidkindlekey - from calibre_plugins.dedrm import kfxdedrm -else: - import mobidedrm - import topazextract - import kgenpids - import androidkindlekey - import kfxdedrm - -# 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"\'").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, kDatabases, androidFiles, 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 - magic8 = open(infile,'rb').read(8) - if magic8 == '\xeaDRMION\xee': - raise DrmException(u"The .kfx DRMION file cannot be decrypted by itself. A .kfx-zip archive containing a DRM voucher is required.") - - magic3 = magic8[:3] - if magic3 == 'TPZ': - mobi = False - - if magic8[:4] == 'PK\x03\x04': - mb = kfxdedrm.KFXZipBook(infile) - elif mobi: - mb = mobidedrm.MobiBook(infile) - else: - mb = topazextract.TopazBook(infile) - - bookname = unescape(mb.getBookTitle()) - print u"Decrypting {1} ebook: {0}".format(bookname, mb.getBookType()) - - # copy list of pids - totalpids = list(pids) - # extend list of serials with serials from android databases - for aFile in androidFiles: - serials.extend(androidkindlekey.get_serials(aFile)) - # extend PID list with book-specific PIDs from seriala and kDatabases - md1, md2 = mb.getPIDMetaInfo() - totalpids.extend(kgenpids.getPidList(md1, md2, serials, kDatabases)) - # remove any duplicates - totalpids = list(set(totalpids)) - print u"Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(totalpids)) - #print totalpids - - try: - mb.processBook(totalpids) - except: - mb.cleanup - raise - - print u"Decryption succeeded after {0:.1f} seconds".format(time.time()-starttime) - return mb - - -# kDatabaseFiles is a list of files created by kindlekey -def decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids): - starttime = time.time() - kDatabases = [] - for dbfile in kDatabaseFiles: - kindleDatabase = {} - try: - with open(dbfile, 'r') as keyfilein: - kindleDatabase = json.loads(keyfilein.read()) - kDatabases.append([dbfile,kindleDatabase]) - except Exception, e: - print u"Error getting database from file {0:s}: {1:s}".format(dbfile,e) - traceback.print_exc() - - - - try: - book = GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime) - except Exception, e: - print u"Error decrypting book after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime) - traceback.print_exc() - return 1 - - # Try to infer a reasonable name - orig_fn_root = os.path.splitext(os.path.basename(infile))[0] - if ( - re.match('^B[A-Z0-9]{9}(_EBOK|_EBSP|_sample)?$', orig_fn_root) or - re.match('^{0-9A-F-}{36}$', orig_fn_root) - ): # Kindle for PC / Mac / Android / Fire / iOS - clean_title = cleanup_name(book.getBookTitle()) - outfilename = '{}_{}'.format(orig_fn_root, clean_title) - else: # E Ink Kindle, which already uses a reasonable name - outfilename = orig_fn_root - - # avoid excessively long file names - if len(outfilename)>150: - outfilename = outfilename[:99]+"--"+outfilename[-49:] - - 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() - return 0 - - -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 ] [ -a ] ".format(progname) - -# -# Main -# -def cli_main(): - argv=unicode_argv() - progname = os.path.basename(argv[0]) - print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2017 Apprentice Harper et al.".format(__version__) - - try: - opts, args = getopt.getopt(argv[1:], "k:p:s:a:") - 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] - kDatabaseFiles = [] - androidFiles = [] - serials = [] - pids = [] - - for o, a in opts: - if o == "-k": - if a == None : - raise DrmException("Invalid parameter for -k") - kDatabaseFiles.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(',') - if o == '-a': - if a == None: - raise DrmException("Invalid parameter for -a") - androidFiles.append(a) - - # try with built in Kindle Info files if not on Linux - k4 = not sys.platform.startswith('linux') - - return decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids) - - -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/kfxdedrm.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kfxdedrm.py deleted file mode 100644 index c2b9bb1..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kfxdedrm.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# Engine to remove drm from Kindle KFX ebooks - -import os -import shutil -import zipfile - -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO - -try: - from calibre_plugins.dedrm import ion -except ImportError: - import ion - - -__license__ = 'GPL v3' -__version__ = '1.0' - - -class KFXZipBook: - def __init__(self, infile): - self.infile = infile - self.voucher = None - self.decrypted = {} - - def getPIDMetaInfo(self): - return (None, None) - - def processBook(self, totalpids): - with zipfile.ZipFile(self.infile, 'r') as zf: - for filename in zf.namelist(): - data = zf.read(filename) - if data.startswith('\xeaDRMION\xee'): - if self.voucher is None: - self.decrypt_voucher(totalpids) - print u'Decrypting KFX DRMION: {0}'.format(filename) - outfile = StringIO() - ion.DrmIon(StringIO(data[8:-8]), lambda name: self.voucher).parse(outfile) - self.decrypted[filename] = outfile.getvalue() - - if not self.decrypted: - print(u'The .kfx-zip archive does not contain an encrypted DRMION file') - - def decrypt_voucher(self, totalpids): - with zipfile.ZipFile(self.infile, 'r') as zf: - for info in zf.infolist(): - if info.file_size < 0x10000: - data = zf.read(info.filename) - if data.startswith('\xe0\x01\x00\xea') and 'ProtectedData' in data: - break # found DRM voucher - else: - raise Exception(u'The .kfx-zip archive contains an encrypted DRMION file without a DRM voucher') - - print u'Decrypting KFX DRM voucher: {0}'.format(info.filename) - - for pid in [''] + totalpids: - for dsn_len,secret_len in [(0,0), (16,0), (16,40), (32,40), (40,40)]: - if len(pid) == dsn_len + secret_len: - break # split pid into DSN and account secret - else: - continue - - try: - voucher = ion.DrmIonVoucher(StringIO(data), pid[:dsn_len], pid[dsn_len:]) - voucher.parse() - voucher.decryptvoucher() - break - except: - pass - else: - raise Exception(u'Failed to decrypt KFX DRM voucher with any key') - - print u'KFX DRM voucher successfully decrypted' - - license_type = voucher.getlicensetype() - if license_type != "Purchase": - raise Exception((u'This book is licensed as {0}. ' - 'These tools are intended for use on purchased books.').format(license_type)) - - self.voucher = voucher - - def getBookTitle(self): - return os.path.splitext(os.path.split(self.infile)[1])[0] - - def getBookExtension(self): - return '.kfx-zip' - - def getBookType(self): - return 'KFX-ZIP' - - def cleanup(self): - pass - - def getFile(self, outpath): - if not self.decrypted: - shutil.copyfile(self.infile, outpath) - else: - with zipfile.ZipFile(self.infile, 'r') as zif: - with zipfile.ZipFile(outpath, 'w') as zof: - for info in zif.infolist(): - zof.writestr(info, self.decrypted.get(info.filename, zif.read(info.filename))) diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kgenpids.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kgenpids.py deleted file mode 100644 index 9b4373e..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kgenpids.py +++ /dev/null @@ -1,310 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# kgenpids.py -# Copyright © 2008-2017 Apprentice Harper et al. - -__license__ = 'GPL v3' -__version__ = '2.1' - -# Revision history: -# 2.0 - Fix for non-ascii Windows user names -# 2.1 - Actual fix for non-ascii WIndows user names. -# x.x - Return information needed for KFX decryption - -import sys -import os, csv -import binascii -import zlib -import re -from struct import pack, unpack, unpack_from -import traceback - -class DrmException(Exception): - pass - -global charMap1 -global charMap3 -global charMap4 - - -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 getKindlePids(rec209, token, serialnum): - if rec209 is None: - return [serialnum] - - pids=[] - - if isinstance(serialnum,unicode): - serialnum = serialnum.encode('utf-8') - - # Compute book PID - pidHash = SHA1(serialnum+rec209+token) - bookPID = encodePID(pidHash) - bookPID = checksumPid(bookPID) - pids.append(bookPID) - - # compute fixed pid for old pre 2.5 firmware update pid as well - kindlePID = pidFromSerial(serialnum, 7) + "*" - kindlePID = checksumPid(kindlePID) - pids.append(kindlePID) - - 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'] - -def getK4Pids(rec209, token, kindleDatabase): - global charMap1 - pids = [] - - try: - # Get the kindle account token, if present - kindleAccountToken = (kindleDatabase[1])['kindle.account.tokens'].decode('hex') - - except KeyError: - kindleAccountToken="" - pass - - try: - # Get the DSN token, if present - DSN = (kindleDatabase[1])['DSN'].decode('hex') - print u"Got DSN key from database {0}".format(kindleDatabase[0]) - except KeyError: - # See if we have the info to generate the DSN - try: - # Get the Mazama Random number - MazamaRandomNumber = (kindleDatabase[1])['MazamaRandomNumber'].decode('hex') - #print u"Got MazamaRandomNumber from database {0}".format(kindleDatabase[0]) - - try: - # Get the SerialNumber token, if present - IDString = (kindleDatabase[1])['SerialNumber'].decode('hex') - print u"Got SerialNumber from database {0}".format(kindleDatabase[0]) - except KeyError: - # Get the IDString we added - IDString = (kindleDatabase[1])['IDString'].decode('hex') - - try: - # Get the UsernameHash token, if present - encodedUsername = (kindleDatabase[1])['UsernameHash'].decode('hex') - print u"Got UsernameHash from database {0}".format(kindleDatabase[0]) - except KeyError: - # Get the UserName we added - UserName = (kindleDatabase[1])['UserName'].decode('hex') - # encode it - encodedUsername = encodeHash(UserName,charMap1) - #print u"encodedUsername",encodedUsername.encode('hex') - except KeyError: - print u"Keys not found in the database {0}.".format(kindleDatabase[0]) - return pids - - # Get the ID string used - encodedIDString = encodeHash(IDString,charMap1) - #print u"encodedIDString",encodedIDString.encode('hex') - - # concat, hash and encode to calculate the DSN - DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1) - #print u"DSN",DSN.encode('hex') - pass - - if rec209 is None: - pids.append(DSN+kindleAccountToken) - return pids - - # Compute the device PID (for which I can tell, is used for nothing). - table = generatePidEncryptionTable() - devicePID = generateDevicePID(table,DSN,4) - devicePID = checksumPid(devicePID) - pids.append(devicePID) - - # Compute book PIDs - - # book pid - pidHash = SHA1(DSN+kindleAccountToken+rec209+token) - bookPID = encodePID(pidHash) - bookPID = checksumPid(bookPID) - pids.append(bookPID) - - # variant 1 - pidHash = SHA1(kindleAccountToken+rec209+token) - bookPID = encodePID(pidHash) - bookPID = checksumPid(bookPID) - pids.append(bookPID) - - # variant 2 - pidHash = SHA1(DSN+rec209+token) - bookPID = encodePID(pidHash) - bookPID = checksumPid(bookPID) - pids.append(bookPID) - - return pids - -def getPidList(md1, md2, serials=[], kDatabases=[]): - pidlst = [] - - if kDatabases is None: - kDatabases = [] - if serials is None: - serials = [] - - for kDatabase in kDatabases: - try: - pidlst.extend(getK4Pids(md1, md2, kDatabase)) - except Exception, e: - print u"Error getting PIDs from database {0}: {1}".format(kDatabase[0],e.args[0]) - traceback.print_exc() - - for serialnum in serials: - try: - pidlst.extend(getKindlePids(md1, md2, serialnum)) - except Exception, e: - print u"Error getting PIDs from serial number {0}: {1}".format(serialnum ,e.args[0]) - traceback.print_exc() - - return pidlst diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kindlekey.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kindlekey.py deleted file mode 100644 index e20b7c9..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kindlekey.py +++ /dev/null @@ -1,1727 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# kindlekey.py -# Copyright © 2008-2017 Apprentice Harper et al. - -__license__ = 'GPL v3' -__version__ = '2.5' - -# Revision history: -# 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc. -# 1.1 - Added Tkinter to match adobekey.py -# 1.2 - Fixed testing of successful retrieval on Mac -# 1.3 - Added getkey interface for Windows DeDRM application -# Simplified some of the Kindle for Mac code. -# 1.4 - Remove dependency on alfcrypto -# 1.5 - moved unicode_argv call inside main for Windows DeDRM compatibility -# 1.6 - Fixed a problem getting the disk serial numbers -# 1.7 - Work if TkInter is missing -# 1.8 - Fixes for Kindle for Mac, and non-ascii in Windows user names -# 1.9 - Fixes for Unicode in Windows user names -# 2.0 - Added comments and extra fix for non-ascii Windows user names -# 2.1 - Fixed Kindle for PC encryption changes March 2016 -# 2.2 - Fixes for Macs with bonded ethernet ports -# Also removed old .kinfo file support (pre-2011) -# 2.3 - Added more field names thanks to concavegit's KFX code. -# 2.4 - Fix for complex Mac disk setups, thanks to Tibs -# 2.5 - Final Fix for Windows user names with non-ascii characters, thanks to oneofusoneofus - - -""" -Retrieve Kindle for PC/Mac user key. -""" - -import sys, os, re -from struct import pack, unpack, unpack_from -import json -import getopt - -# Routines common to Mac and PC - -# 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 '?'. So use shell32.GetCommandLineArgvW to get sys.argv - # as a list of Unicode strings and encode them as utf-8 - - from ctypes import POINTER, byref, cdll, c_int, windll - from ctypes.wintypes import LPCWSTR, LPWSTR - - GetCommandLineW = cdll.kernel32.GetCommandLineW - GetCommandLineW.argtypes = [] - GetCommandLineW.restype = LPCWSTR - - CommandLineToArgvW = windll.shell32.CommandLineToArgvW - CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] - CommandLineToArgvW.restype = POINTER(LPWSTR) - - cmd = GetCommandLineW() - argc = c_int(0) - argv = CommandLineToArgvW(cmd, byref(argc)) - if argc.value > 0: - # Remove Python executable and commands if present - start = argc.value - len(sys.argv) - return [argv[i] 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"kindlekey.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 - -# 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 K4M/PC 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 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 aescbc_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' - - 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, noPadding(), len(userkey)) - - def decrypt(self, data): - iv = self._iv - cleartext = self.aes.decrypt(iv + data) - return cleartext - - import hmac - - 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] - - 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 - - # Various character maps used to decrypt kindle info values. - # Probably supposed to act as obfuscation - charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_" - charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE" - # New maps in K4PC 1.9.0 - testMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" - testMap6 = "9YzAb0Cd1Ef2n5Pr6St7Uvh3Jk4M8WxG" - testMap8 = "YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD" - - # interface with Windows OS Routines - class DataBlob(Structure): - _fields_ = [('cbData', c_uint), - ('pbData', c_void_p)] - DataBlob_p = POINTER(DataBlob) - - - 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 = GetSystemDirectory().split('\\')[0] + '\\'): - vsn = c_uint(0) - GetVolumeInformationW(path, None, 0, byref(vsn), None, None, None, 0) - return str(vsn.value) - return GetVolumeSerialNumber - GetVolumeSerialNumber = GetVolumeSerialNumber() - - def GetIDString(): - vsn = GetVolumeSerialNumber() - #print('Using Volume Serial Number for ID: '+vsn) - return vsn - - def getLastError(): - GetLastError = kernel32.GetLastError - GetLastError.argtypes = None - GetLastError.restype = c_uint - def getLastError(): - return GetLastError() - return getLastError - getLastError = getLastError() - - def GetUserName(): - GetUserNameW = advapi32.GetUserNameW - GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)] - GetUserNameW.restype = c_uint - def GetUserName(): - buffer = create_unicode_buffer(2) - size = c_uint(len(buffer)) - while not GetUserNameW(buffer, byref(size)): - errcd = getLastError() - if errcd == 234: - # bad wine implementation up through wine 1.3.21 - return "AlternateUserName" - # double the buffer size - buffer = create_unicode_buffer(len(buffer) * 2) - size.value = len(buffer) - - # replace any non-ASCII values with 0xfffd - for i in xrange(0,len(buffer)): - if buffer[i]>u"\u007f": - #print u"swapping char "+str(i)+" ("+buffer[i]+")" - buffer[i] = u"\ufffd" - # return utf-8 encoding of modified username - #print u"modified username:"+buffer.value - return buffer.value.encode('utf-8') - return GetUserName - GetUserName = GetUserName() - - 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, flags): - 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, flags, byref(outdata)): - # raise DrmException("Failed to Unprotect Data") - return 'failed' - return string_at(outdata.pbData, outdata.cbData) - return CryptUnprotectData - CryptUnprotectData = CryptUnprotectData() - - # Returns Environmental Variables that contain unicode - def getEnvironmentVariable(name): - import ctypes - name = unicode(name) # make sure string argument is unicode - n = ctypes.windll.kernel32.GetEnvironmentVariableW(name, None, 0) - if n == 0: - return None - buf = ctypes.create_unicode_buffer(u'\0'*n) - ctypes.windll.kernel32.GetEnvironmentVariableW(name, buf, n) - return buf.value - - # Locate all of the kindle-info style files and return as list - def getKindleInfoFiles(): - kInfoFiles = [] - # some 64 bit machines do not have the proper registry key for some reason - # or the python interface to the 32 vs 64 bit registry is broken - path = "" - if 'LOCALAPPDATA' in os.environ.keys(): - # Python 2.x does not return unicode env. Use Python 3.x - path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%") - # this is just another alternative. - # path = getEnvironmentVariable('LOCALAPPDATA') - if not os.path.isdir(path): - path = "" - else: - # User Shell Folders show take precedent over Shell Folders if present - try: - # this will still break - regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\") - path = winreg.QueryValueEx(regkey, 'Local AppData')[0] - if not os.path.isdir(path): - path = "" - try: - regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\") - path = winreg.QueryValueEx(regkey, 'Local AppData')[0] - if not os.path.isdir(path): - path = "" - except RegError: - pass - except RegError: - pass - - found = False - if path == "": - print ('Could not find the folder in which to look for kinfoFiles.') - else: - # Probably not the best. To Fix (shouldn't ignore in encoding) or use utf-8 - print(u'searching for kinfoFiles in ' + path.encode('ascii', 'ignore')) - - # look for (K4PC 1.9.0 and later) .kinf2011 file - kinfopath = path +'\\Amazon\\Kindle\\storage\\.kinf2011' - if os.path.isfile(kinfopath): - found = True - print('Found K4PC 1.9+ kinf2011 file: ' + kinfopath.encode('ascii','ignore')) - kInfoFiles.append(kinfopath) - - # look for (K4PC 1.6.0 and later) rainier.2.1.1.kinf file - kinfopath = path +'\\Amazon\\Kindle\\storage\\rainier.2.1.1.kinf' - if os.path.isfile(kinfopath): - found = True - print('Found K4PC 1.6-1.8 kinf file: ' + kinfopath) - kInfoFiles.append(kinfopath) - - # look for (K4PC 1.5.0 and later) rainier.2.1.1.kinf file - kinfopath = path +'\\Amazon\\Kindle For PC\\storage\\rainier.2.1.1.kinf' - if os.path.isfile(kinfopath): - found = True - print('Found K4PC 1.5 kinf file: ' + kinfopath) - kInfoFiles.append(kinfopath) - - # look for original (earlier than K4PC 1.5.0) kindle-info files - kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info' - if os.path.isfile(kinfopath): - found = True - print('Found K4PC kindle.info file: ' + kinfopath) - kInfoFiles.append(kinfopath) - - if not found: - print('No K4PC kindle.info/kinf/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',\ - 'build_version',\ - 'SerialNumber',\ - 'UsernameHash',\ - 'kindle.directedid.info',\ - 'DSN',\ - 'kindle.accounttype.info',\ - 'krx.flashcardsplugin.data.encryption_key',\ - 'krx.notebookexportplugin.data.encryption_key',\ - 'proxy.http.password',\ - 'proxy.http.username' - ] - DB = {} - with open(kInfoFile, 'rb') as infoReader: - data = infoReader.read() - # assume 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 - data = data[:-1] - items = data.split('/') - - # starts with an encoded and encrypted header blob - headerblob = items.pop(0) - encryptedValue = decode(headerblob, testMap1) - cleartext = UnprotectHeaderData(encryptedValue) - #print "header cleartext:",cleartext - # 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 - #print "keyname found from hash:",keyname - break - if keyname == "unknown": - keyname = keyhash - #print "keyname not found, hash is:",keyname - - # 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) - #print "encrypted data:",encdata - contlen = len(encdata) - noffset = contlen - primes(int(contlen/3))[-1] - pfx = encdata[0:noffset] - encdata = encdata[noffset:] - encdata = encdata + pfx - #print "rearranged data:",encdata - - - # decode using new testMap8 to get the original CryptProtect Data - encryptedValue = decode(encdata,testMap8) - #print "decoded data:",encryptedValue.encode('hex') - cleartext = CryptUnprotectData(encryptedValue, entropy, 1) - if len(cleartext)>0: - #print "cleartext data:",cleartext,":end data" - DB[keyname] = cleartext - #print keyname, cleartext - - if len(DB)>6: - # store values used in decryption - DB['IDString'] = GetIDString() - DB['UserName'] = GetUserName() - print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(GetIDString(), GetUserName().encode('hex')) - else: - print u"Couldn't decrypt file." - DB = {} - return DB -elif isosx: - import copy - import subprocess - - # 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(u"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(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(u"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(u"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() - - # 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' - - # uses a sub process to get the Hard Drive Serial Number using ioreg - # returns serial numbers of all internal hard drive drives - def GetVolumesSerialNumbers(): - sernums = [] - sernum = os.getenv('MYSERIALNUMBER') - if sernum != None: - sernums.append(sernum.strip()) - cmdline = '/usr/sbin/ioreg -w 0 -r -c AppleAHCIDiskDriver' - cmdline = cmdline.encode(sys.getfilesystemencoding()) - p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) - out1, out2 = p.communicate() - #print out1 - reslst = out1.split('\n') - cnt = len(reslst) - for j in xrange(cnt): - resline = reslst[j] - pp = resline.find('\"Serial Number\" = \"') - if pp >= 0: - sernum = resline[pp+19:-1] - sernums.append(sernum.strip()) - return sernums - - def GetDiskPartitionNames(): - names = [] - cmdline = '/sbin/mount' - cmdline = cmdline.encode(sys.getfilesystemencoding()) - p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) - out1, out2 = p.communicate() - reslst = out1.split('\n') - cnt = len(reslst) - for j in xrange(cnt): - resline = reslst[j] - if resline.startswith('/dev'): - (devpart, mpath) = resline.split(' on ')[:2] - dpart = devpart[5:] - names.append(dpart) - return names - - # uses a sub process to get the UUID of all disk partitions - def GetDiskPartitionUUIDs(): - uuids = [] - uuidnum = os.getenv('MYUUIDNUMBER') - if uuidnum != None: - uuids.append(strip(uuidnum)) - cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver' - cmdline = cmdline.encode(sys.getfilesystemencoding()) - p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) - out1, out2 = p.communicate() - #print out1 - reslst = out1.split('\n') - cnt = len(reslst) - for j in xrange(cnt): - resline = reslst[j] - pp = resline.find('\"UUID\" = \"') - if pp >= 0: - uuidnum = resline[pp+10:-1] - uuidnum = uuidnum.strip() - uuids.append(uuidnum) - return uuids - - def GetMACAddressesMunged(): - macnums = [] - macnum = os.getenv('MYMACNUM') - if macnum != None: - macnums.append(macnum) - cmdline = 'networksetup -listallhardwareports' # en0' - cmdline = cmdline.encode(sys.getfilesystemencoding()) - p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) - out1, out2 = p.communicate() - reslst = out1.split('\n') - cnt = len(reslst) - for j in xrange(cnt): - resline = reslst[j] - pp = resline.find('Ethernet Address: ') - if pp >= 0: - #print resline - macnum = resline[pp+18:] - macnum = macnum.strip() - maclst = macnum.split(':') - n = len(maclst) - if n != 6: - continue - #print 'original mac', macnum - # now munge it up the way Kindle app does - # by xoring it with 0xa5 and swapping elements 3 and 4 - for i in range(6): - maclst[i] = int('0x' + maclst[i], 0) - mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] - mlst[5] = maclst[5] ^ 0xa5 - mlst[4] = maclst[3] ^ 0xa5 - mlst[3] = maclst[4] ^ 0xa5 - 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]) - #print 'munged mac', macnum - macnums.append(macnum) - return macnums - - - # uses unix env to get username instead of using sysctlbyname - def GetUserName(): - username = os.getenv('USER') - #print "Username:",username - return username - - def GetIDStrings(): - # Return all possible ID Strings - strings = [] - strings.extend(GetMACAddressesMunged()) - strings.extend(GetVolumesSerialNumbers()) - strings.extend(GetDiskPartitionNames()) - strings.extend(GetDiskPartitionUUIDs()) - strings.append('9999999999') - #print "ID Strings:\n",strings - return strings - - - # 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 - class CryptUnprotectData(object): - def __init__(self, entropy, IDString): - sp = GetUserName() + '+@#$%+' + IDString - 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 from 1.10 - 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 from 1.6 - 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 .kindle-info files from 1.4 - 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 - # check for .kindle-info file from 1.2.2 - testpath = home + '/Library/Application Support/Amazon/Kindle/storage/.kindle-info' - if os.path.isfile(testpath): - kInfoFiles.append(testpath) - print('Found k4Mac kindle-info file: ' + testpath) - found = True - # check for .kindle-info file from 1.0 beta 1 (27214) - testpath = home + '/Library/Application Support/Amazon/Kindle for Mac/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',\ - 'build_version',\ - 'SerialNumber',\ - 'UsernameHash',\ - 'kindle.directedid.info',\ - 'DSN' - ] - with open(kInfoFile, 'rb') as infoReader: - filedata = infoReader.read() - - data = filedata[:-1] - items = data.split('/') - IDStrings = GetIDStrings() - for IDString in IDStrings: - #print "trying IDString:",IDString - try: - DB = {} - 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 = CryptUnprotectData(entropy,IDString) - - # 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 - if len(cleartext) > 0: - DB[keyname] = cleartext - - if len(DB)>6: - break - except: - pass - if len(DB)>6: - # store values used in decryption - print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString, GetUserName()) - DB['IDString'] = IDString - DB['UserName'] = GetUserName() - else: - print u"Couldn't decrypt file." - DB = {} - return DB -else: - def getDBfromFile(kInfoFile): - raise DrmException(u"This script only runs under Windows or Mac OS X.") - return {} - -def kindlekeys(files = []): - keys = [] - if files == []: - files = getKindleInfoFiles() - for file in files: - key = getDBfromFile(file) - if key: - # convert all values to hex, just in case. - for keyname in key: - key[keyname]=key[keyname].encode('hex') - keys.append(key) - return keys - -# interface for Python DeDRM -# returns single key or multiple keys, depending on path or file passed in -def getkey(outpath, files=[]): - keys = kindlekeys(files) - if len(keys) > 0: - if not os.path.isdir(outpath): - outfile = outpath - with file(outfile, 'w') as keyfileout: - keyfileout.write(json.dumps(keys[0])) - print u"Saved a key to {0}".format(outfile) - else: - keycount = 0 - for key in keys: - while True: - keycount += 1 - outfile = os.path.join(outpath,u"kindlekey{0:d}.k4i".format(keycount)) - if not os.path.exists(outfile): - break - with file(outfile, 'w') as keyfileout: - keyfileout.write(json.dumps(key)) - print u"Saved a key to {0}".format(outfile) - return True - return False - -def usage(progname): - print u"Finds, decrypts and saves the default Kindle For Mac/PC encryption keys." - print u"Keys are saved to the current directory, or a specified output directory." - print u"If a file name is passed instead of a directory, only the first key is saved, in that file." - print u"Usage:" - print u" {0:s} [-h] [-k ] []".format(progname) - - -def cli_main(): - sys.stdout=SafeUnbuffered(sys.stdout) - sys.stderr=SafeUnbuffered(sys.stderr) - argv=unicode_argv() - progname = os.path.basename(argv[0]) - print u"{0} v{1}\nCopyright © 2010-2016 by some_updates, Apprentice Alf and Apprentice Harper".format(progname,__version__) - - try: - opts, args = getopt.getopt(argv[1:], "hk:") - except getopt.GetoptError, err: - print u"Error in options or arguments: {0}".format(err.args[0]) - usage(progname) - sys.exit(2) - - files = [] - for o, a in opts: - if o == "-h": - usage(progname) - sys.exit(0) - if o == "-k": - files = [a] - - if len(args) > 1: - usage(progname) - sys.exit(2) - - if len(args) == 1: - # save to the specified file or directory - outpath = args[0] - if not os.path.isabs(outpath): - outpath = os.path.abspath(outpath) - else: - # save to the same directory as the script - outpath = os.path.dirname(argv[0]) - - # make sure the outpath is canonical - outpath = os.path.realpath(os.path.normpath(outpath)) - - if not getkey(outpath, files): - print u"Could not retrieve Kindle for Mac/PC key." - return 0 - - -def gui_main(): - try: - import Tkinter - import Tkconstants - import tkMessageBox - import traceback - except: - return cli_main() - - class ExceptionDialog(Tkinter.Frame): - def __init__(self, root, text): - Tkinter.Frame.__init__(self, root, border=5) - 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) - - - argv=unicode_argv() - root = Tkinter.Tk() - root.withdraw() - progpath, progname = os.path.split(argv[0]) - success = False - try: - keys = kindlekeys() - keycount = 0 - for key in keys: - while True: - keycount += 1 - outfile = os.path.join(progpath,u"kindlekey{0:d}.k4i".format(keycount)) - if not os.path.exists(outfile): - break - - with file(outfile, 'w') as keyfileout: - keyfileout.write(json.dumps(key)) - success = True - tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile)) - except DrmException, e: - tkMessageBox.showerror(progname, u"Error: {0}".format(str(e))) - except Exception: - root.wm_state('normal') - root.title(progname) - text = traceback.format_exc() - ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1) - root.mainloop() - if not success: - return 1 - return 0 - -if __name__ == '__main__': - if len(sys.argv) > 1: - sys.exit(cli_main()) - sys.exit(gui_main()) diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kindlepid.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kindlepid.py deleted file mode 100644 index 8bbcf69..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kindlepid.py +++ /dev/null @@ -1,144 +0,0 @@ -#!/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 -# 0.4 Added support for serial numbers starting with '9', fixed unicode bugs. -# 0.5 moved unicode_argv call inside main for Windows DeDRM compatibility - -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"kindlepid.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(): - print u"Mobipocket PID calculator for Amazon Kindle. Copyright © 2007, 2009 Igor Skochinsky" - argv=unicode_argv() - if len(argv)==2: - serial = argv[1] - else: - print u"Usage: kindlepid.py /" - return 1 - if len(serial)==16: - if serial.startswith("B") or serial.startswith("9"): - 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/libalfcrypto.dylib b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/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/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/libalfcrypto32.so b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/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/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/libalfcrypto64.so b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/libalfcrypto64.so deleted file mode 100644 index a08ac28930fdd54e0eb4e8973a398e5e951d3825..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33417 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 Ko^Mvd(mwtG diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/mobidedrm.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/mobidedrm.py deleted file mode 100644 index 501aa2d..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/mobidedrm.py +++ /dev/null @@ -1,543 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# mobidedrm.py -# Copyright © 2008 The Dark Reverser -# Portions © 2008–2017 Apprentice Harper et al. - -__license__ = 'GPL v3' -__version__ = u"0.42" - -# 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 -# 0.38 - Unicode used wherever possible, cope with absent alfcrypto -# 0.39 - Fixed problem with TEXtREAd and getBookType interface -# 0.40 - moved unicode_argv call inside main for Windows DeDRM compatibility -# 0.41 - Fixed potential unicode problem in command line calls -# 0.42 - Added GPL v3 licence. updated/removed some print statements - -import sys -import os -import struct -import binascii -try: - from alfcrypto import Pukall_Cipher -except: - print u"AlfCrypto not found. Using python PC1 implementation." - -# 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] - - -class DrmException(Exception): - pass - - -# -# MobiBook Utility Routines -# - -# Implementation of Pukall Cipher 1 -def PC1(key, src, decryption=True): - # if we can get it from alfcrypto, use that - try: - return Pukall_Cipher().PC1(key,src,decryption) - except NameError: - pass - except TypeError: - 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' - 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 cleanup(self): - # to match function in Topaz book - pass - - def __init__(self, infile): - print u"MobiDeDrm v{0:s}.\nCopyright © 2008-2017 The Dark Reverser, Apprentice Harper 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(u"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]) - - # det default values before PalmDoc test - self.print_replica = False - self.extra_data_flags = 0 - self.meta_array = {} - self.mobi_length = 0 - self.mobi_codepage = 1252 - self.mobi_version = -1 - - if self.magic == 'TEXtREAd': - print u"PalmDoc format book detected." - 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 u"MOBI header version {0:d}, header length {1:d}".format(self.mobi_version, self.mobi_length) - if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5): - self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4]) - #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. - self.extra_data_flags &= 0xFFFE - - # if exth region exists parse it for metadata array - try: - exth_flag, = struct.unpack('>L', self.sect[0x80:0x84]) - exth = '' - if exth_flag & 0x40: - exth = self.sect[16 + self.mobi_length:] - if (len(exth) >= 12) 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: - pass - - 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) - - 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 getFile(self, outpath): - file(outpath,'wb').write(self.mobi_data) - - def getBookType(self): - if self.print_replica: - return u"Print Replica" - if self.mobi_version >= 8: - return u"Kindle Format 8" - if self.mobi_version >= 0: - return u"Mobipocket {0:d}".format(self.mobi_version) - return u"PalmDoc" - - 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 u"Crypto Type is: {0:d}".format(crypto_type) - self.crypto_type = crypto_type - if crypto_type == 0: - 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(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(u"Cannot decode library or rented ebooks.") - - goodpids = [] - for pid in pidlist: - if len(pid)==10: - if checksumPid(pid[0:-2]) != pid: - 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) - else: - print u"Warning: PID {0} has wrong number of digits".format(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(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(u"No key found in {0:d} keys tried.".format(len(goodpids))) - # 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 u"File has default encryption, no specific key needed." - else: - print u"File is encoded with PID {0}.".format(checksumPid(pid)) - - # clear the crypto type - self.patchSection(0, "\0" * 2, 0xC) - - # decrypt sections - 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 u".", - # 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 u"done" - return - -def getUnencryptedBook(infile,pidlist): - if not os.path.isfile(infile): - raise DrmException(u"Input File Not Found.") - book = MobiBook(infile) - book.processBook(pidlist) - return book.mobi_data - - -def cli_main(): - argv=unicode_argv() - progname = os.path.basename(argv[0]) - if len(argv)<3 or len(argv)>4: - print u"MobiDeDrm v{0:s}.\nCopyright © 2008-2017 The Dark Reverser, Apprentice Harper 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(progname) - return 1 - else: - infile = argv[1] - outfile = argv[2] - if len(argv) is 4: - pidlist = argv[3].split(',') - else: - pidlist = [] - try: - stripped_file = getUnencryptedBook(infile, pidlist) - file(outfile, 'wb').write(stripped_file) - except DrmException, e: - print u"MobiDeDRM v{0} Error: {1:s}".format(__version__,e.args[0]) - return 1 - return 0 - - -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/openssl_des.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/openssl_des.py deleted file mode 100644 index 9a84e58..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/openssl_des.py +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab - -# implement just enough of des from openssl to make erdr2pml.py happy - -def load_libcrypto(): - from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_char, c_int, c_long, \ - Structure, c_ulong, create_string_buffer, cast - from ctypes.util import find_library - import sys - - if sys.platform.startswith('win'): - libcrypto = find_library('libeay32') - else: - libcrypto = find_library('crypto') - - if libcrypto is None: - return None - - libcrypto = CDLL(libcrypto) - - # typedef struct DES_ks - # { - # union - # { - # DES_cblock cblock; - # /* make sure things are correct size on machines with - # * 8 byte longs */ - # DES_LONG deslong[2]; - # } ks[16]; - # } DES_key_schedule; - - # 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), - ('DES_cblock2', c_char * 16), - ('DES_cblock3', c_char * 16), - ('DES_cblock4', c_char * 16), - ('DES_cblock5', c_char * 16), - ('DES_cblock6', c_char * 16), - ('DES_cblock7', c_char * 16), - ('DES_cblock8', c_char * 16), - ('DES_cblock9', c_char * 16), - ('DES_cblock10', c_char * 16), - ('DES_cblock11', c_char * 16), - ('DES_cblock12', c_char * 16), - ('DES_cblock13', c_char * 16), - ('DES_cblock14', c_char * 16), - ('DES_cblock15', c_char * 16), - ('DES_cblock16', c_char * 16)] - - DES_KEY_SCHEDULE_p = POINTER(DES_KEY_SCHEDULE) - - def F(restype, name, argtypes): - func = getattr(libcrypto, name) - func.restype = restype - func.argtypes = argtypes - return func - - 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 : - raise Exception('DES improper key used') - return - self.key = key - self.keyschedule = DES_KEY_SCHEDULE() - DES_set_key(self.key, self.keyschedule) - def desdecrypt(self, data): - ob = create_string_buffer(len(data)) - DES_ecb_encrypt(data, ob, self.keyschedule, 0) - return ob.raw - def decrypt(self, data): - if not data: - return '' - i = 0 - result = [] - while i < len(data): - block = data[i:i+8] - processed_block = self.desdecrypt(block) - result.append(processed_block) - i += 8 - return ''.join(result) - - return DES diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/plugin-import-name-dedrm.txt b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/plugin-import-name-dedrm.txt deleted file mode 100644 index e69de29..0000000 diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/prefs.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/prefs.py deleted file mode 100644 index c1bfcb9..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/prefs.py +++ /dev/null @@ -1,295 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai - -from __future__ import with_statement -__license__ = 'GPL v3' - -# Standard Python modules. -import os, sys, re, hashlib -import json -import traceback - -from calibre.utils.config import dynamic, config_dir, JSONConfig -from calibre_plugins.dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION -from calibre.constants import iswindows, isosx - -class DeDRM_Prefs(): - def __init__(self): - JSON_PATH = os.path.join(u"plugins", PLUGIN_NAME.strip().lower().replace(' ', '_') + '.json') - self.dedrmprefs = JSONConfig(JSON_PATH) - - self.dedrmprefs.defaults['configured'] = False - self.dedrmprefs.defaults['bandnkeys'] = {} - self.dedrmprefs.defaults['adeptkeys'] = {} - self.dedrmprefs.defaults['ereaderkeys'] = {} - self.dedrmprefs.defaults['kindlekeys'] = {} - self.dedrmprefs.defaults['androidkeys'] = {} - self.dedrmprefs.defaults['pids'] = [] - self.dedrmprefs.defaults['serials'] = [] - self.dedrmprefs.defaults['adobewineprefix'] = "" - self.dedrmprefs.defaults['kindlewineprefix'] = "" - - # initialise - # we must actually set the prefs that are dictionaries and lists - # to empty dictionaries and lists, otherwise we are unable to add to them - # as then it just adds to the (memory only) dedrmprefs.defaults versions! - if self.dedrmprefs['bandnkeys'] == {}: - self.dedrmprefs['bandnkeys'] = {} - if self.dedrmprefs['adeptkeys'] == {}: - self.dedrmprefs['adeptkeys'] = {} - if self.dedrmprefs['ereaderkeys'] == {}: - self.dedrmprefs['ereaderkeys'] = {} - if self.dedrmprefs['kindlekeys'] == {}: - self.dedrmprefs['kindlekeys'] = {} - if self.dedrmprefs['androidkeys'] == {}: - self.dedrmprefs['androidkeys'] = {} - if self.dedrmprefs['pids'] == []: - self.dedrmprefs['pids'] = [] - if self.dedrmprefs['serials'] == []: - self.dedrmprefs['serials'] = [] - - def __getitem__(self,kind = None): - if kind is not None: - return self.dedrmprefs[kind] - return self.dedrmprefs - - def set(self, kind, value): - self.dedrmprefs[kind] = value - - def writeprefs(self,value = True): - self.dedrmprefs['configured'] = value - - def addnamedvaluetoprefs(self, prefkind, keyname, keyvalue): - try: - if keyvalue not in self.dedrmprefs[prefkind].values(): - # ensure that the keyname is unique - # by adding a number (starting with 2) to the name if it is not - namecount = 1 - newname = keyname - while newname in self.dedrmprefs[prefkind]: - namecount += 1 - newname = "{0:s}_{1:d}".format(keyname,namecount) - # add to the preferences - self.dedrmprefs[prefkind][newname] = keyvalue - return (True, newname) - except: - traceback.print_exc() - pass - return (False, keyname) - - def addvaluetoprefs(self, prefkind, prefsvalue): - # ensure the keyvalue isn't already in the preferences - try: - if prefsvalue not in self.dedrmprefs[prefkind]: - self.dedrmprefs[prefkind].append(prefsvalue) - return True - except: - traceback.print_exc() - return False - - -def convertprefs(always = False): - - def parseIgnobleString(keystuff): - from calibre_plugins.dedrm.ignoblekeygen import generate_key - userkeys = [] - ar = keystuff.split(':') - for keystring in ar: - try: - name, ccn = keystring.split(',') - # Generate Barnes & Noble EPUB user key from name and credit card number. - keyname = u"{0}_{1}".format(name.strip(),ccn.strip()[-4:]) - keyvalue = generate_key(name, ccn) - userkeys.append([keyname,keyvalue]) - except Exception, e: - traceback.print_exc() - print e.args[0] - pass - return userkeys - - def parseeReaderString(keystuff): - from calibre_plugins.dedrm.erdr2pml import getuser_key - userkeys = [] - ar = keystuff.split(':') - for keystring in ar: - try: - name, cc = keystring.split(',') - # Generate eReader user key from name and credit card number. - keyname = u"{0}_{1}".format(name.strip(),cc.strip()[-4:]) - keyvalue = getuser_key(name,cc).encode('hex') - userkeys.append([keyname,keyvalue]) - except Exception, e: - traceback.print_exc() - print e.args[0] - pass - return userkeys - - def parseKindleString(keystuff): - pids = [] - serials = [] - ar = keystuff.split(',') - for keystring in ar: - keystring = str(keystring).strip().replace(" ","") - if len(keystring) == 10 or len(keystring) == 8 and keystring not in pids: - pids.append(keystring) - elif len(keystring) == 16 and keystring[0] == 'B' and keystring not in serials: - serials.append(keystring) - return (pids,serials) - - def getConfigFiles(extension, encoding = None): - # get any files with extension 'extension' in the config dir - userkeys = [] - files = [f for f in os.listdir(config_dir) if f.endswith(extension)] - for filename in files: - try: - fpath = os.path.join(config_dir, filename) - key = os.path.splitext(filename)[0] - value = open(fpath, 'rb').read() - if encoding is not None: - value = value.encode(encoding) - userkeys.append([key,value]) - except: - traceback.print_exc() - pass - return userkeys - - dedrmprefs = DeDRM_Prefs() - - if (not always) and dedrmprefs['configured']: - # We've already converted old preferences, - # and we're not being forced to do it again, so just return - return - - - print u"{0} v{1}: Importing configuration data from old DeDRM plugins".format(PLUGIN_NAME, PLUGIN_VERSION) - - IGNOBLEPLUGINNAME = "Ignoble Epub DeDRM" - EREADERPLUGINNAME = "eReader PDB 2 PML" - OLDKINDLEPLUGINNAME = "K4PC, K4Mac, Kindle Mobi and Topaz DeDRM" - - # get prefs from older tools - kindleprefs = JSONConfig(os.path.join(u"plugins", u"K4MobiDeDRM")) - ignobleprefs = JSONConfig(os.path.join(u"plugins", u"ignoble_epub_dedrm")) - - # Handle the old ignoble plugin's customization string by converting the - # old string to stored keys... get that personal data out of plain sight. - from calibre.customize.ui import config - sc = config['plugin_customization'] - val = sc.pop(IGNOBLEPLUGINNAME, None) - if val is not None: - print u"{0} v{1}: Converting old Ignoble plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION) - priorkeycount = len(dedrmprefs['bandnkeys']) - userkeys = parseIgnobleString(str(val)) - for keypair in userkeys: - name = keypair[0] - value = keypair[1] - dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value) - addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount - print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from old Ignoble plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys") - # Make the json write all the prefs to disk - dedrmprefs.writeprefs(False) - - # Handle the old eReader plugin's customization string by converting the - # old string to stored keys... get that personal data out of plain sight. - val = sc.pop(EREADERPLUGINNAME, None) - if val is not None: - print u"{0} v{1}: Converting old eReader plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION) - priorkeycount = len(dedrmprefs['ereaderkeys']) - userkeys = parseeReaderString(str(val)) - for keypair in userkeys: - name = keypair[0] - value = keypair[1] - dedrmprefs.addnamedvaluetoprefs('ereaderkeys', name, value) - addedkeycount = len(dedrmprefs['ereaderkeys'])-priorkeycount - print u"{0} v{1}: {2:d} eReader {3} imported from old eReader plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys") - # Make the json write all the prefs to disk - dedrmprefs.writeprefs(False) - - # get old Kindle plugin configuration string - val = sc.pop(OLDKINDLEPLUGINNAME, None) - if val is not None: - print u"{0} v{1}: Converting old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION) - priorpidcount = len(dedrmprefs['pids']) - priorserialcount = len(dedrmprefs['serials']) - pids, serials = parseKindleString(val) - for pid in pids: - dedrmprefs.addvaluetoprefs('pids',pid) - for serial in serials: - dedrmprefs.addvaluetoprefs('serials',serial) - addedpidcount = len(dedrmprefs['pids']) - priorpidcount - addedserialcount = len(dedrmprefs['serials']) - priorserialcount - print u"{0} v{1}: {2:d} {3} and {4:d} {5} imported from old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, u"PID" if addedpidcount==1 else u"PIDs", addedserialcount, u"serial number" if addedserialcount==1 else u"serial numbers") - # Make the json write all the prefs to disk - dedrmprefs.writeprefs(False) - - # copy the customisations back into calibre preferences, as we've now removed the nasty plaintext - config['plugin_customization'] = sc - - # get any .b64 files in the config dir - priorkeycount = len(dedrmprefs['bandnkeys']) - bandnfilekeys = getConfigFiles('.b64') - for keypair in bandnfilekeys: - name = keypair[0] - value = keypair[1] - dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value) - addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount - if addedkeycount > 0: - print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key file" if addedkeycount==1 else u"key files") - # Make the json write all the prefs to disk - dedrmprefs.writeprefs(False) - - # get any .der files in the config dir - priorkeycount = len(dedrmprefs['adeptkeys']) - adeptfilekeys = getConfigFiles('.der','hex') - for keypair in adeptfilekeys: - name = keypair[0] - value = keypair[1] - dedrmprefs.addnamedvaluetoprefs('adeptkeys', name, value) - addedkeycount = len(dedrmprefs['adeptkeys'])-priorkeycount - if addedkeycount > 0: - print u"{0} v{1}: {2:d} Adobe Adept {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"keyfile" if addedkeycount==1 else u"keyfiles") - # Make the json write all the prefs to disk - dedrmprefs.writeprefs(False) - - # get ignoble json prefs - if 'keys' in ignobleprefs: - priorkeycount = len(dedrmprefs['bandnkeys']) - for name in ignobleprefs['keys']: - value = ignobleprefs['keys'][name] - dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value) - addedkeycount = len(dedrmprefs['bandnkeys']) - priorkeycount - # no need to delete old prefs, since they contain no recoverable private data - if addedkeycount > 0: - print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from Ignoble plugin preferences.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys") - # Make the json write all the prefs to disk - dedrmprefs.writeprefs(False) - - # get kindle json prefs - priorpidcount = len(dedrmprefs['pids']) - priorserialcount = len(dedrmprefs['serials']) - if 'pids' in kindleprefs: - pids, serials = parseKindleString(kindleprefs['pids']) - for pid in pids: - dedrmprefs.addvaluetoprefs('pids',pid) - if 'serials' in kindleprefs: - pids, serials = parseKindleString(kindleprefs['serials']) - for serial in serials: - dedrmprefs.addvaluetoprefs('serials',serial) - addedpidcount = len(dedrmprefs['pids']) - priorpidcount - if addedpidcount > 0: - print u"{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, u"PID" if addedpidcount==1 else u"PIDs") - addedserialcount = len(dedrmprefs['serials']) - priorserialcount - if addedserialcount > 0: - print u"{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedserialcount, u"serial number" if addedserialcount==1 else u"serial numbers") - try: - if 'wineprefix' in kindleprefs and kindleprefs['wineprefix'] != "": - dedrmprefs.set('adobewineprefix',kindleprefs['wineprefix']) - dedrmprefs.set('kindlewineprefix',kindleprefs['wineprefix']) - print u"{0} v{1}: WINEPREFIX ‘(2)’ imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, kindleprefs['wineprefix']) - except: - traceback.print_exc() - - - # Make the json write all the prefs to disk - dedrmprefs.writeprefs() - print u"{0} v{1}: Finished setting up configuration data.".format(PLUGIN_NAME, PLUGIN_VERSION) diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/pycrypto_des.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/pycrypto_des.py deleted file mode 100644 index 80d7d65..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/pycrypto_des.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab - - -def load_pycrypto(): - try : - from Crypto.Cipher import DES as _DES - except: - return None - - class DES(object): - def __init__(self, key): - if len(key) != 8 : - raise Error('DES improper key used') - self.key = key - self._des = _DES.new(key,_DES.MODE_ECB) - def desdecrypt(self, data): - return self._des.decrypt(data) - def decrypt(self, data): - if not data: - return '' - i = 0 - result = [] - while i < len(data): - block = data[i:i+8] - processed_block = self.desdecrypt(block) - result.append(processed_block) - i += 8 - return ''.join(result) - return DES diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/python_des.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/python_des.py deleted file mode 100644 index bd02904..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/python_des.py +++ /dev/null @@ -1,220 +0,0 @@ -#!/usr/bin/env python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab -import sys - -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, - 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, - 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] - __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, - 23, 24, 25, 26, 27, 28,27, 28, 29, 30, 31, 0] - __sbox = [[14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, - 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8, - 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0, - 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13], - [15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10, - 3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5, - 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15, - 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9], - [10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8, - 13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1, - 13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7, - 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12], - [7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15, - 13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9, - 10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4, - 3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14], - [2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9, - 14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6, - 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14, - 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3], - [12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11, - 10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8, - 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6, - 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13], - [4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1, - 13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6, - 1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2, - 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12], - [13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, - 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2, - 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8, - 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11],] - __p = [15, 6, 19, 20, 28, 11,27, 16, 0, 14, 22, 25, - 4, 17, 30, 9, 1, 7,23,13, 31, 26, 2, 8,18, 12, 29, 5, 21, 10,3, 24] - __fp = [39, 7, 47, 15, 55, 23, 63, 31,38, 6, 46, 14, 54, 22, 62, 30, - 37, 5, 45, 13, 53, 21, 61, 29,36, 4, 44, 12, 52, 20, 60, 28, - 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 - 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.") - self.block_size = 8 - self.key_size = 8 - self.__padding = '' - self.setMode(mode) - if IV: - self.setIV(IV) - self.L = [] - self.R = [] - self.Kn = [ [0] * 48 ] * 16 # 16 48-bit keys (K1 - K16) - self.final = [] - self.setKey(key) - def getKey(self): - return self.__key - def setKey(self, key): - self.__key = key - self.__create_sub_keys() - def getMode(self): - return self.__mode - def setMode(self, mode): - self.__mode = mode - def getIV(self): - return self.__iv - def setIV(self, IV): - if not IV or len(IV) != self.block_size: - raise ValueError("Invalid Initial Value (IV), must be a multiple of " + str(self.block_size) + " bytes") - self.__iv = IV - def getPadding(self): - return self.__padding - def __String_to_BitList(self, data): - l = len(data) * 8 - result = [0] * l - pos = 0 - for c in data: - i = 7 - ch = ord(c) - while i >= 0: - if ch & (1 << i) != 0: - result[pos] = 1 - else: - result[pos] = 0 - pos += 1 - i -= 1 - return result - def __BitList_to_String(self, data): - result = '' - pos = 0 - c = 0 - while pos < len(data): - c += data[pos] << (7 - (pos % 8)) - if (pos % 8) == 7: - result += chr(c) - c = 0 - pos += 1 - return result - def __permutate(self, table, block): - return [block[x] for x in table] - def __create_sub_keys(self): - key = self.__permutate(Des.__pc1, self.__String_to_BitList(self.getKey())) - i = 0 - self.L = key[:28] - self.R = key[28:] - while i < 16: - j = 0 - while j < Des.__left_rotations[i]: - self.L.append(self.L[0]) - del self.L[0] - self.R.append(self.R[0]) - del self.R[0] - j += 1 - self.Kn[i] = self.__permutate(Des.__pc2, self.L + self.R) - i += 1 - def __des_crypt(self, block, crypt_type): - block = self.__permutate(Des.__ip, block) - self.L = block[:32] - self.R = block[32:] - if crypt_type == Des.ENCRYPT: - iteration = 0 - iteration_adjustment = 1 - else: - iteration = 15 - iteration_adjustment = -1 - i = 0 - while i < 16: - tempR = self.R[:] - self.R = self.__permutate(Des.__expansion_table, self.R) - self.R = [x ^ y for x,y in zip(self.R, self.Kn[iteration])] - B = [self.R[:6], self.R[6:12], self.R[12:18], self.R[18:24], self.R[24:30], self.R[30:36], self.R[36:42], self.R[42:]] - j = 0 - Bn = [0] * 32 - pos = 0 - while j < 8: - m = (B[j][0] << 1) + B[j][5] - n = (B[j][1] << 3) + (B[j][2] << 2) + (B[j][3] << 1) + B[j][4] - v = Des.__sbox[j][(m << 4) + n] - Bn[pos] = (v & 8) >> 3 - Bn[pos + 1] = (v & 4) >> 2 - Bn[pos + 2] = (v & 2) >> 1 - Bn[pos + 3] = v & 1 - pos += 4 - j += 1 - self.R = self.__permutate(Des.__p, Bn) - self.R = [x ^ y for x, y in zip(self.R, self.L)] - self.L = tempR - i += 1 - iteration += iteration_adjustment - self.final = self.__permutate(Des.__fp, self.R + self.L) - return self.final - def crypt(self, data, crypt_type): - if not data: - return '' - if len(data) % self.block_size != 0: - if crypt_type == Des.DECRYPT: # Decryption must work on 8 byte blocks - raise ValueError("Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n.") - if not self.getPadding(): - raise ValueError("Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n. Try setting the optional padding character") - else: - data += (self.block_size - (len(data) % self.block_size)) * self.getPadding() - if self.getMode() == CBC: - if self.getIV(): - iv = self.__String_to_BitList(self.getIV()) - else: - raise ValueError("For CBC mode, you must supply the Initial Value (IV) for ciphering") - i = 0 - dict = {} - result = [] - while i < len(data): - block = self.__String_to_BitList(data[i:i+8]) - if self.getMode() == CBC: - if crypt_type == Des.ENCRYPT: - block = [x ^ y for x, y in zip(block, iv)] - processed_block = self.__des_crypt(block, crypt_type) - if crypt_type == Des.DECRYPT: - processed_block = [x ^ y for x, y in zip(processed_block, iv)] - iv = block - else: - iv = processed_block - else: - processed_block = self.__des_crypt(block, crypt_type) - result.append(self.__BitList_to_String(processed_block)) - i += 8 - if crypt_type == Des.DECRYPT and self.getPadding(): - s = result[-1] - while s[-1] == self.getPadding(): - s = s[:-1] - result[-1] = s - return ''.join(result) - def encrypt(self, data, pad=''): - self.__padding = pad - return self.crypt(data, Des.ENCRYPT) - def decrypt(self, data, pad=''): - self.__padding = pad - return self.crypt(data, Des.DECRYPT) diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/scriptinterface.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/scriptinterface.py deleted file mode 100644 index ec86b13..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/scriptinterface.py +++ /dev/null @@ -1,198 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab - -import sys -import os -import re -import ineptepub -import ignobleepub -import epubtest -import zipfix -import ineptpdf -import erdr2pml -import k4mobidedrm -import traceback - -def decryptepub(infile, outdir, rscpath): - errlog = '' - - # first fix the epub to make sure we do not get errors - name, ext = os.path.splitext(os.path.basename(infile)) - bpath = os.path.dirname(infile) - zippath = os.path.join(bpath,name + '_temp.zip') - rv = zipfix.repairBook(infile, zippath) - if rv != 0: - print "Error while trying to fix epub" - return rv - - # determine a good name for the output file - outfile = os.path.join(outdir, name + '_nodrm.epub') - - rv = 1 - # first try with the Adobe adept epub - if ineptepub.adeptBook(zippath): - # try with any keyfiles (*.der) in the rscpath - files = os.listdir(rscpath) - filefilter = re.compile("\.der$", re.IGNORECASE) - files = filter(filefilter.search, files) - if files: - for filename in files: - keypath = os.path.join(rscpath, filename) - userkey = open(keypath,'rb').read() - try: - rv = ineptepub.decryptBook(userkey, zippath, outfile) - if rv == 0: - print "Decrypted Adobe ePub with key file {0}".format(filename) - break - except Exception, e: - errlog += traceback.format_exc() - errlog += str(e) - rv = 1 - # now try with ignoble epub - elif ignobleepub.ignobleBook(zippath): - # try with any keyfiles (*.b64) in the rscpath - files = os.listdir(rscpath) - filefilter = re.compile("\.b64$", re.IGNORECASE) - files = filter(filefilter.search, files) - if files: - for filename in files: - keypath = os.path.join(rscpath, filename) - userkey = open(keypath,'r').read() - #print userkey - try: - rv = ignobleepub.decryptBook(userkey, zippath, outfile) - if rv == 0: - print "Decrypted B&N ePub with key file {0}".format(filename) - break - except Exception, e: - errlog += traceback.format_exc() - errlog += str(e) - rv = 1 - else: - encryption = epubtest.encryption(zippath) - if encryption == "Unencrypted": - print "{0} is not DRMed.".format(name) - rv = 0 - else: - print "{0} has an unknown encryption.".format(name) - - os.remove(zippath) - if rv != 0: - print errlog - return rv - - -def decryptpdf(infile, outdir, rscpath): - errlog = '' - rv = 1 - - # determine a good name for the output file - name, ext = os.path.splitext(os.path.basename(infile)) - outfile = os.path.join(outdir, name + '_nodrm.pdf') - - # try with any keyfiles (*.der) in the rscpath - files = os.listdir(rscpath) - filefilter = re.compile("\.der$", re.IGNORECASE) - files = filter(filefilter.search, files) - if files: - for filename in files: - keypath = os.path.join(rscpath, filename) - userkey = open(keypath,'rb').read() - try: - rv = ineptpdf.decryptBook(userkey, infile, outfile) - if rv == 0: - break - except Exception, e: - errlog += traceback.format_exc() - errlog += str(e) - rv = 1 - - if rv != 0: - print errlog - return rv - - -def decryptpdb(infile, outdir, rscpath): - outname = os.path.splitext(os.path.basename(infile))[0] + ".pmlz" - outpath = os.path.join(outdir, outname) - rv = 1 - socialpath = os.path.join(rscpath,'sdrmlist.txt') - if os.path.exists(socialpath): - keydata = file(socialpath,'r').read() - keydata = keydata.rstrip(os.linesep) - ar = keydata.split(',') - for i in ar: - try: - name, cc8 = i.split(':') - except ValueError: - print ' Error parsing user supplied social drm data.' - return 1 - try: - rv = erdr2pml.decryptBook(infile, outpath, True, erdr2pml.getuser_key(name, cc8)) - except Exception, e: - errlog += traceback.format_exc() - errlog += str(e) - rv = 1 - - if rv == 0: - break - return rv - - -def decryptk4mobi(infile, outdir, rscpath): - rv = 1 - pidnums = [] - pidspath = os.path.join(rscpath,'pidlist.txt') - if os.path.exists(pidspath): - pidstr = file(pidspath,'r').read() - pidstr = pidstr.rstrip(os.linesep) - pidstr = pidstr.strip() - if pidstr != '': - pidnums = pidstr.split(',') - serialnums = [] - serialnumspath = os.path.join(rscpath,'seriallist.txt') - if os.path.exists(serialnumspath): - serialstr = file(serialnumspath,'r').read() - serialstr = serialstr.rstrip(os.linesep) - serialstr = serialstr.strip() - if serialstr != '': - serialnums = serialstr.split(',') - kDatabaseFiles = [] - files = os.listdir(rscpath) - filefilter = re.compile("\.k4i$", re.IGNORECASE) - files = filter(filefilter.search, files) - if files: - for filename in files: - dpath = os.path.join(rscpath,filename) - kDatabaseFiles.append(dpath) - androidFiles = [] - files = os.listdir(rscpath) - filefilter = re.compile("\.ab$", re.IGNORECASE) - files = filter(filefilter.search, files) - if files: - for filename in files: - dpath = os.path.join(rscpath,filename) - androidFiles.append(dpath) - files = os.listdir(rscpath) - filefilter = re.compile("\.db$", re.IGNORECASE) - files = filter(filefilter.search, files) - if files: - for filename in files: - dpath = os.path.join(rscpath,filename) - androidFiles.append(dpath) - files = os.listdir(rscpath) - filefilter = re.compile("\.xml$", re.IGNORECASE) - files = filter(filefilter.search, files) - if files: - for filename in files: - dpath = os.path.join(rscpath,filename) - androidFiles.append(dpath) - try: - rv = k4mobidedrm.decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serialnums, pidnums) - except Exception, e: - errlog += traceback.format_exc() - errlog += str(e) - rv = 1 - - return rv diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/scrolltextwidget.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/scrolltextwidget.py deleted file mode 100644 index 98b4147..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/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/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/simpleprefs.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/simpleprefs.py deleted file mode 100644 index 0809944..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/simpleprefs.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab - -import sys -import os, os.path -import shutil - -class SimplePrefsError(Exception): - pass - -class SimplePrefs(object): - def __init__(self, target, description): - self.prefs = {} - self.key2file={} - self.file2key={} - for keyfilemap in description: - [key, filename] = keyfilemap - self.key2file[key] = filename - self.file2key[filename] = key - self.target = target + 'Prefs' - if sys.platform.startswith('win'): - import _winreg as winreg - regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\") - path = winreg.QueryValueEx(regkey, 'Local AppData')[0] - prefdir = path + os.sep + self.target - elif sys.platform.startswith('darwin'): - home = os.getenv('HOME') - prefdir = os.path.join(home,'Library','Preferences','org.' + self.target) - else: - # linux and various flavors of unix - home = os.getenv('HOME') - prefdir = os.path.join(home,'.' + self.target) - if not os.path.exists(prefdir): - os.makedirs(prefdir) - self.prefdir = prefdir - self.prefs['dir'] = self.prefdir - self._loadPreferences() - - def _loadPreferences(self): - filenames = os.listdir(self.prefdir) - for filename in filenames: - if filename in self.file2key: - key = self.file2key[filename] - filepath = os.path.join(self.prefdir,filename) - if os.path.isfile(filepath): - try : - data = file(filepath,'rb').read() - self.prefs[key] = data - except Exception, e: - pass - - def getPreferences(self): - return self.prefs - - def setPreferences(self, newprefs={}): - if 'dir' not in newprefs: - raise SimplePrefsError('Error: Attempt to Set Preferences in unspecified directory') - if newprefs['dir'] != self.prefs['dir']: - raise SimplePrefsError('Error: Attempt to Set Preferences in unspecified directory') - for key in newprefs: - if key != 'dir': - if key in self.key2file: - filename = self.key2file[key] - filepath = os.path.join(self.prefdir,filename) - data = newprefs[key] - if data != None: - data = str(data) - if data == None or data == '': - if os.path.exists(filepath): - os.remove(filepath) - else: - try: - file(filepath,'wb').write(data) - except Exception, e: - pass - self.prefs = newprefs - return diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/stylexml2css.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/stylexml2css.py deleted file mode 100644 index daa108a..0000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/stylexml2css.py +++ /dev/null @@ -1,284 +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 - -debug = False - -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 - - - - -

Managing Adobe Digital Editions Keys

- - -

If you have upgraded from an earlier version of the plugin, any existing Adobe Digital Editions keys will have been automatically imported, so you might not need to do any more configuration. In addition, on Windows and Mac, the default Adobe Digital Editions key is added the first time the plugin is run. Continue reading for key generation and management instructions.

- -

Creating New Keys:

- -

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog prompting you to enter a key name for the default Adobe Digital Editions key.

-
    -
  • Unique Key Name: this is a unique name you choose to help you identify the key. This name will show in the list of configured keys.
  • -
- -

Click the OK button to create and store the Adobe Digital Editions key for the current installation of Adobe Digital Editions. Or Cancel if you don’t want to create the key.

-

New keys are checked against the current list of keys before being added, and duplicates are discarded.

- -

Deleting Keys:

- -

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

- -

Renaming Keys:

- -

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will promt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..

- -

Exporting Keys:

- -

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

- -

Linux Users: WINEPREFIX

- -

Under the list of keys, Linux users will see a text field labeled "WINEPREFIX". If you are use Adobe Digital Editions under Wine, and your wine installation containing Adobe Digital Editions isn't the default Wine installation, you may enter the full path to the correct Wine installation here. Leave blank if you are unsure.

- -

Importing Existing Keyfiles:

- -

At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing ‘.der’ key files. Key files might come from being exported from this or older plugins, or may have been generated using the adobekey.pyw script running under Wine on Linux systems.

- -

Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes will only be saved permanently when you click OK in the main configuration dialog.

- - - - - diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Barnes and Noble Key_Help.htm b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Barnes and Noble Key_Help.htm deleted file mode 100644 index 8f22f21..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Barnes and Noble Key_Help.htm +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - -Managing Barnes and Noble Keys - - - - - -

Managing Barnes and Noble Keys

- - -

If you have upgraded from an earlier version of the plugin, any existing Barnes and Noble keys will have been automatically imported, so you might not need to do any more configuration. Continue reading for key generation and management instructions.

- -

Changes at Barnes & Noble

- -

In mid-2014, Barnes & Noble changed the way they generated encryption keys. Instead of deriving the key from the user's name and credit card number, they started generating a random key themselves, sending that key through to devices when they connected to the Barnes & Noble servers. This means that most users will now find that no combination of their name and CC# will work in decrypting their recently downloaded ebooks.

- -

Someone commenting at Apprentice Alf's blog detailed a way to retrieve a new account key using the account's email address and password. This method has now been incorporated into the plugin. - -

Creating New Keys:

- -

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

-
    -
  • Unique Key Name: this is a unique name you choose to help you identify the key. This name will show in the list of configured keys. Choose something that will help you remember the data (account email address) it was created with.
  • -
  • B&N/nook account email address: This is the default email address for your Barnes and Noble/nook account. This email will not be stored anywhere on your computer or in calibre. It will only be used to fetch the account key that from the B&N server, and it is that key that will be stored in the preferences.
  • -
  • B&N/nook account password: this is the password for your Barnes and Noble/nook account. As with the email address, this will not be stored anywhere on your computer or in calibre. It will only be used to fetch the key from the B&N server.
  • -
- -

Click the OK button to create and store the generated key. Or Cancel if you don’t want to create a key.

-

New keys are checked against the current list of keys before being added, and duplicates are discarded.

- -

Deleting Keys:

- -

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

- -

Renaming Keys:

- -

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will promt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..

- -

Exporting Keys:

- -

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

- -

Importing Existing Keyfiles:

- -

At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing ‘.b64’ key files. Key files might come from being exported from this or older plugins, or may have been generated using the original i♥cabbages script, or you may have made it by following the instructions above.

- -

Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.

- -

NOOK Study

-

Books downloaded through NOOK Study may or may not use the key found using the above method. If a book is not decrypted successfully with any of the keys, the plugin will attempt to recover keys from the NOOK Study log file and use them.

- - - - - - diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_EInk Kindle Serial Number_Help.htm b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_EInk Kindle Serial Number_Help.htm deleted file mode 100644 index 5a4692d..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_EInk Kindle Serial Number_Help.htm +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - -Managing eInk Kindle serial numbers - - - - - -

Managing eInk Kindle serial numbers

- -

If you have upgraded from an earlier version of the plugin, any existing eInk Kindle serial numbers will have been automatically imported, so you might not need to do any more configuration.

- -

Please note that Kindle serial numbers are only valid keys for eInk Kindles like the Kindle Touch and PaperWhite. The Kindle Fire and Fire HD do not use their serial number for DRM and it is useless to enter those serial numbers.

- -

Creating New Kindle serial numbers:

- -

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering a new Kindle serial number.

-
    -
  • Eink Kindle Serial Number: this is the unique serial number of your device. It usually starts with a ‘B’ or a ‘9’ and is sixteen characters long. For a reference of where to find serial numbers and their ranges, please refer to this mobileread wiki page.
  • -
- -

Click the OK button to save the serial number. Or Cancel if you didn’t want to enter a serial number.

- -

Deleting Kindle serial numbers:

- -

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

- -

Once done creating/deleting serial numbers, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.

- - - - diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Help.htm b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Help.htm deleted file mode 100644 index 3fed9df..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Help.htm +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - -DeDRM Plugin Configuration - - - - - -

DeDRM Plugin (v6.3.0)

- -

This plugin removes DRM from ebooks when they are imported into calibre. If you already have DRMed ebooks in your calibre library, you will need to remove them and import them again.

- -

Installation

-

You have obviously managed to install the plugin, as otherwise you wouldn’t be reading this help file. However, you should also delete any older DeDRM plugins, as this DeDRM plugin replaces the five older plugins: Kindle and Mobipocket DeDRM (K4MobiDeDRM), Ignoble Epub DeDRM (ignobleepub), Inept Epub DeDRM (ineptepub), Inept PDF DeDRM (ineptepub) and eReader PDB 2 PML (eReaderPDB2PML).

- -

Configuration

-

On Windows and Mac, the keys for ebooks downloaded for Kindle for Mac/PC and Adobe Digital Editions are automatically generated. If all your DRMed ebooks can be opened and read in Kindle for Mac/PC and/or Adobe Digital Editions on the same computer on which you are running calibre, you do not need to do any configuration of this plugin. On Linux, keys for Kindle for PC and Adobe Digital Editions need to be generated separately (see the Linux section below)

- -

If you have other DRMed ebooks, you will need to enter extra configuration information. The buttons in this dialog will open individual configuration dialogs that will allow you to enter the needed information, depending on the type and source of your DRMed eBooks. Additional help on the information required is available in each of the the dialogs.

- -

If you have used previous versions of the various DeDRM plugins on this machine, you may find that some of the configuration dialogs already contain the information you entered through those previous plugins.

- -

When you have finished entering your configuration information, you must click the OK button to save it. If you click the Cancel button, all your changes in all the configuration dialogs will be lost.

- -

Troubleshooting:

- -

If you find that it’s not working for you , you can save a lot of time by trying to add the ebook to Calibre in debug mode. This will print out a lot of helpful info that can be copied into any online help requests.

- -

Open a command prompt (terminal window) and type "calibre-debug -g" (without the quotes). Calibre will launch, and you can can add the problem ebook the usual way. The debug info will be output to the original command prompt (terminal window). Copy the resulting output and paste it into the comment you make at my blog.

-

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

- -

Credits:

-
    -
  • The Dark Reverser for the Mobipocket and eReader scripts
  • -
  • i♥cabbages for the Adobe Digital Editions scripts
  • -
  • Skindle aka Bart Simpson for the Amazon Kindle for PC script
  • -
  • CMBDTC for Amazon Topaz DRM removal script
  • -
  • some_updates, clarknova and Bart Simpson for Amazon Topaz conversion scripts
  • -
  • DiapDealer for the first calibre plugin versions of the tools
  • -
  • some_updates, DiapDealer, Apprentice Alf and mdlnx for Amazon Kindle/Mobipocket tools
  • -
  • some_updates for the DeDRM all-in-one Python tool
  • -
  • Apprentice Alf for the DeDRM all-in-one AppleScript tool
  • -
  • Apprentice Alf for the DeDRM all-in-one calibre plugin
  • -
  • And probably many more.
  • -
- -

For additional help read the FAQs at Apprentice Harpers’s GitHub repository. You can ask questions in the comments section of the first post at Apprentice Alf's blog or raise an issue.

- -

Linux Systems Only

-

Generating decryption keys for Adobe Digital Editions and Kindle for PC

-

If you install Kindle for PC and/or Adobe Digital Editions in Wine, you will be able to download DRMed ebooks to them under Wine. To be able to remove the DRM, you will need to generate key files and add them in the plugin's customisation dialogs.

- -

To generate the key files you will need to install Python and PyCrypto under the same Wine setup as your Kindle for PC and/or Adobe Digital Editions installations. (Kindle for PC, Python and Pycrypto installation instructions in the ReadMe.)

- -

Once everything's installed under Wine, you'll need to run the adobekey.pyw script (for Adobe Digital Editions) and kindlekey.pyw (For Kindle for PC) using the python installation in your Wine system. The scripts can be found in Other_Tools/Key_Retrieval_Scripts.

- -

Each script will create a key file in the same folder as the script. Copy the key files to your Linux system and then load the key files using the Adobe Digital Editions ebooks dialog and the Kindle for Mac/PC ebooks dialog.

- - - - - diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Kindle for Android Key_Help.htm b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Kindle for Android Key_Help.htm deleted file mode 100644 index 65b11b4..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Kindle for Android Key_Help.htm +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - -Managing Kindle for Android Keys - - - - - -

Managing Kindle for Android Keys

- -

Amazon's Kindle for Android application uses an internal key equivalent to an eInk Kindle's serial number. Extracting that key is a little tricky, but worth it, as it then allows the DRM to be removed from any Kindle ebooks that have been downloaded to that Android device.

- -

Please note that it is not currently known whether the same applies to the Kindle application on the Kindle Fire and Fire HD.

- -

Getting the Kindle for Android backup file

- -

Obtain and install adb (Android Debug Bridge) on your computer. Details of how to do this are beyond the scope of this help file, but there are plenty of on-line guides.

-

Enable developer mode on your Android device. Again, look for an on-line guide for your device.

-

Once you have adb installed and your device in developer mode, connect your device to your computer with a USB cable and then open up a command line (Terminal on Mac OS X and cmd.exe on Windows) and enter "adb backup com.amazon.kindle" (without the quotation marks!) and press return. A file "backup.ab" should be created in your home directory. - -

Adding a Kindle for Android Key

- -

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog with two main controls. -

    -
  • Choose backup file: click this button and you will be prompted to find the backup.ab file you created earlier. Once selected the file will be processed to extract the decryption key, and if successful the file name will be displayed to the right of the button.
  • -
  • Unique Key Name: this is a unique name you choose to help you identify the key. This name will show in the list of Kindle for Android keys. Enter a name that will help you remember which device this key came from.
  • -
- -

Click the OK button to store the Kindle for Android key for the current list of Kindle for Android keys. Or click Cancel if you don’t want to store the key.

-

New keys are checked against the current list of keys before being added, and duplicates are discarded.

- -

Deleting Keys:

- -

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

- -

Renaming Keys:

- -

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will prompt you to enter a new name for the highlighted key in the list. Enter the new name for the key and click the OK button to use the new name, or Cancel to revert to the old name.

- -

Exporting Keys:

- -

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

- -

Importing Existing Keyfiles:

- -

At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import any ‘.k4a’ file you obtained by using the androidkindlekey.py script manually, or by exporting from another copy of calibre.

- - - - diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Kindle for Mac and PC Key_Help.htm b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Kindle for Mac and PC Key_Help.htm deleted file mode 100644 index 35f1a50..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Kindle for Mac and PC Key_Help.htm +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - -Managing Kindle for Mac/PC Keys - - - - - -

Managing Kindle for Mac/PC Keys

- - -

If you have upgraded from an earlier version of the plugin, any existing Kindle for Mac/PC keys will have been automatically imported, so you might not need to do any more configuration. In addition, on Windows and Mac, the default Kindle for Mac/PC key is added the first time the plugin is run. Continue reading for key generation and management instructions.

- -

Creating New Keys:

- -

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog prompting you to enter a key name for the default Kindle for Mac/PC key.

-
    -
  • Unique Key Name: this is a unique name you choose to help you identify the key. This name will show in the list of configured keys.
  • -
- -

Click the OK button to create and store the Kindle for Mac/PC key for the current installation of Kindle for Mac/PC. Or Cancel if you don’t want to create the key.

-

New keys are checked against the current list of keys before being added, and duplicates are discarded.

- -

Deleting Keys:

- -

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

- -

Renaming Keys:

- -

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will promt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..

- -

Exporting Keys:

- -

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

- -

Linux Users: WINEPREFIX

- -

Under the list of keys, Linux users will see a text field labeled "WINEPREFIX". If you are use Kindle for PC under Wine, and your wine installation containing Kindle for PC isn't the default Wine installation, you may enter the full path to the correct Wine installation here. Leave blank if you are unsure.

- -

Importing Existing Keyfiles:

- -

At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing ‘.k4i’ key files. Key files might come from being exported from this plugin, or may have been generated using the kindlekey.pyw script running under Wine on Linux systems.

- -

Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.

- - - - diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Mobipocket PID_Help.htm b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Mobipocket PID_Help.htm deleted file mode 100644 index 00aeeca..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Mobipocket PID_Help.htm +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - -Managing Mobipocket PIDs - - - - - -

Managing Mobipocket PIDs

- -

If you have upgraded from an earlier version of the plugin, any existing Mobipocket PIDs will have been automatically imported, so you might not need to do any more configuration.

- - -

Creating New Mobipocket PIDs:

- -

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

-
    -
  • PID: this is a PID used to decrypt your Mobipocket ebooks. It is eight or ten characters long. Mobipocket PIDs are usualy displayed in the About screen of your Mobipocket device.
  • -
- -

Click the OK button to save the PID. Or Cancel if you didn’t want to enter a PID.

- -

Deleting Mobipocket PIDs:

- -

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

- -

Once done creating/deleting PIDs, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.

- - - - diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_eReader Key_Help.htm b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_eReader Key_Help.htm deleted file mode 100644 index c1c78ad..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_eReader Key_Help.htm +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - -Managing eReader Keys - - - - - -

Managing eReader Keys

- -

If you have upgraded from an earlier version of the plugin, any existing eReader (Fictionwise ‘.pdb’) keys will have been automatically imported, so you might not need to do any more configuration. Continue reading for key generation and management instructions.

- -

Creating New Keys:

- -

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

-
    -
  • Unique Key Name: this is a unique name you choose to help you identify the key. This name will show in the list of configured keys. Choose something that will help you remember the data (name, cc#) it was created with.
  • -
  • Your Name: This is the name used by Fictionwise to generate your encryption key. Since Fictionwise has now closed down, you might not have easy access to this. It was often the name on the Credit Card used at Fictionwise.
  • -
  • Credit Card#: this is the default credit card number that was on file with Fictionwise at the time of download of the ebook to be de-DRMed. Just enter the last 8 digits of the number. As with the name, this number will not be stored anywhere on your computer or in calibre. It will only be used in the creation of the one-way hash/key that’s stored in the preferences.
  • -
- -

Click the OK button to create and store the generated key. Or Cancel if you don’t want to create a key.

-

New keys are checked against the current list of keys before being added, and duplicates are discarded.

- -

Deleting Keys:

- -

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

- -

Renaming Keys:

- -

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will promt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..

- -

Exporting Keys:

- -

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

- -

Importing Existing Keyfiles:

- -

At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing ‘.b63’ key files that have previously been exported.

- -

Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.

- - - - diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/__init__.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/__init__.py deleted file mode 100644 index 553687d..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/__init__.py +++ /dev/null @@ -1,647 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# __init__.py for DeDRM_plugin -# Copyright © 2008-2018 Apprentice Harper et al. - -__license__ = 'GPL v3' -__docformat__ = 'restructuredtext en' - - -# Released under the terms of the GNU General Public Licence, version 3 -# -# -# All credit given to i♥cabbages and The Dark Reverser for the original standalone scripts. -# We had the much easier job of converting them to a calibre plugin. -# -# This plugin is meant to decrypt eReader PDBs, Adobe Adept ePubs, Barnes & Noble ePubs, -# Adobe Adept PDFs, Amazon Kindle and Mobipocket files without having to -# install any dependencies... other than having calibre installed, of course. -# -# Configuration: -# Check out the plugin's configuration settings by clicking the "Customize plugin" -# button when you have the "DeDRM" plugin highlighted (under Preferences-> -# Plugins->File type plugins). Once you have the configuration dialog open, you'll -# see a Help link on the top right-hand side. -# -# Revision history: -# 6.0.0 - Initial release -# 6.0.1 - Bug Fixes for Windows App, Kindle for Mac and Windows Adobe Digital Editions -# 6.0.2 - Restored call to Wine to get Kindle for PC keys, added for ADE -# 6.0.3 - Fixes for Kindle for Mac and Windows non-ascii user names -# 6.0.4 - Fixes for stand-alone scripts and applications -# and pdb files in plugin and initial conversion of prefs. -# 6.0.5 - Fix a key issue -# 6.0.6 - Fix up an incorrect function call -# 6.0.7 - Error handling for incomplete PDF metadata -# 6.0.8 - Fixes a Wine key issue and topaz support -# 6.0.9 - Ported to work with newer versions of Calibre (moved to Qt5). Still supports older Qt4 versions. -# 6.1.0 - Fixed multiple books import problem and PDF import with no key problem -# 6.2.0 - Support for getting B&N key from nook Study log. Fix for UTF-8 filenames in Adobe ePubs. -# Fix for not copying needed files. Fix for getting default Adobe key for PDFs -# 6.2.1 - Fix for non-ascii Windows user names -# 6.2.2 - Added URL method for B&N/nook books -# 6.3.0 - Added in Kindle for Android serial number solution -# 6.3.1 - Version number bump for clarity -# 6.3.2 - Fixed Kindle for Android help file -# 6.3.3 - Bug fix for Kindle for PC support -# 6.3.4 - Fixes for Kindle for Android, Linux, and Kobo 3.17 -# 6.3.5 - Fixes for Linux, and Kobo 3.19 and more logging -# 6.3.6 - Fixes for ADE ePub and PDF introduced in 6.3.5 -# 6.4.0 - Updated for new Kindle for PC encryption -# 6.4.1 - Fix for some new tags in Topaz ebooks. -# 6.4.2 - Fix for more new tags in Topaz ebooks and very small Topaz ebooks -# 6.4.3 - Fix for error that only appears when not in debug mode -# Also includes fix for Macs with bonded ethernet ports -# 6.5.0 - Big update to Macintosh app -# Fix for some more 'new' tags in Topaz ebooks. -# Fix an error in wineutils.py -# 6.5.1 - Updated version number, added PDF check for DRM-free documents -# 6.5.2 - Another Topaz fix -# 6.5.3 - Warn about KFX files explicitly -# 6.5.4 - Mac App Fix, improve PDF decryption, handle latest tcl changes in ActivePython -# 6.5.5 - Finally a fix for the Windows non-ASCII user names. -# 6.6.0 - Add kfx and kfx-zip as supported file types (also invoke this plugin if the original -# imported format was azw8 since that may be converted to kfx) - - -""" -Decrypt DRMed ebooks. -""" - -PLUGIN_NAME = u"DeDRM" -PLUGIN_VERSION_TUPLE = (6, 6, 0) -PLUGIN_VERSION = u".".join([unicode(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, re -import time -import zipfile -import traceback -from zipfile import ZipFile - -class DeDRMError(Exception): - pass - -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 safely -# 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") - try: - self.stream.write(data) - self.stream.flush() - except: - # We can do nothing if a write fails - pass - def __getattr__(self, attr): - return getattr(self.stream, attr) - -class DeDRM(FileTypePlugin): - name = PLUGIN_NAME - description = u"Removes DRM from Amazon Kindle, Adobe Adept (including Kobo), Barnes & Noble, Mobipocket and eReader ebooks. Credit given to i♥cabbages and The Dark Reverser for the original stand-alone scripts." - supported_platforms = ['linux', 'osx', 'windows'] - author = u"Apprentice Alf, Aprentice Harper, The Dark Reverser and i♥cabbages" - version = PLUGIN_VERSION_TUPLE - minimum_calibre_version = (1, 0, 0) # Compiled python libraries cannot be imported in earlier versions. - file_types = set(['epub','pdf','pdb','prc','mobi','pobi','azw','azw1','azw3','azw4','azw8','tpz','kfx','kfx-zip']) - on_import = True - priority = 600 - - - def initialize(self): - """ - Dynamic modules can't be imported/loaded from a zipfile. - So this routine will extract the appropriate - library for the target OS and copy it to the 'alfcrypto' subdirectory of - calibre's configuration directory. That 'alfcrypto' directory is then - inserted into the syspath (as the very first entry) in the run function - so the CDLL stuff will work in the alfcrypto.py script. - - The extraction only happens once per version of the plugin - Also perform upgrade of preferences once per version - """ - try: - self.pluginsdir = os.path.join(config_dir,u"plugins") - if not os.path.exists(self.pluginsdir): - os.mkdir(self.pluginsdir) - self.maindir = os.path.join(self.pluginsdir,u"DeDRM") - if not os.path.exists(self.maindir): - os.mkdir(self.maindir) - self.helpdir = os.path.join(self.maindir,u"help") - if not os.path.exists(self.helpdir): - os.mkdir(self.helpdir) - self.alfdir = os.path.join(self.maindir,u"libraryfiles") - if not os.path.exists(self.alfdir): - os.mkdir(self.alfdir) - # only continue if we've never run this version of the plugin before - self.verdir = os.path.join(self.maindir,PLUGIN_VERSION) - if not os.path.exists(self.verdir): - if iswindows: - names = [u"alfcrypto.dll",u"alfcrypto64.dll"] - elif isosx: - names = [u"libalfcrypto.dylib"] - else: - names = [u"libalfcrypto32.so",u"libalfcrypto64.so",u"kindlekey.py",u"adobekey.py",u"subasyncio.py"] - lib_dict = self.load_resources(names) - print u"{0} v{1}: Copying needed library files from plugin's zip".format(PLUGIN_NAME, PLUGIN_VERSION) - - for entry, data in lib_dict.items(): - file_path = os.path.join(self.alfdir, entry) - try: - os.remove(file_path) - except: - pass - - try: - open(file_path,'wb').write(data) - except: - print u"{0} v{1}: Exception when copying needed library files".format(PLUGIN_NAME, PLUGIN_VERSION) - traceback.print_exc() - pass - - # convert old preferences, if necessary. - from calibre_plugins.dedrm.prefs import convertprefs - convertprefs() - - # mark that this version has been initialized - os.mkdir(self.verdir) - except Exception, e: - traceback.print_exc() - raise - - def ePubDecrypt(self,path_to_ebook): - # Create a TemporaryPersistent file to work with. - # Check original epub archive for zip errors. - import calibre_plugins.dedrm.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 \'{2}\' when checking zip archive".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0]) - raise Exception(e) - - # import the decryption keys - import calibre_plugins.dedrm.prefs as prefs - dedrmprefs = prefs.DeDRM_Prefs() - - # import the Barnes & Noble ePub handler - import calibre_plugins.dedrm.ignobleepub as ignobleepub - - - #check the book - if ignobleepub.ignobleBook(inf.name): - print u"{0} v{1}: “{2}” is a secure Barnes & Noble ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)) - - # Attempt to decrypt epub with each encryption key (generated or provided). - for keyname, userkey in dedrmprefs['bandnkeys'].items(): - keyname_masked = 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. - try: - result = ignobleepub.decryptBook(userkey, inf.name, of.name) - except: - print u"{0} v{1}: Exception when trying to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - result = 1 - - of.close() - - if result == 0: - # Decryption was successful. - # Return the modified PersistentTemporary file to calibre. - return of.name - - print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime) - - # perhaps we should see if we can get a key from a log file - print u"{0} v{1}: Looking for new NOOK Study Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - - # get the default NOOK Study keys - defaultkeys = [] - - try: - if iswindows or isosx: - from calibre_plugins.dedrm.ignoblekey import nookkeys - - defaultkeys = nookkeys() - else: # linux - from wineutils import WineGetKeys - - scriptpath = os.path.join(self.alfdir,u"ignoblekey.py") - defaultkeys = WineGetKeys(scriptpath, u".b64",dedrmprefs['adobewineprefix']) - - except: - print u"{0} v{1}: Exception when getting default NOOK Study Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - - newkeys = [] - for keyvalue in defaultkeys: - if keyvalue not in dedrmprefs['bandnkeys'].values(): - newkeys.append(keyvalue) - - if len(newkeys) > 0: - try: - for i,userkey in enumerate(newkeys): - print u"{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION) - - of = self.temporary_file(u".epub") - - # Give the user key, ebook and TemporaryPersistent file to the decryption function. - try: - result = ignobleepub.decryptBook(userkey, inf.name, of.name) - except: - print u"{0} v{1}: Exception when trying to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - result = 1 - - of.close() - - if result == 0: - # Decryption was a success - # Store the new successful key in the defaults - print u"{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION) - try: - dedrmprefs.addnamedvaluetoprefs('bandnkeys','nook_Study_key',keyvalue) - dedrmprefs.writeprefs() - print u"{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) - except: - print u"{0} v{1}: Exception saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - # Return the modified PersistentTemporary file to calibre. - return of.name - - print u"{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) - except Exception, e: - pass - - print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) - raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) - - # import the Adobe Adept ePub handler - import calibre_plugins.dedrm.ineptepub as ineptepub - - if ineptepub.adeptBook(inf.name): - print u"{0} v{1}: {2} is a secure Adobe Adept ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)) - - # Attempt to decrypt epub with each encryption key (generated or provided). - for keyname, userkeyhex in dedrmprefs['adeptkeys'].items(): - userkey = userkeyhex.decode('hex') - print u"{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname) - of = self.temporary_file(u".epub") - - # Give the user key, ebook and TemporaryPersistent file to the decryption function. - try: - result = ineptepub.decryptBook(userkey, inf.name, of.name) - except: - print u"{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - result = 1 - - try: - of.close() - except: - print u"{0} v{1}: Exception closing temporary file after {2:.1f} seconds. Ignored.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - - if result == 0: - # Decryption was successful. - # Return the modified PersistentTemporary file to calibre. - print u"{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime) - return of.name - - print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime) - - # perhaps we need to get a new default ADE key - print u"{0} v{1}: Looking for new default Adobe Digital Editions Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - - # get the default Adobe keys - defaultkeys = [] - - try: - if iswindows or isosx: - from calibre_plugins.dedrm.adobekey import adeptkeys - - defaultkeys = adeptkeys() - else: # linux - from wineutils import WineGetKeys - - scriptpath = os.path.join(self.alfdir,u"adobekey.py") - defaultkeys = WineGetKeys(scriptpath, u".der",dedrmprefs['adobewineprefix']) - - self.default_key = defaultkeys[0] - except: - print u"{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - self.default_key = u"" - - newkeys = [] - for keyvalue in defaultkeys: - if keyvalue.encode('hex') not in dedrmprefs['adeptkeys'].values(): - newkeys.append(keyvalue) - - if len(newkeys) > 0: - try: - for i,userkey in enumerate(newkeys): - print u"{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION) - of = self.temporary_file(u".epub") - - # Give the user key, ebook and TemporaryPersistent file to the decryption function. - try: - result = ineptepub.decryptBook(userkey, inf.name, of.name) - except: - print u"{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - result = 1 - - of.close() - - if result == 0: - # Decryption was a success - # Store the new successful key in the defaults - print u"{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION) - try: - dedrmprefs.addnamedvaluetoprefs('adeptkeys','default_key',keyvalue.encode('hex')) - dedrmprefs.writeprefs() - print u"{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) - except: - print u"{0} v{1}: Exception when saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - print u"{0} v{1}: Decrypted with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) - # Return the modified PersistentTemporary file to calibre. - return of.name - - print u"{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) - except Exception, e: - print u"{0} v{1}: Unexpected Exception trying a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - pass - - # Something went wrong with decryption. - print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) - raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)) - - # Not a Barnes & Noble nor an Adobe Adept - # Import the fixed epub. - print u"{0} v{1}: “{2}” is neither an Adobe Adept nor a Barnes & Noble encrypted ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)) - raise DeDRMError(u"{0} v{1}: Couldn't decrypt after {2:.1f} seconds. DRM free perhaps?".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)) - - def PDFDecrypt(self,path_to_ebook): - import calibre_plugins.dedrm.prefs as prefs - import calibre_plugins.dedrm.ineptpdf - - dedrmprefs = prefs.DeDRM_Prefs() - # Attempt to decrypt epub with each encryption key (generated or provided). - print u"{0} v{1}: {2} is a PDF ebook".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)) - for keyname, userkeyhex in dedrmprefs['adeptkeys'].items(): - userkey = userkeyhex.decode('hex') - print u"{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname) - of = self.temporary_file(u".pdf") - - # Give the user key, ebook and TemporaryPersistent file to the decryption function. - try: - result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name) - except: - print u"{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - result = 1 - - of.close() - - if result == 0: - # Decryption was successful. - # Return the modified PersistentTemporary file to calibre. - return of.name - - print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime) - - # perhaps we need to get a new default ADE key - print u"{0} v{1}: Looking for new default Adobe Digital Editions Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - - # get the default Adobe keys - defaultkeys = [] - - try: - if iswindows or isosx: - from calibre_plugins.dedrm.adobekey import adeptkeys - - defaultkeys = adeptkeys() - else: # linux - from wineutils import WineGetKeys - - scriptpath = os.path.join(self.alfdir,u"adobekey.py") - defaultkeys = WineGetKeys(scriptpath, u".der",dedrmprefs['adobewineprefix']) - - self.default_key = defaultkeys[0] - except: - print u"{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - self.default_key = u"" - - newkeys = [] - for keyvalue in defaultkeys: - if keyvalue.encode('hex') not in dedrmprefs['adeptkeys'].values(): - newkeys.append(keyvalue) - - if len(newkeys) > 0: - try: - for i,userkey in enumerate(newkeys): - print u"{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION) - of = self.temporary_file(u".pdf") - - # Give the user key, ebook and TemporaryPersistent file to the decryption function. - try: - result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name) - except: - print u"{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - result = 1 - - of.close() - - if result == 0: - # Decryption was a success - # Store the new successful key in the defaults - print u"{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION) - try: - dedrmprefs.addnamedvaluetoprefs('adeptkeys','default_key',keyvalue.encode('hex')) - dedrmprefs.writeprefs() - print u"{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) - except: - print u"{0} v{1}: Exception when saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - # Return the modified PersistentTemporary file to calibre. - return of.name - - print u"{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) - except Exception, e: - pass - - # Something went wrong with decryption. - print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) - raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) - - - def KindleMobiDecrypt(self,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 this import here so the custom libs can be - # extracted to the appropriate places beforehand these routines - # look for them. - import calibre_plugins.dedrm.prefs as prefs - import calibre_plugins.dedrm.k4mobidedrm - - dedrmprefs = prefs.DeDRM_Prefs() - pids = dedrmprefs['pids'] - serials = dedrmprefs['serials'] - for android_serials_list in dedrmprefs['androidkeys'].values(): - #print android_serials_list - serials.extend(android_serials_list) - #print serials - androidFiles = [] - kindleDatabases = dedrmprefs['kindlekeys'].items() - - try: - book = k4mobidedrm.GetDecryptedBook(path_to_ebook,kindleDatabases,androidFiles,serials,pids,self.starttime) - except Exception, e: - decoded = False - # perhaps we need to get a new default Kindle for Mac/PC key - defaultkeys = [] - print u"{0} v{1}: Failed to decrypt with error: {2}".format(PLUGIN_NAME, PLUGIN_VERSION,e.args[0]) - print u"{0} v{1}: Looking for new default Kindle Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - - try: - if iswindows or isosx: - from calibre_plugins.dedrm.kindlekey import kindlekeys - - defaultkeys = kindlekeys() - else: # linux - from wineutils import WineGetKeys - - scriptpath = os.path.join(self.alfdir,u"kindlekey.py") - defaultkeys = WineGetKeys(scriptpath, u".k4i",dedrmprefs['kindlewineprefix']) - except: - print u"{0} v{1}: Exception when getting default Kindle Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) - traceback.print_exc() - pass - - newkeys = {} - for i,keyvalue in enumerate(defaultkeys): - keyname = u"default_key_{0:d}".format(i+1) - if keyvalue not in dedrmprefs['kindlekeys'].values(): - newkeys[keyname] = keyvalue - if len(newkeys) > 0: - print u"{0} v{1}: Found {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), u"key" if len(newkeys)==1 else u"keys") - try: - book = k4mobidedrm.GetDecryptedBook(path_to_ebook,newkeys.items(),[],[],[],self.starttime) - decoded = True - # store the new successful keys in the defaults - print u"{0} v{1}: Saving {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), u"key" if len(newkeys)==1 else u"keys") - for keyvalue in newkeys.values(): - dedrmprefs.addnamedvaluetoprefs('kindlekeys','default_key',keyvalue) - dedrmprefs.writeprefs() - except Exception, e: - pass - if not decoded: - #if you reached here then no luck raise and exception - print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) - raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)) - - of = self.temporary_file(book.getBookExtension()) - book.getFile(of.name) - of.close() - book.cleanup() - return of.name - - - def eReaderDecrypt(self,path_to_ebook): - - import calibre_plugins.dedrm.prefs as prefs - import calibre_plugins.dedrm.erdr2pml - - dedrmprefs = prefs.DeDRM_Prefs() - # Attempt to decrypt epub with each encryption key (generated or provided). - for keyname, userkey in dedrmprefs['ereaderkeys'].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".pmlz") - - # Give the userkey, ebook and TemporaryPersistent file to the decryption function. - result = erdr2pml.decryptBook(path_to_ebook, of.name, True, userkey.decode('hex')) - - of.close() - - # Decryption was successful return the modified PersistentTemporary - # file to Calibre's import process. - if result == 0: - print u"{0} v{1}: Successfully decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime) - return of.name - - print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime) - - print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) - raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) - - - 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)) - self.starttime = time.time() - - booktype = os.path.splitext(path_to_ebook)[1].lower()[1:] - if booktype in ['prc','mobi','pobi','azw','azw1','azw3','azw4','tpz','kfx-zip']: - # Kindle/Mobipocket - decrypted_ebook = self.KindleMobiDecrypt(path_to_ebook) - elif booktype == 'pdb': - # eReader - decrypted_ebook = self.eReaderDecrypt(path_to_ebook) - pass - elif booktype == 'pdf': - # Adobe Adept PDF (hopefully) - decrypted_ebook = self.PDFDecrypt(path_to_ebook) - pass - elif booktype == 'epub': - # Adobe Adept or B&N ePub - decrypted_ebook = self.ePubDecrypt(path_to_ebook) - else: - print u"Unknown booktype {0}. Passing back to calibre unchanged".format(booktype) - return path_to_ebook - print u"{0} v{1}: Finished after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) - return decrypted_ebook - - def is_customizable(self): - # return true to allow customization via the Plugin->Preferences. - return True - - def config_widget(self): - import calibre_plugins.dedrm.config as config - return config.ConfigWidget(self.plugin_path, self.alfdir) - - def save_settings(self, config_widget): - config_widget.save_settings() diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/activitybar.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/activitybar.py deleted file mode 100644 index b21c01d..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/activitybar.py +++ /dev/null @@ -1,75 +0,0 @@ -import sys -import Tkinter -import Tkconstants - -class ActivityBar(Tkinter.Frame): - - def __init__(self, master, length=300, height=20, barwidth=15, interval=50, bg='white', fillcolor='orchid1',\ - bd=2, relief=Tkconstants.GROOVE, *args, **kw): - Tkinter.Frame.__init__(self, master, bg=bg, width=length, height=height, *args, **kw) - self._master = master - self._interval = interval - self._maximum = length - self._startx = 0 - self._barwidth = barwidth - self._bardiv = length / barwidth - if self._bardiv < 10: - self._bardiv = 10 - stopx = self._startx + self._barwidth - if stopx > self._maximum: - stopx = self._maximum - # self._canv = Tkinter.Canvas(self, bg=self['bg'], width=self['width'], height=self['height'],\ - # highlightthickness=0, relief='flat', bd=0) - self._canv = Tkinter.Canvas(self, bg=self['bg'], width=self['width'], height=self['height'],\ - highlightthickness=0, relief=relief, bd=bd) - self._canv.pack(fill='both', expand=1) - self._rect = self._canv.create_rectangle(0, 0, self._canv.winfo_reqwidth(), self._canv.winfo_reqheight(), fill=fillcolor, width=0) - - self._set() - self.bind('', self._update_coords) - self._running = False - - def _update_coords(self, event): - '''Updates the position of the rectangle inside the canvas when the size of - the widget gets changed.''' - # looks like we have to call update_idletasks() twice to make sure - # to get the results we expect - self._canv.update_idletasks() - self._maximum = self._canv.winfo_width() - self._startx = 0 - self._barwidth = self._maximum / self._bardiv - if self._barwidth < 2: - self._barwidth = 2 - stopx = self._startx + self._barwidth - if stopx > self._maximum: - stopx = self._maximum - self._canv.coords(self._rect, 0, 0, stopx, self._canv.winfo_height()) - self._canv.update_idletasks() - - def _set(self): - if self._startx < 0: - self._startx = 0 - if self._startx > self._maximum: - self._startx = self._startx % self._maximum - stopx = self._startx + self._barwidth - if stopx > self._maximum: - stopx = self._maximum - self._canv.coords(self._rect, self._startx, 0, stopx, self._canv.winfo_height()) - self._canv.update_idletasks() - - def start(self): - self._running = True - self.after(self._interval, self._step) - - def stop(self): - self._running = False - self._set() - - def _step(self): - if self._running: - stepsize = self._barwidth / 4 - if stepsize < 2: - stepsize = 2 - self._startx += stepsize - self._set() - self.after(self._interval, self._step) diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/adobekey.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/adobekey.py deleted file mode 100644 index 7fbd516..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/adobekey.py +++ /dev/null @@ -1,603 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# adobekey.pyw, version 6.0 -# Copyright © 2009-2010 i♥cabbages - -# Released under the terms of the GNU General Public Licence, version 3 -# - -# Modified 2010–2016 by several people - -# Windows users: Before running this program, you must first install Python. -# We recommend ActiveState Python 2.7.X for Windows (x86) from -# http://www.activestate.com/activepython/downloads. -# You must also install PyCrypto from -# http://www.voidspace.org.uk/python/modules.shtml#pycrypto -# (make certain to install the version for Python 2.7). -# Then save this script file as adobekey.pyw and double-click on it to run it. -# It will create a file named adobekey_1.der in in the same directory as the script. -# This is your Adobe Digital Editions user key. -# -# Mac OS X users: Save this script file as adobekey.pyw. You can run this -# program from the command line (python adobekey.pyw) or by double-clicking -# it when it has been associated with PythonLauncher. It will create a file -# named adobekey_1.der in the same directory as the script. -# This is your Adobe Digital Editions 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 - Revised to allow use in Plugins to eliminate need for duplicate code -# 5.7 - Unicode support added, renamed adobekey from ineptkey -# 5.8 - Added getkey interface for Windows DeDRM application -# 5.9 - moved unicode_argv call inside main for Windows DeDRM compatibility -# 6.0 - Work if TkInter is missing - -""" -Retrieve Adobe ADEPT user key. -""" - -__license__ = 'GPL v3' -__version__ = '6.0' - -import sys, os, struct, getopt - -# 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 '?'. So use shell32.GetCommandLineArgvW to get sys.argv - # as a list of Unicode strings and encode them as utf-8 - - from ctypes import POINTER, byref, cdll, c_int, windll - from ctypes.wintypes import LPCWSTR, LPWSTR - - GetCommandLineW = cdll.kernel32.GetCommandLineW - GetCommandLineW.argtypes = [] - GetCommandLineW.restype = LPCWSTR - - CommandLineToArgvW = windll.shell32.CommandLineToArgvW - CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] - CommandLineToArgvW.restype = POINTER(LPWSTR) - - cmd = GetCommandLineW() - argc = c_int(0) - argv = CommandLineToArgvW(cmd, byref(argc)) - if argc.value > 0: - # Remove Python executable and commands if present - start = argc.value - len(sys.argv) - return [argv[i] 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"adobekey.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 - -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 adeptkeys(): - 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) - device = winreg.QueryValueEx(regkey, 'key')[0] - except WindowsError: - raise ADEPTError("Adobe Digital Editions not activated") - 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])] - #print "found key:",userkey.encode('hex') - keys.append(userkey) - if len(keys) == 0: - raise ADEPTError('Could not locate privateLicenseKey') - print u"Found {0:d} keys".format(len(keys)) - 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(): - import warnings - warnings.filterwarnings('ignore', category=FutureWarning) - - 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) - ActDatPath = "activation.dat" - 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 adeptkeys(): - actpath = findActivationDat() - if actpath is None: - raise ADEPTError("Could not find ADE activation.dat file.") - 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 adeptkeys(): - raise ADEPTError("This script only supports Windows and Mac OS X.") - return [] - -# interface for Python DeDRM -def getkey(outpath): - keys = adeptkeys() - if len(keys) > 0: - if not os.path.isdir(outpath): - outfile = outpath - with file(outfile, 'wb') as keyfileout: - keyfileout.write(keys[0]) - print u"Saved a key to {0}".format(outfile) - else: - keycount = 0 - for key in keys: - while True: - keycount += 1 - outfile = os.path.join(outpath,u"adobekey_{0:d}.der".format(keycount)) - if not os.path.exists(outfile): - break - with file(outfile, 'wb') as keyfileout: - keyfileout.write(key) - print u"Saved a key to {0}".format(outfile) - return True - return False - -def usage(progname): - print u"Finds, decrypts and saves the default Adobe Adept encryption key(s)." - print u"Keys are saved to the current directory, or a specified output directory." - print u"If a file name is passed instead of a directory, only the first key is saved, in that file." - print u"Usage:" - print u" {0:s} [-h] []".format(progname) - -def cli_main(): - sys.stdout=SafeUnbuffered(sys.stdout) - sys.stderr=SafeUnbuffered(sys.stderr) - argv=unicode_argv() - progname = os.path.basename(argv[0]) - print u"{0} v{1}\nCopyright © 2009-2013 i♥cabbages and Apprentice Alf".format(progname,__version__) - - try: - opts, args = getopt.getopt(argv[1:], "h") - except getopt.GetoptError, err: - print u"Error in options or arguments: {0}".format(err.args[0]) - usage(progname) - sys.exit(2) - - for o, a in opts: - if o == "-h": - usage(progname) - sys.exit(0) - - if len(args) > 1: - usage(progname) - sys.exit(2) - - if len(args) == 1: - # save to the specified file or directory - outpath = args[0] - if not os.path.isabs(outpath): - outpath = os.path.abspath(outpath) - else: - # save to the same directory as the script - outpath = os.path.dirname(argv[0]) - - # make sure the outpath is the - outpath = os.path.realpath(os.path.normpath(outpath)) - - keys = adeptkeys() - if len(keys) > 0: - if not os.path.isdir(outpath): - outfile = outpath - with file(outfile, 'wb') as keyfileout: - keyfileout.write(keys[0]) - print u"Saved a key to {0}".format(outfile) - else: - keycount = 0 - for key in keys: - while True: - keycount += 1 - outfile = os.path.join(outpath,u"adobekey_{0:d}.der".format(keycount)) - if not os.path.exists(outfile): - break - with file(outfile, 'wb') as keyfileout: - keyfileout.write(key) - print u"Saved a key to {0}".format(outfile) - else: - print u"Could not retrieve Adobe Adept key." - return 0 - - -def gui_main(): - try: - import Tkinter - import Tkconstants - import tkMessageBox - import traceback - except: - return cli_main() - - class ExceptionDialog(Tkinter.Frame): - def __init__(self, root, text): - Tkinter.Frame.__init__(self, root, border=5) - 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) - - - argv=unicode_argv() - root = Tkinter.Tk() - root.withdraw() - progpath, progname = os.path.split(argv[0]) - success = False - try: - keys = adeptkeys() - keycount = 0 - for key in keys: - while True: - keycount += 1 - outfile = os.path.join(progpath,u"adobekey_{0:d}.der".format(keycount)) - if not os.path.exists(outfile): - break - - with file(outfile, 'wb') as keyfileout: - keyfileout.write(key) - success = True - tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile)) - except ADEPTError, e: - tkMessageBox.showerror(progname, u"Error: {0}".format(str(e))) - except Exception: - root.wm_state('normal') - root.title(progname) - text = traceback.format_exc() - ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1) - root.mainloop() - if not success: - return 1 - return 0 - -if __name__ == '__main__': - if len(sys.argv) > 1: - sys.exit(cli_main()) - sys.exit(gui_main()) diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/aescbc.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/aescbc.py deleted file mode 100644 index 5667511..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/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/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/alfcrypto.dll b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/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/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/alfcrypto.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/alfcrypto.py deleted file mode 100644 index 036ba10..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/alfcrypto.py +++ /dev/null @@ -1,301 +0,0 @@ -#!/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 - 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' - - # hard code to local location for libalfcrypto - libalfcrypto = os.path.join(sys.path[0],name_of_lib) - if not os.path.isfile(libalfcrypto): - libalfcrypto = os.path.join(sys.path[0], 'lib', name_of_lib) - if not os.path.isfile(libalfcrypto): - libalfcrypto = os.path.join('.',name_of_lib) - if not os.path.isfile(libalfcrypto): - raise Exception('libalfcrypto not found at %s' % libalfcrypto) - - libalfcrypto = CDLL(libalfcrypto) - - 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 u"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: - 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])) - 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 - - print u"Using Library AlfCrypto Python" - 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/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/alfcrypto64.dll b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/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/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/alfcrypto_src.zip b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/alfcrypto_src.zip deleted file mode 100644 index 269810cfa24541f8be10050e7192fb6211a45a98..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17393 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 VE;* 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"kindlekey.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 - -STORAGE = u"backup.ab" -STORAGE1 = u"AmazonSecureStorage.xml" -STORAGE2 = u"map_data_storage.db" - -class AndroidObfuscation(object): - '''AndroidObfuscation - For the key, it's written in java, and run in android dalvikvm - ''' - - key = a2b_hex('0176e04c9408b1702d90be333fd53523') - - def encrypt(self, plaintext): - cipher = self._get_cipher() - padding = len(self.key) - len(plaintext) % len(self.key) - plaintext += chr(padding) * padding - return b2a_hex(cipher.encrypt(plaintext)) - - def decrypt(self, ciphertext): - cipher = self._get_cipher() - plaintext = cipher.decrypt(a2b_hex(ciphertext)) - return plaintext[:-ord(plaintext[-1])] - - def _get_cipher(self): - try: - from Crypto.Cipher import AES - return AES.new(self.key) - except ImportError: - from aescbc import AES, noPadding - return AES(self.key, padding=noPadding()) - -class AndroidObfuscationV2(AndroidObfuscation): - '''AndroidObfuscationV2 - ''' - - count = 503 - password = 'Thomsun was here!' - - def __init__(self, salt): - key = self.password + salt - for _ in range(self.count): - key = md5(key).digest() - self.key = key[:8] - self.iv = key[8:16] - - def _get_cipher(self): - try : - from Crypto.Cipher import DES - return DES.new(self.key, DES.MODE_CBC, self.iv) - except ImportError: - from python_des import Des, CBC - return Des(self.key, CBC, self.iv) - -def parse_preference(path): - ''' parse android's shared preference xml ''' - storage = {} - read = open(path) - for line in read: - line = line.strip() - # value - if line.startswith(' 0: - dsns.append(userdata_utf8) - except: - print "Error getting one of the device serial name keys" - traceback.print_exc() - pass - dsns = list(set(dsns)) - - cursor.execute('''select userdata_value from userdata where userdata_key like '%/%kindle.account.tokens%' ''') - userdata_keys = cursor.fetchall() - tokens = [] - for userdata_row in userdata_keys: - try: - if userdata_row and userdata_row[0]: - userdata_utf8 = userdata_row[0].encode('utf8') - if len(userdata_utf8) > 0: - tokens.append(userdata_utf8) - except: - print "Error getting one of the account token keys" - traceback.print_exc() - pass - tokens = list(set(tokens)) - - serials = [] - for x in dsns: - serials.append(x) - for y in tokens: - serials.append('%s%s' % (x, y)) - for y in tokens: - serials.append(y) - return serials - -def get_serials(path=STORAGE): - '''get serials from files in from android backup.ab - backup.ab can be get using adb command: - shell> adb backup com.amazon.kindle - or from individual files if they're passed. - ''' - if not os.path.isfile(path): - return [] - - basename = os.path.basename(path) - if basename == STORAGE1: - return get_serials1(path) - elif basename == STORAGE2: - return get_serials2(path) - - output = None - try : - read = open(path, 'rb') - head = read.read(24) - if head[:14] == 'ANDROID BACKUP': - output = StringIO(zlib.decompress(read.read())) - except Exception: - pass - finally: - read.close() - - if not output: - return [] - - serials = [] - tar = tarfile.open(fileobj=output) - for member in tar.getmembers(): - if member.name.strip().endswith(STORAGE1): - write = tempfile.NamedTemporaryFile(mode='wb', delete=False) - write.write(tar.extractfile(member).read()) - write.close() - write_path = os.path.abspath(write.name) - serials.extend(get_serials1(write_path)) - os.remove(write_path) - elif member.name.strip().endswith(STORAGE2): - write = tempfile.NamedTemporaryFile(mode='wb', delete=False) - write.write(tar.extractfile(member).read()) - write.close() - write_path = os.path.abspath(write.name) - serials.extend(get_serials2(write_path)) - os.remove(write_path) - return list(set(serials)) - -__all__ = [ 'get_serials', 'getkey'] - -# procedure for CLI and GUI interfaces -# returns single or multiple keys (one per line) in the specified file -def getkey(outfile, inpath): - keys = get_serials(inpath) - if len(keys) > 0: - with file(outfile, 'w') as keyfileout: - for key in keys: - keyfileout.write(key) - keyfileout.write("\n") - return True - return False - - -def usage(progname): - print u"Decrypts the serial number(s) of Kindle For Android from Android backup or file" - print u"Get backup.ab file using adb backup com.amazon.kindle for Android 4.0+." - print u"Otherwise extract AmazonSecureStorage.xml from /data/data/com.amazon.kindle/shared_prefs/AmazonSecureStorage.xml" - print u"Or map_data_storage.db from /data/data/com.amazon.kindle/databases/map_data_storage.db" - print u"" - print u"Usage:" - print u" {0:s} [-h] [-b ] []".format(progname) - - -def cli_main(): - sys.stdout=SafeUnbuffered(sys.stdout) - sys.stderr=SafeUnbuffered(sys.stderr) - argv=unicode_argv() - progname = os.path.basename(argv[0]) - print u"{0} v{1}\nCopyright © 2010-2015 Thom, some_updates, Apprentice Alf and Apprentice Harper".format(progname,__version__) - - try: - opts, args = getopt.getopt(argv[1:], "hb:") - except getopt.GetoptError, err: - usage(progname) - print u"\nError in options or arguments: {0}".format(err.args[0]) - return 2 - - inpath = "" - for o, a in opts: - if o == "-h": - usage(progname) - return 0 - if o == "-b": - inpath = a - - if len(args) > 1: - usage(progname) - return 2 - - if len(args) == 1: - # save to the specified file or directory - outfile = args[0] - if not os.path.isabs(outfile): - outfile = os.path.join(os.path.dirname(argv[0]),outfile) - outfile = os.path.abspath(outfile) - if os.path.isdir(outfile): - outfile = os.path.join(os.path.dirname(argv[0]),"androidkindlekey.k4a") - else: - # save to the same directory as the script - outfile = os.path.join(os.path.dirname(argv[0]),"androidkindlekey.k4a") - - # make sure the outpath is OK - outfile = os.path.realpath(os.path.normpath(outfile)) - - if not os.path.isfile(inpath): - usage(progname) - print u"\n{0:s} file not found".format(inpath) - return 2 - - if getkey(outfile, inpath): - print u"\nSaved Kindle for Android key to {0}".format(outfile) - else: - print u"\nCould not retrieve Kindle for Android key." - return 0 - - -def gui_main(): - try: - import Tkinter - import Tkconstants - import tkMessageBox - import tkFileDialog - except: - print "Tkinter not installed" - return cli_main() - - class DecryptionDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - self.status = Tkinter.Label(self, text=u"Select backup.ab file") - 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"Backup file").grid(row=0, column=0) - self.keypath = Tkinter.Entry(body, width=40) - self.keypath.grid(row=0, column=1, sticky=sticky) - self.keypath.insert(2, u"backup.ab") - button = Tkinter.Button(body, text=u"...", command=self.get_keypath) - button.grid(row=0, column=2) - buttons = Tkinter.Frame(self) - buttons.pack() - button2 = Tkinter.Button( - buttons, text=u"Extract", width=10, command=self.generate) - button2.pack(side=Tkconstants.LEFT) - Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) - button3 = Tkinter.Button( - buttons, text=u"Quit", width=10, command=self.quit) - button3.pack(side=Tkconstants.RIGHT) - - def get_keypath(self): - keypath = tkFileDialog.askopenfilename( - parent=None, title=u"Select backup.ab file", - defaultextension=u".ab", - filetypes=[('adb backup com.amazon.kindle', '.ab'), - ('All Files', '.*')]) - if keypath: - keypath = os.path.normpath(keypath) - self.keypath.delete(0, Tkconstants.END) - self.keypath.insert(0, keypath) - return - - def generate(self): - inpath = self.keypath.get() - self.status['text'] = u"Getting key..." - try: - keys = get_serials(inpath) - keycount = 0 - for key in keys: - while True: - keycount += 1 - outfile = os.path.join(progpath,u"kindlekey{0:d}.k4a".format(keycount)) - if not os.path.exists(outfile): - break - - with file(outfile, 'w') as keyfileout: - keyfileout.write(key) - success = True - tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile)) - except Exception, e: - self.status['text'] = u"Error: {0}".format(e.args[0]) - return - self.status['text'] = u"Select backup.ab file" - - argv=unicode_argv() - progpath, progname = os.path.split(argv[0]) - root = Tkinter.Tk() - root.title(u"Kindle for Android Key Extraction 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.exit(cli_main()) - sys.exit(gui_main()) diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/argv_utils.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/argv_utils.py deleted file mode 100644 index 85ffaa4..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/argv_utils.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import sys, os -import locale -import codecs - -# get sys.argv arguments and encode them into utf-8 -def unicode_argv(): - if sys.platform.startswith('win'): - # 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"DeDRM.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] - - -def add_cp65001_codec(): - try: - codecs.lookup('cp65001') - except LookupError: - codecs.register( - lambda name: name == 'cp65001' and codecs.lookup('utf-8') or None) - return - - -def set_utf8_default_encoding(): - if sys.getdefaultencoding() == 'utf-8': - return - - # Regenerate setdefaultencoding. - reload(sys) - sys.setdefaultencoding('utf-8') - - for attr in dir(locale): - if attr[0:3] != 'LC_': - continue - aref = getattr(locale, attr) - try: - locale.setlocale(aref, '') - except locale.Error: - continue - try: - lang = locale.getlocale(aref)[0] - except (TypeError, ValueError): - continue - if lang: - try: - locale.setlocale(aref, (lang, 'UTF-8')) - except locale.Error: - os.environ[attr] = lang + '.UTF-8' - try: - locale.setlocale(locale.LC_ALL, '') - except locale.Error: - pass - return - - diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/askfolder_ed.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/askfolder_ed.py deleted file mode 100644 index a4a2ae0..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/askfolder_ed.py +++ /dev/null @@ -1,211 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab - -# to work around tk_chooseDirectory not properly returning unicode paths on Windows -# need to use a dialog that can be hacked up to actually return full unicode paths -# originally based on AskFolder from EasyDialogs for Windows but modified to fix it -# to actually use unicode for path - -# The original license for EasyDialogs is as follows -# -# Copyright (c) 2003-2005 Jimmy Retzlaff -# -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# 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 OR COPYRIGHT HOLDERS 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. - -""" -AskFolder(...) -- Ask the user to select a folder Windows specific -""" - -import os - -import ctypes -from ctypes import POINTER, byref, cdll, c_int, windll -from ctypes.wintypes import LPCWSTR, LPWSTR -import ctypes.wintypes as wintypes - - -__all__ = ['AskFolder'] - -# Load required Windows DLLs -ole32 = ctypes.windll.ole32 -shell32 = ctypes.windll.shell32 -user32 = ctypes.windll.user32 - - -# Windows Constants -BFFM_INITIALIZED = 1 -BFFM_SETOKTEXT = 1129 -BFFM_SETSELECTIONA = 1126 -BFFM_SETSELECTIONW = 1127 -BIF_EDITBOX = 16 -BS_DEFPUSHBUTTON = 1 -CB_ADDSTRING = 323 -CB_GETCURSEL = 327 -CB_SETCURSEL = 334 -CDM_SETCONTROLTEXT = 1128 -EM_GETLINECOUNT = 186 -EM_GETMARGINS = 212 -EM_POSFROMCHAR = 214 -EM_SETSEL = 177 -GWL_STYLE = -16 -IDC_STATIC = -1 -IDCANCEL = 2 -IDNO = 7 -IDOK = 1 -IDYES = 6 -MAX_PATH = 260 -OFN_ALLOWMULTISELECT = 512 -OFN_ENABLEHOOK = 32 -OFN_ENABLESIZING = 8388608 -OFN_ENABLETEMPLATEHANDLE = 128 -OFN_EXPLORER = 524288 -OFN_OVERWRITEPROMPT = 2 -OPENFILENAME_SIZE_VERSION_400 = 76 -PBM_GETPOS = 1032 -PBM_SETMARQUEE = 1034 -PBM_SETPOS = 1026 -PBM_SETRANGE = 1025 -PBM_SETRANGE32 = 1030 -PBS_MARQUEE = 8 -PM_REMOVE = 1 -SW_HIDE = 0 -SW_SHOW = 5 -SW_SHOWNORMAL = 1 -SWP_NOACTIVATE = 16 -SWP_NOMOVE = 2 -SWP_NOSIZE = 1 -SWP_NOZORDER = 4 -VER_PLATFORM_WIN32_NT = 2 -WM_COMMAND = 273 -WM_GETTEXT = 13 -WM_GETTEXTLENGTH = 14 -WM_INITDIALOG = 272 -WM_NOTIFY = 78 - -# Windows function prototypes -BrowseCallbackProc = ctypes.WINFUNCTYPE(ctypes.c_int, wintypes.HWND, ctypes.c_uint, wintypes.LPARAM, wintypes.LPARAM) - -# Windows types -LPCTSTR = ctypes.c_char_p -LPTSTR = ctypes.c_char_p -LPVOID = ctypes.c_voidp -TCHAR = ctypes.c_char - -class BROWSEINFO(ctypes.Structure): - _fields_ = [ - ("hwndOwner", wintypes.HWND), - ("pidlRoot", LPVOID), - ("pszDisplayName", LPTSTR), - ("lpszTitle", LPCTSTR), - ("ulFlags", ctypes.c_uint), - ("lpfn", BrowseCallbackProc), - ("lParam", wintypes.LPARAM), - ("iImage", ctypes.c_int) - ] - - -# Utilities -def CenterWindow(hwnd): - desktopRect = GetWindowRect(user32.GetDesktopWindow()) - myRect = GetWindowRect(hwnd) - x = width(desktopRect) // 2 - width(myRect) // 2 - y = height(desktopRect) // 2 - height(myRect) // 2 - user32.SetWindowPos(hwnd, 0, - desktopRect.left + x, - desktopRect.top + y, - 0, 0, - SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER - ) - - -def GetWindowRect(hwnd): - rect = wintypes.RECT() - user32.GetWindowRect(hwnd, ctypes.byref(rect)) - return rect - -def width(rect): - return rect.right-rect.left - -def height(rect): - return rect.bottom-rect.top - - -def AskFolder( - message=None, - version=None, - defaultLocation=None, - location=None, - windowTitle=None, - actionButtonLabel=None, - cancelButtonLabel=None, - multiple=None): - """Display a dialog asking the user for select a folder. - modified to use unicode strings as much as possible - returns unicode path - """ - - def BrowseCallback(hwnd, uMsg, lParam, lpData): - if uMsg == BFFM_INITIALIZED: - if actionButtonLabel: - label = unicode(actionButtonLabel, errors='replace') - user32.SendMessageW(hwnd, BFFM_SETOKTEXT, 0, label) - if cancelButtonLabel: - label = unicode(cancelButtonLabel, errors='replace') - cancelButton = user32.GetDlgItem(hwnd, IDCANCEL) - if cancelButton: - user32.SetWindowTextW(cancelButton, label) - if windowTitle: - title = unicode(windowTitle, erros='replace') - user32.SetWindowTextW(hwnd, title) - if defaultLocation: - user32.SendMessageW(hwnd, BFFM_SETSELECTIONW, 1, defaultLocation.replace('/', '\\')) - if location: - x, y = location - desktopRect = wintypes.RECT() - user32.GetWindowRect(0, ctypes.byref(desktopRect)) - user32.SetWindowPos(hwnd, 0, - desktopRect.left + x, - desktopRect.top + y, 0, 0, - SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER) - else: - CenterWindow(hwnd) - return 0 - - # This next line is needed to prevent gc of the callback - callback = BrowseCallbackProc(BrowseCallback) - - browseInfo = BROWSEINFO() - browseInfo.pszDisplayName = ctypes.c_char_p('\0' * (MAX_PATH+1)) - browseInfo.lpszTitle = message - browseInfo.lpfn = callback - - pidl = shell32.SHBrowseForFolder(ctypes.byref(browseInfo)) - if not pidl: - result = None - else: - path = LPCWSTR(u" " * (MAX_PATH+1)) - shell32.SHGetPathFromIDListW(pidl, path) - ole32.CoTaskMemFree(pidl) - result = path.value - return result - - - - diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/config.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/config.py deleted file mode 100644 index 3a56e44..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/config.py +++ /dev/null @@ -1,1021 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -__license__ = 'GPL v3' - -# Standard Python modules. -import os, traceback, json - -# PyQT4 modules (part of calibre). -try: - from PyQt5.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, - QGroupBox, QPushButton, QListWidget, QListWidgetItem, - QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl) -except ImportError: - from PyQt4.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, - QGroupBox, QPushButton, QListWidget, QListWidgetItem, - QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl) -try: - from PyQt5 import Qt as QtGui -except ImportError: - from PyQt4 import QtGui - -from zipfile import ZipFile - -# calibre modules and constants. -from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url, - choose_dir, choose_files, choose_save_file) -from calibre.utils.config import dynamic, config_dir, JSONConfig -from calibre.constants import iswindows, isosx - -# modules from this plugin's zipfile. -from calibre_plugins.dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION -from calibre_plugins.dedrm.__init__ import RESOURCE_NAME as help_file_name -from calibre_plugins.dedrm.utilities import uStrCmp - -import calibre_plugins.dedrm.prefs as prefs -import calibre_plugins.dedrm.androidkindlekey as androidkindlekey - -class ConfigWidget(QWidget): - def __init__(self, plugin_path, alfdir): - QWidget.__init__(self) - - self.plugin_path = plugin_path - self.alfdir = alfdir - - # get the prefs - self.dedrmprefs = prefs.DeDRM_Prefs() - - # make a local copy - self.tempdedrmprefs = {} - self.tempdedrmprefs['bandnkeys'] = self.dedrmprefs['bandnkeys'].copy() - self.tempdedrmprefs['adeptkeys'] = self.dedrmprefs['adeptkeys'].copy() - self.tempdedrmprefs['ereaderkeys'] = self.dedrmprefs['ereaderkeys'].copy() - self.tempdedrmprefs['kindlekeys'] = self.dedrmprefs['kindlekeys'].copy() - self.tempdedrmprefs['androidkeys'] = self.dedrmprefs['androidkeys'].copy() - self.tempdedrmprefs['pids'] = list(self.dedrmprefs['pids']) - self.tempdedrmprefs['serials'] = list(self.dedrmprefs['serials']) - self.tempdedrmprefs['adobewineprefix'] = self.dedrmprefs['adobewineprefix'] - self.tempdedrmprefs['kindlewineprefix'] = self.dedrmprefs['kindlewineprefix'] - - # 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. - help_label = QLabel('Plugin Help', self) - help_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard) - help_label.setAlignment(Qt.AlignRight) - help_label.linkActivated.connect(self.help_link_activated) - help_layout.addWidget(help_label) - - keys_group_box = QGroupBox(_('Configuration:'), self) - layout.addWidget(keys_group_box) - keys_group_box_layout = QHBoxLayout() - keys_group_box.setLayout(keys_group_box_layout) - - - button_layout = QVBoxLayout() - keys_group_box_layout.addLayout(button_layout) - self.bandn_button = QtGui.QPushButton(self) - self.bandn_button.setToolTip(_(u"Click to manage keys for Barnes and Noble ebooks")) - self.bandn_button.setText(u"Barnes and Noble ebooks") - self.bandn_button.clicked.connect(self.bandn_keys) - self.kindle_android_button = QtGui.QPushButton(self) - self.kindle_android_button.setToolTip(_(u"Click to manage keys for Kindle for Android ebooks")) - self.kindle_android_button.setText(u"Kindle for Android ebooks") - self.kindle_android_button.clicked.connect(self.kindle_android) - self.kindle_serial_button = QtGui.QPushButton(self) - self.kindle_serial_button.setToolTip(_(u"Click to manage eInk Kindle serial numbers for Kindle ebooks")) - self.kindle_serial_button.setText(u"eInk Kindle ebooks") - self.kindle_serial_button.clicked.connect(self.kindle_serials) - self.kindle_key_button = QtGui.QPushButton(self) - self.kindle_key_button.setToolTip(_(u"Click to manage keys for Kindle for Mac/PC ebooks")) - self.kindle_key_button.setText(u"Kindle for Mac/PC ebooks") - self.kindle_key_button.clicked.connect(self.kindle_keys) - self.adept_button = QtGui.QPushButton(self) - self.adept_button.setToolTip(_(u"Click to manage keys for Adobe Digital Editions ebooks")) - self.adept_button.setText(u"Adobe Digital Editions ebooks") - self.adept_button.clicked.connect(self.adept_keys) - self.mobi_button = QtGui.QPushButton(self) - self.mobi_button.setToolTip(_(u"Click to manage PIDs for Mobipocket ebooks")) - self.mobi_button.setText(u"Mobipocket ebooks") - self.mobi_button.clicked.connect(self.mobi_keys) - self.ereader_button = QtGui.QPushButton(self) - self.ereader_button.setToolTip(_(u"Click to manage keys for eReader ebooks")) - self.ereader_button.setText(u"eReader ebooks") - self.ereader_button.clicked.connect(self.ereader_keys) - button_layout.addWidget(self.kindle_serial_button) - button_layout.addWidget(self.kindle_android_button) - button_layout.addWidget(self.bandn_button) - button_layout.addWidget(self.mobi_button) - button_layout.addWidget(self.ereader_button) - button_layout.addWidget(self.adept_button) - button_layout.addWidget(self.kindle_key_button) - - self.resize(self.sizeHint()) - - def kindle_serials(self): - d = ManageKeysDialog(self,u"EInk Kindle Serial Number",self.tempdedrmprefs['serials'], AddSerialDialog) - d.exec_() - - def kindle_android(self): - d = ManageKeysDialog(self,u"Kindle for Android Key",self.tempdedrmprefs['androidkeys'], AddAndroidDialog, 'k4a') - d.exec_() - - def kindle_keys(self): - if isosx or iswindows: - d = ManageKeysDialog(self,u"Kindle for Mac and PC Key",self.tempdedrmprefs['kindlekeys'], AddKindleDialog, 'k4i') - else: - # linux - d = ManageKeysDialog(self,u"Kindle for Mac and PC Key",self.tempdedrmprefs['kindlekeys'], AddKindleDialog, 'k4i', self.tempdedrmprefs['kindlewineprefix']) - d.exec_() - self.tempdedrmprefs['kindlewineprefix'] = d.getwineprefix() - - def adept_keys(self): - if isosx or iswindows: - d = ManageKeysDialog(self,u"Adobe Digital Editions Key",self.tempdedrmprefs['adeptkeys'], AddAdeptDialog, 'der') - else: - # linux - d = ManageKeysDialog(self,u"Adobe Digital Editions Key",self.tempdedrmprefs['adeptkeys'], AddAdeptDialog, 'der', self.tempdedrmprefs['adobewineprefix']) - d.exec_() - self.tempdedrmprefs['adobewineprefix'] = d.getwineprefix() - - def mobi_keys(self): - d = ManageKeysDialog(self,u"Mobipocket PID",self.tempdedrmprefs['pids'], AddPIDDialog) - d.exec_() - - def bandn_keys(self): - d = ManageKeysDialog(self,u"Barnes and Noble Key",self.tempdedrmprefs['bandnkeys'], AddBandNKeyDialog, 'b64') - d.exec_() - - def ereader_keys(self): - d = ManageKeysDialog(self,u"eReader Key",self.tempdedrmprefs['ereaderkeys'], AddEReaderDialog, 'b63') - d.exec_() - - def help_link_activated(self, url): - def get_help_file_resource(): - # Copy the HTML helpfile to the plugin directory each time the - # link is clicked in case the helpfile is updated in newer plugins. - file_path = os.path.join(config_dir, u"plugins", u"DeDRM", u"help", help_file_name) - with open(file_path,'w') as f: - f.write(self.load_resource(help_file_name)) - return file_path - url = 'file:///' + get_help_file_resource() - open_url(QUrl(url)) - - def save_settings(self): - self.dedrmprefs.set('bandnkeys', self.tempdedrmprefs['bandnkeys']) - self.dedrmprefs.set('adeptkeys', self.tempdedrmprefs['adeptkeys']) - self.dedrmprefs.set('ereaderkeys', self.tempdedrmprefs['ereaderkeys']) - self.dedrmprefs.set('kindlekeys', self.tempdedrmprefs['kindlekeys']) - self.dedrmprefs.set('androidkeys', self.tempdedrmprefs['androidkeys']) - self.dedrmprefs.set('pids', self.tempdedrmprefs['pids']) - self.dedrmprefs.set('serials', self.tempdedrmprefs['serials']) - self.dedrmprefs.set('adobewineprefix', self.tempdedrmprefs['adobewineprefix']) - self.dedrmprefs.set('kindlewineprefix', self.tempdedrmprefs['kindlewineprefix']) - self.dedrmprefs.set('configured', True) - self.dedrmprefs.writeprefs() - - def load_resource(self, name): - with ZipFile(self.plugin_path, 'r') as zf: - if name in zf.namelist(): - return zf.read(name) - return "" - - - -class ManageKeysDialog(QDialog): - def __init__(self, parent, key_type_name, plugin_keys, create_key, keyfile_ext = u"", wineprefix = None): - QDialog.__init__(self,parent) - self.parent = parent - self.key_type_name = key_type_name - self.plugin_keys = plugin_keys - self.create_key = create_key - self.keyfile_ext = keyfile_ext - self.import_key = (keyfile_ext != u"") - self.binary_file = (keyfile_ext == u"der") - self.json_file = (keyfile_ext == u"k4i") - self.android_file = (keyfile_ext == u"k4a") - self.wineprefix = wineprefix - - self.setWindowTitle("{0} {1}: Manage {2}s".format(PLUGIN_NAME, PLUGIN_VERSION, self.key_type_name)) - - # 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. - help_label = QLabel('Help', self) - help_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard) - help_label.setAlignment(Qt.AlignRight) - help_label.linkActivated.connect(self.help_link_activated) - help_layout.addWidget(help_label) - - keys_group_box = QGroupBox(_(u"{0}s".format(self.key_type_name)), 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(u"{0}s that will be used to decrypt ebooks".format(self.key_type_name)) - self.listy.setSelectionMode(QAbstractItemView.SingleSelection) - self.populate_list() - keys_group_box_layout.addWidget(self.listy) - - button_layout = QVBoxLayout() - keys_group_box_layout.addLayout(button_layout) - self._add_key_button = QtGui.QToolButton(self) - self._add_key_button.setIcon(QIcon(I('plus.png'))) - self._add_key_button.setToolTip(u"Create new {0}".format(self.key_type_name)) - 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(_(u"Delete highlighted key")) - self._delete_key_button.setIcon(QIcon(I('list_remove.png'))) - self._delete_key_button.clicked.connect(self.delete_key) - button_layout.addWidget(self._delete_key_button) - - if type(self.plugin_keys) == dict and self.import_key: - self._rename_key_button = QtGui.QToolButton(self) - self._rename_key_button.setToolTip(_(u"Rename highlighted key")) - 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(u"Save highlighted key to a .{0} file".format(self.keyfile_ext)) - self.export_key_button.setIcon(QIcon(I('save.png'))) - self.export_key_button.clicked.connect(self.export_key) - button_layout.addWidget(self.export_key_button) - spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) - button_layout.addItem(spacerItem) - - if self.wineprefix is not None: - layout.addSpacing(5) - wineprefix_layout = QHBoxLayout() - layout.addLayout(wineprefix_layout) - wineprefix_layout.setAlignment(Qt.AlignCenter) - self.wp_label = QLabel(u"WINEPREFIX:") - wineprefix_layout.addWidget(self.wp_label) - self.wp_lineedit = QLineEdit(self) - wineprefix_layout.addWidget(self.wp_lineedit) - self.wp_label.setBuddy(self.wp_lineedit) - self.wp_lineedit.setText(self.wineprefix) - - layout.addSpacing(5) - migrate_layout = QHBoxLayout() - layout.addLayout(migrate_layout) - if self.import_key: - migrate_layout.setAlignment(Qt.AlignJustify) - self.migrate_btn = QPushButton(u"Import Existing Keyfiles", self) - self.migrate_btn.setToolTip(u"Import *.{0} files (created using other tools).".format(self.keyfile_ext)) - self.migrate_btn.clicked.connect(self.migrate_wrapper) - migrate_layout.addWidget(self.migrate_btn) - migrate_layout.addStretch() - self.button_box = QDialogButtonBox(QDialogButtonBox.Close) - self.button_box.rejected.connect(self.close) - migrate_layout.addWidget(self.button_box) - - self.resize(self.sizeHint()) - - def getwineprefix(self): - if self.wineprefix is not None: - return unicode(self.wp_lineedit.text()).strip() - return u"" - - def populate_list(self): - if type(self.plugin_keys) == dict: - for key in self.plugin_keys.keys(): - self.listy.addItem(QListWidgetItem(key)) - else: - for key in self.plugin_keys: - self.listy.addItem(QListWidgetItem(key)) - - def add_key(self): - d = self.create_key(self) - d.exec_() - - if d.result() != d.Accepted: - # New key generation cancelled. - return - new_key_value = d.key_value - if type(self.plugin_keys) == dict: - if new_key_value in self.plugin_keys.values(): - old_key_name = [name for name, value in self.plugin_keys.iteritems() if value == new_key_value][0] - info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name), - u"The new {1} is the same as the existing {1} named {0} and has not been added.".format(old_key_name,self.key_type_name), show=True) - return - self.plugin_keys[d.key_name] = new_key_value - else: - if new_key_value in self.plugin_keys: - info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name), - u"This {0} is already in the list of {0}s has not been added.".format(self.key_type_name), show=True) - return - - self.plugin_keys.append(d.key_value) - self.listy.clear() - self.populate_list() - - def rename_key(self): - if not self.listy.currentItem(): - errmsg = u"No {0} selected to rename. Highlight a keyfile first.".format(self.key_type_name) - r = error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), - _(errmsg), show=True, show_copy_button=False) - return - - d = RenameKeyDialog(self) - d.exec_() - - if d.result() != d.Accepted: - # rename cancelled or moot. - return - keyname = unicode(self.listy.currentItem().text()) - if not question_dialog(self, "{0} {1}: Confirm Rename".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to rename the {2} named {0} to {1}?".format(keyname,d.key_name,self.key_type_name), show_copy_button=False, default_yes=False): - return - self.plugin_keys[d.key_name] = self.plugin_keys[keyname] - del self.plugin_keys[keyname] - - self.listy.clear() - self.populate_list() - - def delete_key(self): - if not self.listy.currentItem(): - return - keyname = unicode(self.listy.currentItem().text()) - if not question_dialog(self, "{0} {1}: Confirm Delete".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to delete the {1} {0}?".format(keyname, self.key_type_name), show_copy_button=False, default_yes=False): - return - if type(self.plugin_keys) == dict: - del self.plugin_keys[keyname] - else: - self.plugin_keys.remove(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 - # link is clicked in case the helpfile is updated in newer plugins. - help_file_name = u"{0}_{1}_Help.htm".format(PLUGIN_NAME, self.key_type_name) - file_path = os.path.join(config_dir, u"plugins", u"DeDRM", u"help", help_file_name) - with open(file_path,'w') as f: - f.write(self.parent.load_resource(help_file_name)) - return file_path - url = 'file:///' + get_help_file_resource() - open_url(QUrl(url)) - - def migrate_files(self): - unique_dlg_name = PLUGIN_NAME + u"import {0} keys".format(self.key_type_name).replace(' ', '_') #takes care of automatically remembering last directory - caption = u"Select {0} files to import".format(self.key_type_name) - filters = [(u"{0} files".format(self.key_type_name), [self.keyfile_ext])] - files = choose_files(self, unique_dlg_name, caption, filters, all_files=False) - counter = 0 - skipped = 0 - if files: - for filename in files: - fpath = os.path.join(config_dir, filename) - filename = os.path.basename(filename) - new_key_name = os.path.splitext(os.path.basename(filename))[0] - with open(fpath,'rb') as keyfile: - new_key_value = keyfile.read() - if self.binary_file: - new_key_value = new_key_value.encode('hex') - elif self.json_file: - new_key_value = json.loads(new_key_value) - elif self.android_file: - # convert to list of the keys in the string - new_key_value = new_key_value.splitlines() - match = False - for key in self.plugin_keys.keys(): - if uStrCmp(new_key_name, key, True): - skipped += 1 - msg = u"A key with the name {0} already exists!\nSkipping key file {1}.\nRename the existing key and import again".format(new_key_name,filename) - inf = info_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), - _(msg), show_copy_button=False, show=True) - match = True - break - if not match: - if new_key_value in self.plugin_keys.values(): - old_key_name = [name for name, value in self.plugin_keys.iteritems() if value == new_key_value][0] - skipped += 1 - info_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), - u"The key in file {0} is the same as the existing key {1} and has been skipped.".format(filename,old_key_name), show_copy_button=False, show=True) - else: - counter += 1 - self.plugin_keys[new_key_name] = new_key_value - - msg = u"" - if counter+skipped > 1: - if counter > 0: - msg += u"Imported {0:d} key {1}. ".format(counter, u"file" if counter == 1 else u"files") - if skipped > 0: - msg += u"Skipped {0:d} key {1}.".format(skipped, u"file" if counter == 1 else u"files") - inf = info_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), - _(msg), show_copy_button=False, show=True) - return counter > 0 - - def migrate_wrapper(self): - if self.migrate_files(): - self.listy.clear() - self.populate_list() - - def export_key(self): - if not self.listy.currentItem(): - errmsg = u"No keyfile selected to export. Highlight a keyfile first." - r = error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), - _(errmsg), show=True, show_copy_button=False) - return - keyname = unicode(self.listy.currentItem().text()) - unique_dlg_name = PLUGIN_NAME + u"export {0} keys".format(self.key_type_name).replace(' ', '_') #takes care of automatically remembering last directory - caption = u"Save {0} File as...".format(self.key_type_name) - filters = [(u"{0} Files".format(self.key_type_name), [u"{0}".format(self.keyfile_ext)])] - defaultname = u"{0}.{1}".format(keyname, self.keyfile_ext) - filename = choose_save_file(self, unique_dlg_name, caption, filters, all_files=False, initial_filename=defaultname) - if filename: - with file(filename, 'wb') as fname: - if self.binary_file: - fname.write(self.plugin_keys[keyname].decode('hex')) - elif self.json_file: - fname.write(json.dumps(self.plugin_keys[keyname])) - elif self.android_file: - for key in self.plugin_keys[keyname]: - fname.write(key) - fname.write("\n") - else: - fname.write(self.plugin_keys[keyname]) - - - - -class RenameKeyDialog(QDialog): - def __init__(self, parent=None,): - print repr(self), repr(parent) - QDialog.__init__(self, parent) - self.parent = parent - self.setWindowTitle("{0} {1}: Rename {0}".format(PLUGIN_NAME, PLUGIN_VERSION, parent.key_type_name)) - layout = QVBoxLayout(self) - self.setLayout(layout) - - data_group_box = QGroupBox('', self) - layout.addWidget(data_group_box) - data_group_box_layout = QVBoxLayout() - data_group_box.setLayout(data_group_box_layout) - - data_group_box_layout.addWidget(QLabel('New Key Name:', self)) - self.key_ledit = QLineEdit(self.parent.listy.currentItem().text(), self) - self.key_ledit.setToolTip(u"Enter a new name for this existing {0}.".format(parent.key_type_name)) - data_group_box_layout.addWidget(self.key_ledit) - - layout.addSpacing(20) - - self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - self.button_box.accepted.connect(self.accept) - self.button_box.rejected.connect(self.reject) - layout.addWidget(self.button_box) - - self.resize(self.sizeHint()) - - def accept(self): - if not unicode(self.key_ledit.text()) or unicode(self.key_ledit.text()).isspace(): - errmsg = u"Key name field cannot be empty!" - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), - _(errmsg), show=True, show_copy_button=False) - if len(self.key_ledit.text()) < 4: - errmsg = u"Key name must be at least 4 characters long!" - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), - _(errmsg), show=True, show_copy_button=False) - if uStrCmp(self.key_ledit.text(), self.parent.listy.currentItem().text()): - # Same exact name ... do nothing. - return QDialog.reject(self) - for k in self.parent.plugin_keys.keys(): - if (uStrCmp(self.key_ledit.text(), k, True) and - not uStrCmp(k, self.parent.listy.currentItem().text(), True)): - errmsg = u"The key name {0} is already being used.".format(self.key_ledit.text()) - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), - _(errmsg), show=True, show_copy_button=False) - QDialog.accept(self) - - @property - def key_name(self): - return unicode(self.key_ledit.text()).strip() - - - - - - - - -class AddBandNKeyDialog(QDialog): - def __init__(self, parent=None,): - QDialog.__init__(self, parent) - self.parent = parent - self.setWindowTitle(u"{0} {1}: Create New Barnes & Noble Key".format(PLUGIN_NAME, PLUGIN_VERSION)) - layout = QVBoxLayout(self) - self.setLayout(layout) - - data_group_box = QGroupBox(u"", self) - 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(u"Unique Key Name:", self)) - self.key_ledit = QLineEdit("", self) - self.key_ledit.setToolTip(_(u"

Enter an identifying name for this new key.

" + - u"

It should be something that will help you remember " + - u"what personal information was used to create it.")) - key_group.addWidget(self.key_ledit) - - name_group = QHBoxLayout() - data_group_box_layout.addLayout(name_group) - name_group.addWidget(QLabel(u"B&N/nook account email address:", self)) - self.name_ledit = QLineEdit(u"", self) - self.name_ledit.setToolTip(_(u"

Enter your email address as it appears in your B&N " + - u"account.

" + - u"

It will only be used to generate this " + - u"key and won\'t be stored anywhere " + - u"in calibre or on your computer.

" + - u"

eg: apprenticeharper@gmail.com

")) - name_group.addWidget(self.name_ledit) - name_disclaimer_label = QLabel(_(u"(Will not be saved in configuration data)"), self) - name_disclaimer_label.setAlignment(Qt.AlignHCenter) - data_group_box_layout.addWidget(name_disclaimer_label) - - ccn_group = QHBoxLayout() - data_group_box_layout.addLayout(ccn_group) - ccn_group.addWidget(QLabel(u"B&N/nook account password:", self)) - self.cc_ledit = QLineEdit(u"", self) - self.cc_ledit.setToolTip(_(u"

Enter the password " + - u"for your B&N account.

" + - u"

The password will only be used to generate this " + - u"key and won\'t be stored anywhere in " + - u"calibre or on your computer.")) - ccn_group.addWidget(self.cc_ledit) - ccn_disclaimer_label = QLabel(_('(Will not be saved in configuration data)'), self) - ccn_disclaimer_label.setAlignment(Qt.AlignHCenter) - data_group_box_layout.addWidget(ccn_disclaimer_label) - layout.addSpacing(10) - - key_group = QHBoxLayout() - data_group_box_layout.addLayout(key_group) - key_group.addWidget(QLabel(u"Retrieved key:", self)) - self.key_display = QLabel(u"", self) - self.key_display.setToolTip(_(u"Click the Retrieve Key button to fetch your B&N encryption key from the B&N servers")) - key_group.addWidget(self.key_display) - self.retrieve_button = QtGui.QPushButton(self) - self.retrieve_button.setToolTip(_(u"Click to retrieve your B&N encryption key from the B&N servers")) - self.retrieve_button.setText(u"Retrieve Key") - self.retrieve_button.clicked.connect(self.retrieve_key) - key_group.addWidget(self.retrieve_button) - layout.addSpacing(10) - - self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - self.button_box.accepted.connect(self.accept) - self.button_box.rejected.connect(self.reject) - layout.addWidget(self.button_box) - - self.resize(self.sizeHint()) - - @property - def key_name(self): - return unicode(self.key_ledit.text()).strip() - - @property - def key_value(self): - return unicode(self.key_display.text()).strip() - - @property - def user_name(self): - return unicode(self.name_ledit.text()).strip().lower().replace(' ','') - - @property - def cc_number(self): - return unicode(self.cc_ledit.text()).strip() - - def retrieve_key(self): - from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key - fetched_key = fetch_bandn_key(self.user_name,self.cc_number) - if fetched_key == "": - errmsg = u"Could not retrieve key. Check username, password and intenet connectivity and try again." - error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - else: - self.key_display.setText(fetched_key) - - def accept(self): - if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace(): - errmsg = u"All fields are required!" - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - if len(self.key_name) < 4: - errmsg = u"Key name must be at least 4 characters long!" - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - if len(self.key_value) == 0: - self.retrieve_key() - if len(self.key_value) == 0: - return - QDialog.accept(self) - -class AddEReaderDialog(QDialog): - def __init__(self, parent=None,): - QDialog.__init__(self, parent) - self.parent = parent - self.setWindowTitle(u"{0} {1}: Create New eReader Key".format(PLUGIN_NAME, PLUGIN_VERSION)) - layout = QVBoxLayout(self) - self.setLayout(layout) - - data_group_box = QGroupBox(u"", self) - 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(u"Unique Key Name:", self)) - self.key_ledit = QLineEdit("", self) - self.key_ledit.setToolTip(u"

Enter an identifying name for this new key.\nIt should be something that will help you remember what personal information was used to create it.") - key_group.addWidget(self.key_ledit) - - name_group = QHBoxLayout() - data_group_box_layout.addLayout(name_group) - name_group.addWidget(QLabel(u"Your Name:", self)) - self.name_ledit = QLineEdit(u"", self) - self.name_ledit.setToolTip(u"Enter the name for this eReader key, usually the name on your credit card.\nIt will only be used to generate this one-time key and won\'t be stored anywhere in calibre or on your computer.\n(ex: Mr Jonathan Q Smith)") - name_group.addWidget(self.name_ledit) - name_disclaimer_label = QLabel(_(u"(Will not be saved in configuration data)"), self) - name_disclaimer_label.setAlignment(Qt.AlignHCenter) - data_group_box_layout.addWidget(name_disclaimer_label) - - ccn_group = QHBoxLayout() - data_group_box_layout.addLayout(ccn_group) - ccn_group.addWidget(QLabel(u"Credit Card#:", self)) - self.cc_ledit = QLineEdit(u"", self) - self.cc_ledit.setToolTip(u"

Enter the last 8 digits of credit card number for this eReader key.\nThey will only be used to generate this one-time key and won\'t be stored anywhere in calibre or on your computer.") - ccn_group.addWidget(self.cc_ledit) - ccn_disclaimer_label = QLabel(_('(Will not be saved in configuration data)'), self) - ccn_disclaimer_label.setAlignment(Qt.AlignHCenter) - data_group_box_layout.addWidget(ccn_disclaimer_label) - layout.addSpacing(10) - - self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - self.button_box.accepted.connect(self.accept) - self.button_box.rejected.connect(self.reject) - layout.addWidget(self.button_box) - - self.resize(self.sizeHint()) - - @property - def key_name(self): - return unicode(self.key_ledit.text()).strip() - - @property - def key_value(self): - from calibre_plugins.dedrm.erdr2pml import getuser_key as generate_ereader_key - return generate_ereader_key(self.user_name,self.cc_number).encode('hex') - - @property - def user_name(self): - return unicode(self.name_ledit.text()).strip().lower().replace(' ','') - - @property - def cc_number(self): - return unicode(self.cc_ledit.text()).strip().replace(' ', '').replace('-','') - - - def accept(self): - if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace(): - errmsg = u"All fields are required!" - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - if not self.cc_number.isdigit(): - errmsg = u"Numbers only in the credit card number field!" - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - if len(self.key_name) < 4: - errmsg = u"Key name must be at least 4 characters long!" - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - QDialog.accept(self) - - -class AddAdeptDialog(QDialog): - def __init__(self, parent=None,): - QDialog.__init__(self, parent) - self.parent = parent - self.setWindowTitle(u"{0} {1}: Getting Default Adobe Digital Editions Key".format(PLUGIN_NAME, PLUGIN_VERSION)) - layout = QVBoxLayout(self) - self.setLayout(layout) - - try: - if iswindows or isosx: - from calibre_plugins.dedrm.adobekey import adeptkeys - - defaultkeys = adeptkeys() - else: # linux - from wineutils import WineGetKeys - - scriptpath = os.path.join(parent.parent.alfdir,u"adobekey.py") - defaultkeys = WineGetKeys(scriptpath, u".der",parent.getwineprefix()) - - self.default_key = defaultkeys[0] - except: - traceback.print_exc() - self.default_key = u"" - - self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - - if len(self.default_key)>0: - data_group_box = QGroupBox(u"", self) - 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(u"Unique Key Name:", self)) - self.key_ledit = QLineEdit(u"default_key", self) - self.key_ledit.setToolTip(u"

Enter an identifying name for the current default Adobe Digital Editions key.") - key_group.addWidget(self.key_ledit) - - self.button_box.accepted.connect(self.accept) - else: - default_key_error = QLabel(u"The default encryption key for Adobe Digital Editions could not be found.", self) - default_key_error.setAlignment(Qt.AlignHCenter) - layout.addWidget(default_key_error) - # if no default, bot buttons do the same - self.button_box.accepted.connect(self.reject) - - self.button_box.rejected.connect(self.reject) - layout.addWidget(self.button_box) - - self.resize(self.sizeHint()) - - @property - def key_name(self): - return unicode(self.key_ledit.text()).strip() - - @property - def key_value(self): - return self.default_key.encode('hex') - - - def accept(self): - if len(self.key_name) == 0 or self.key_name.isspace(): - errmsg = u"All fields are required!" - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - if len(self.key_name) < 4: - errmsg = u"Key name must be at least 4 characters long!" - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - QDialog.accept(self) - - -class AddKindleDialog(QDialog): - def __init__(self, parent=None,): - QDialog.__init__(self, parent) - self.parent = parent - self.setWindowTitle(u"{0} {1}: Getting Default Kindle for Mac/PC Key".format(PLUGIN_NAME, PLUGIN_VERSION)) - layout = QVBoxLayout(self) - self.setLayout(layout) - - try: - if iswindows or isosx: - from calibre_plugins.dedrm.kindlekey import kindlekeys - - defaultkeys = kindlekeys() - else: # linux - from wineutils import WineGetKeys - - scriptpath = os.path.join(parent.parent.alfdir,u"kindlekey.py") - defaultkeys = WineGetKeys(scriptpath, u".k4i",parent.getwineprefix()) - - self.default_key = defaultkeys[0] - except: - traceback.print_exc() - self.default_key = u"" - - self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - - if len(self.default_key)>0: - data_group_box = QGroupBox(u"", self) - 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(u"Unique Key Name:", self)) - self.key_ledit = QLineEdit(u"default_key", self) - self.key_ledit.setToolTip(u"

Enter an identifying name for the current default Kindle for Mac/PC key.") - key_group.addWidget(self.key_ledit) - - self.button_box.accepted.connect(self.accept) - else: - default_key_error = QLabel(u"The default encryption key for Kindle for Mac/PC could not be found.", self) - default_key_error.setAlignment(Qt.AlignHCenter) - layout.addWidget(default_key_error) - - # if no default, both buttons do the same - self.button_box.accepted.connect(self.reject) - - self.button_box.rejected.connect(self.reject) - layout.addWidget(self.button_box) - - self.resize(self.sizeHint()) - - @property - def key_name(self): - return unicode(self.key_ledit.text()).strip() - - @property - def key_value(self): - return self.default_key - - - def accept(self): - if len(self.key_name) == 0 or self.key_name.isspace(): - errmsg = u"All fields are required!" - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - if len(self.key_name) < 4: - errmsg = u"Key name must be at least 4 characters long!" - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - QDialog.accept(self) - - -class AddSerialDialog(QDialog): - def __init__(self, parent=None,): - QDialog.__init__(self, parent) - self.parent = parent - self.setWindowTitle(u"{0} {1}: Add New EInk Kindle Serial Number".format(PLUGIN_NAME, PLUGIN_VERSION)) - layout = QVBoxLayout(self) - self.setLayout(layout) - - data_group_box = QGroupBox(u"", self) - 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(u"EInk Kindle Serial Number:", self)) - self.key_ledit = QLineEdit("", self) - self.key_ledit.setToolTip(u"Enter an eInk Kindle serial number. EInk Kindle serial numbers are 16 characters long and usually start with a 'B' or a '9'. Kindle Serial Numbers are case-sensitive, so be sure to enter the upper and lower case letters unchanged.") - key_group.addWidget(self.key_ledit) - - self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - self.button_box.accepted.connect(self.accept) - self.button_box.rejected.connect(self.reject) - layout.addWidget(self.button_box) - - self.resize(self.sizeHint()) - - @property - def key_name(self): - return unicode(self.key_ledit.text()).strip() - - @property - def key_value(self): - return unicode(self.key_ledit.text()).strip() - - def accept(self): - if len(self.key_name) == 0 or self.key_name.isspace(): - errmsg = u"Please enter an eInk Kindle Serial Number or click Cancel in the dialog." - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - if len(self.key_name) != 16: - errmsg = u"EInk Kindle Serial Numbers must be 16 characters long. This is {0:d} characters long.".format(len(self.key_name)) - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - QDialog.accept(self) - - -class AddAndroidDialog(QDialog): - def __init__(self, parent=None,): - - QDialog.__init__(self, parent) - self.parent = parent - self.setWindowTitle(u"{0} {1}: Add new Kindle for Android Key".format(PLUGIN_NAME, PLUGIN_VERSION)) - layout = QVBoxLayout(self) - self.setLayout(layout) - self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - - data_group_box = QGroupBox(u"", self) - layout.addWidget(data_group_box) - data_group_box_layout = QVBoxLayout() - data_group_box.setLayout(data_group_box_layout) - - file_group = QHBoxLayout() - data_group_box_layout.addLayout(file_group) - add_btn = QPushButton(u"Choose Backup File", self) - add_btn.setToolTip(u"Import Kindle for Android backup file.") - add_btn.clicked.connect(self.get_android_file) - file_group.addWidget(add_btn) - self.selected_file_name = QLabel(u"",self) - self.selected_file_name.setAlignment(Qt.AlignHCenter) - file_group.addWidget(self.selected_file_name) - - key_group = QHBoxLayout() - data_group_box_layout.addLayout(key_group) - key_group.addWidget(QLabel(u"Unique Key Name:", self)) - self.key_ledit = QLineEdit(u"", self) - self.key_ledit.setToolTip(u"

Enter an identifying name for the Android for Kindle key.") - key_group.addWidget(self.key_ledit) - #key_label = QLabel(_(''), self) - #key_label.setAlignment(Qt.AlignHCenter) - #data_group_box_layout.addWidget(key_label) - - self.button_box.accepted.connect(self.accept) - self.button_box.rejected.connect(self.reject) - layout.addWidget(self.button_box) - self.resize(self.sizeHint()) - - @property - def key_name(self): - return unicode(self.key_ledit.text()).strip() - - @property - def file_name(self): - return unicode(self.selected_file_name.text()).strip() - - @property - def key_value(self): - return self.serials_from_file - - def get_android_file(self): - unique_dlg_name = PLUGIN_NAME + u"Import Kindle for Android backup file" #takes care of automatically remembering last directory - caption = u"Select Kindle for Android backup file to add" - filters = [(u"Kindle for Android backup files", ['db','ab','xml'])] - files = choose_files(self, unique_dlg_name, caption, filters, all_files=False) - self.serials_from_file = [] - file_name = u"" - if files: - # find the first selected file that yields some serial numbers - for filename in files: - fpath = os.path.join(config_dir, filename) - self.filename = os.path.basename(filename) - file_serials = androidkindlekey.get_serials(fpath) - if len(file_serials)>0: - file_name = os.path.basename(self.filename) - self.serials_from_file.extend(file_serials) - self.selected_file_name.setText(file_name) - - - def accept(self): - if len(self.file_name) == 0 or len(self.key_value) == 0: - errmsg = u"Please choose a Kindle for Android backup file." - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - if len(self.key_name) == 0 or self.key_name.isspace(): - errmsg = u"Please enter a key name." - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - if len(self.key_name) < 4: - errmsg = u"Key name must be at least 4 characters long!" - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - QDialog.accept(self) - -class AddPIDDialog(QDialog): - def __init__(self, parent=None,): - QDialog.__init__(self, parent) - self.parent = parent - self.setWindowTitle(u"{0} {1}: Add New Mobipocket PID".format(PLUGIN_NAME, PLUGIN_VERSION)) - layout = QVBoxLayout(self) - self.setLayout(layout) - - data_group_box = QGroupBox(u"", self) - 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(u"PID:", self)) - self.key_ledit = QLineEdit("", self) - self.key_ledit.setToolTip(u"Enter a Mobipocket PID. Mobipocket PIDs are 8 or 10 characters long. Mobipocket PIDs are case-sensitive, so be sure to enter the upper and lower case letters unchanged.") - key_group.addWidget(self.key_ledit) - - self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - self.button_box.accepted.connect(self.accept) - self.button_box.rejected.connect(self.reject) - layout.addWidget(self.button_box) - - self.resize(self.sizeHint()) - - @property - def key_name(self): - return unicode(self.key_ledit.text()).strip() - - @property - def key_value(self): - return unicode(self.key_ledit.text()).strip() - - def accept(self): - if len(self.key_name) == 0 or self.key_name.isspace(): - errmsg = u"Please enter a Mobipocket PID or click Cancel in the dialog." - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - if len(self.key_name) != 8 and len(self.key_name) != 10: - errmsg = u"Mobipocket PIDs must be 8 or 10 characters long. This is {0:d} characters long.".format(len(self.key_name)) - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - QDialog.accept(self) - - diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/convert2xml.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/convert2xml.py deleted file mode 100644 index 8c2d0f3..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/convert2xml.py +++ /dev/null @@ -1,884 +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.first_unknown = True - 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.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), - '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.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), - '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), - 'img.gridSize' : (1, 'scalar_number', 0, 0), - 'img.gridBottomCenter' : (1, 'scalar_number', 0, 0), - 'img.gridTopCenter' : (1, 'scalar_number', 0, 0), - 'img.gridBeginCenter' : (1, 'scalar_number', 0, 0), - 'img.gridEndCenter' : (1, 'scalar_number', 0, 0), - 'img.image_type' : (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.class' : (1, 'scalar_text', 0, 0), - 'word_semantic.firstWord' : (1, 'scalar_number', 0, 0), - 'word_semantic.lastWord' : (1, 'scalar_number', 0, 0), - 'word_semantic.gridBottomCenter' : (1, 'scalar_number', 0, 0), - 'word_semantic.gridTopCenter' : (1, 'scalar_number', 0, 0), - 'word_semantic.gridBeginCenter' : (1, 'scalar_number', 0, 0), - 'word_semantic.gridEndCenter' : (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.class' : (1, 'scalar_text', 0, 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.class' : (1, 'scalar_text', 0, 0), - 'extratokens.type' : (1, 'scalar_text', 0, 0), - 'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0), - 'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0), - 'extratokens.gridSize' : (1, 'scalar_number', 0, 0), - 'extratokens.gridBottomCenter' : (1, 'scalar_number', 0, 0), - 'extratokens.gridTopCenter' : (1, 'scalar_number', 0, 0), - 'extratokens.gridBeginCenter' : (1, 'scalar_number', 0, 0), - 'extratokens.gridEndCenter' : (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._after_type' : (1, 'scalar_text', 0, 0), - 'style._parent_type' : (1, 'scalar_text', 0, 0), - 'style._after_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), - - 'median_d' : (1, 'number', 0, 0), - 'median_h' : (1, 'number', 0, 0), - 'median_firsty' : (1, 'number', 0, 0), - 'median_lasty' : (1, 'number', 0, 0), - - 'num_footers_maybe' : (1, 'number', 0, 0), - 'num_footers_yes' : (1, 'number', 0, 0), - 'num_headers_maybe' : (1, 'number', 0, 0), - 'num_headers_yes' : (1, 'number', 0, 0), - - 'tracking' : (1, 'number', 0, 0), - 'src' : (1, 'text', 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 or self.first_unknown): - print 'Unknown Token:', token - self.first_unknown = False - 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/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/encodebase64.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/encodebase64.py deleted file mode 100644 index 6bb8c37..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/encodebase64.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/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_Macintosh_Application/DeDRM.app/Contents/Resources/epubtest.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/epubtest.py deleted file mode 100644 index 11f1427..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/epubtest.py +++ /dev/null @@ -1,208 +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 drmcheck -# 1.00 - Initial version, with code from various other scripts -# 1.01 - Moved authorship announcement to usage section. -# -# Changelog epubtest -# 1.00 - Cut to epubtest.py, testing ePub files only by Apprentice Alf -# 1.01 - Added routine for use by Windows DeDRM -# -# 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.01' - -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) - -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 '?'. So use shell32.GetCommandLineArgvW to get sys.argv - # as a list of Unicode strings and encode them as utf-8 - - from ctypes import POINTER, byref, cdll, c_int, windll - from ctypes.wintypes import LPCWSTR, LPWSTR - - GetCommandLineW = cdll.kernel32.GetCommandLineW - GetCommandLineW.argtypes = [] - GetCommandLineW.restype = LPCWSTR - - CommandLineToArgvW = windll.shell32.CommandLineToArgvW - CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] - CommandLineToArgvW.restype = POINTER(LPWSTR) - - cmd = GetCommandLineW() - argc = c_int(0) - argv = CommandLineToArgvW(cmd, byref(argc)) - if argc.value > 0: - # Remove Python executable and commands if present - start = argc.value - len(sys.argv) - return [argv[i] 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"epubtest.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] - -_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 iswindows: - # first try with pycrypto - if inCalibre: - from calibre_plugins.dedrm import pycrypto_des - else: - import pycrypto_des - Des = pycrypto_des.load_pycrypto() - if Des == None: - # they try with openssl - if inCalibre: - from calibre_plugins.dedrm import openssl_des - else: - import openssl_des - Des = openssl_des.load_libcrypto() -else: - # first try with openssl - if inCalibre: - from calibre_plugins.dedrm import openssl_des - else: - import openssl_des - Des = openssl_des.load_libcrypto() - if Des == None: - # then try with pycrypto - if inCalibre: - from calibre_plugins.dedrm import pycrypto_des - else: - import pycrypto_des - Des = pycrypto_des.load_pycrypto() - -# if that did not work then use pure python implementation -# of DES and try to speed it up with Psycho -if Des == None: - if inCalibre: - from calibre_plugins.dedrm import python_des - else: - import python_des - Des = python_des.Des - # Import Psyco if available - try: - # http://psyco.sourceforge.net - import psyco - psyco.full() - except ImportError: - pass - -try: - from hashlib import sha1 -except ImportError: - # older Python release - import sha - sha1 = lambda s: sha.new(s) - -import cgi -import logging - -logging.basicConfig() -#logging.basicConfig(level=logging.DEBUG) - - -class Sectionizer(object): - bkType = "Book" - - 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]) - # Dictionary or normal content (TODO: Not hard-coded) - if self.header[0x3C:0x3C+8] != ident: - if self.header[0x3C:0x3C+8] == "PDctPPrs": - self.bkType = "Dict" - else: - 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] - -# 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]) - -def deXOR(text, sp, table): - r='' - j = sp - for i in xrange(len(text)): - r += chr(ord(table[j]) ^ ord(text[i])) - j = j + 1 - if j == len(table): - j = 0 - return r - -class EreaderProcessor(object): - def __init__(self, sect, user_key): - self.section_reader = sect.loadSection - data = self.section_reader(0) - version, = struct.unpack('>H', data[0:2]) - self.version = version - logging.info('eReader file format version %s', version) - if version != 272 and version != 260 and version != 259: - raise ValueError('incorrect eReader version %d (error 1)' % version) - data = self.section_reader(1) - self.data = data - des = Des(fixKey(data[0:8])) - cookie_shuf, cookie_size = struct.unpack('>LL', des.decrypt(data[-8:])) - if cookie_shuf < 3 or cookie_shuf > 0x14 or cookie_size < 0xf0 or cookie_size > 0x200: - raise ValueError('incorrect eReader version (error 2)') - input = des.decrypt(data[-cookie_size:]) - def unshuff(data, shuf): - r = [''] * len(data) - j = 0 - for i in xrange(len(data)): - j = (j + shuf) % len(data) - r[j] = data[i] - assert len("".join(r)) == len(data) - return "".join(r) - r = unshuff(input[0:-8], cookie_shuf) - - 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] - self.first_image_page = struct.unpack('>H', r[24:24+2])[0] - # Default values - self.num_footnote_pages = 0 - self.num_sidebar_pages = 0 - self.first_footnote_page = -1 - self.first_sidebar_page = -1 - if self.version == 272: - self.num_footnote_pages = struct.unpack('>H', r[46:46+2])[0] - self.first_footnote_page = struct.unpack('>H', r[44:44+2])[0] - if (sect.bkType == "Book"): - self.num_sidebar_pages = struct.unpack('>H', r[38:38+2])[0] - self.first_sidebar_page = struct.unpack('>H', r[36:36+2])[0] - # self.num_bookinfo_pages = struct.unpack('>H', r[34:34+2])[0] - # self.first_bookinfo_page = struct.unpack('>H', r[32:32+2])[0] - # self.num_chapter_pages = struct.unpack('>H', r[22:22+2])[0] - # self.first_chapter_page = struct.unpack('>H', r[20:20+2])[0] - # self.num_link_pages = struct.unpack('>H', r[30:30+2])[0] - # self.first_link_page = struct.unpack('>H', r[28:28+2])[0] - # self.num_xtextsize_pages = struct.unpack('>H', r[54:54+2])[0] - # self.first_xtextsize_page = struct.unpack('>H', r[52:52+2])[0] - - # **before** data record 1 was decrypted and unshuffled, it contained data - # to create an XOR table and which is used to fix footnote record 0, link records, chapter records, etc - self.xortable_offset = struct.unpack('>H', r[40:40+2])[0] - self.xortable_size = struct.unpack('>H', r[42:42+2])[0] - self.xortable = self.data[self.xortable_offset:self.xortable_offset + self.xortable_size] - else: - # Nothing needs to be done - pass - # self.num_bookinfo_pages = 0 - # self.num_chapter_pages = 0 - # self.num_link_pages = 0 - # self.num_xtextsize_pages = 0 - # self.first_bookinfo_page = -1 - # self.first_chapter_page = -1 - # self.first_link_page = -1 - # self.first_xtextsize_page = -1 - - logging.debug('self.num_text_pages %d', self.num_text_pages) - logging.debug('self.num_footnote_pages %d, self.first_footnote_page %d', self.num_footnote_pages , self.first_footnote_page) - logging.debug('self.num_sidebar_pages %d, self.first_sidebar_page %d', self.num_sidebar_pages , self.first_sidebar_page) - self.flags = struct.unpack('>L', r[4:8])[0] - reqd_flags = (1<<9) | (1<<7) | (1<<10) - if (self.flags & reqd_flags) != reqd_flags: - print "Flags: 0x%X" % self.flags - raise ValueError('incompatible eReader file') - des = Des(fixKey(user_key)) - if version == 259: - if drm_sub_version != 7: - raise ValueError('incorrect eReader version %d (error 3)' % drm_sub_version) - encrypted_key_sha = r[44:44+20] - encrypted_key = r[64:64+8] - elif version == 260: - if drm_sub_version != 13 and drm_sub_version != 11: - raise ValueError('incorrect eReader version %d (error 3)' % drm_sub_version) - if drm_sub_version == 13: - encrypted_key = r[44:44+8] - encrypted_key_sha = r[52:52+20] - else: - encrypted_key = r[64:64+8] - encrypted_key_sha = r[44:44+20] - elif version == 272: - encrypted_key = r[172:172+8] - encrypted_key_sha = r[56:56+20] - self.content_key = des.decrypt(encrypted_key) - if sha1(self.content_key).digest() != encrypted_key_sha: - raise ValueError('Incorrect Name and/or Credit Card') - - def getNumImages(self): - return self.num_image_pages - - def getImage(self, i): - sect = self.section_reader(self.first_image_page + i) - name = sect[4:4+32].strip('\0') - data = sect[62:] - return sanitizeFileName(unicode(name,'windows-1252')), data - - - # def getChapterNamePMLOffsetData(self): - # cv = '' - # if self.num_chapter_pages > 0: - # for i in xrange(self.num_chapter_pages): - # chaps = self.section_reader(self.first_chapter_page + i) - # j = i % self.xortable_size - # 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) - # return cv - - # def getLinkNamePMLOffsetData(self): - # lv = '' - # if self.num_link_pages > 0: - # for i in xrange(self.num_link_pages): - # links = self.section_reader(self.first_link_page + i) - # j = i % self.xortable_size - # 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) - # return lv - - # def getExpandedTextSizesData(self): - # ts = '' - # if self.num_xtextsize_pages > 0: - # tsize = deXOR(self.section_reader(self.first_xtextsize_page), 0, self.xortable) - # for i in xrange(self.num_text_pages): - # xsize = struct.unpack('>H', tsize[0:2])[0] - # ts += "%d\n" % xsize - # tsize = tsize[2:] - # return ts - - # def getBookInfo(self): - # bkinfo = '' - # if self.num_bookinfo_pages > 0: - # info = self.section_reader(self.first_bookinfo_page) - # bkinfo = deXOR(info, 0, self.xortable) - # bkinfo = bkinfo.replace('\0','|') - # bkinfo += '\n' - # return bkinfo - - def getText(self): - des = Des(fixKey(self.content_key)) - r = '' - 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' - # the record 0 of the footnote section must pass through the Xor Table to make it useful - sect = self.section_reader(self.first_footnote_page) - fnote_ids = deXOR(sect, 0, self.xortable) - # the remaining records of the footnote sections need to be decoded with the content_key and zlib inflated - des = Des(fixKey(self.content_key)) - for i in xrange(1,self.num_footnote_pages): - logging.debug('get footnotepage %d', i) - id_len = ord(fnote_ids[2]) - id = fnote_ids[3:3+id_len] - fmarker = '\n' % id - fmarker += zlib.decompress(des.decrypt(self.section_reader(self.first_footnote_page + i))) - fmarker += '\n\n' - r += fmarker - fnote_ids = fnote_ids[id_len+4:] - - # TODO: Handle dictionary index (?) pages - which are also marked as - # sidebar_pages (?). For now dictionary sidebars are ignored - # For dictionaries - record 0 is null terminated strings, followed by - # blocks of around 62000 bytes and a final block. Not sure of the - # encoding - - # now handle sidebar pages - if self.num_sidebar_pages > 0: - r += '\n' - # the record 0 of the sidebar section must pass through the Xor Table to make it useful - sect = self.section_reader(self.first_sidebar_page) - sbar_ids = deXOR(sect, 0, self.xortable) - # the remaining records of the sidebar sections need to be decoded with the content_key and zlib inflated - des = Des(fixKey(self.content_key)) - for i in xrange(1,self.num_sidebar_pages): - id_len = ord(sbar_ids[2]) - id = sbar_ids[3:3+id_len] - smarker = '\n' % id - smarker += zlib.decompress(des.decrypt(self.section_reader(self.first_sidebar_page + i))) - smarker += '\n\n' - r += smarker - sbar_ids = sbar_ids[id_len+4:] - - 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 - -def decryptBook(infile, outpath, make_pmlz, user_key): - bookname = os.path.splitext(os.path.basename(infile))[0] - 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: - 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 u"Creating PMLZ file {0}".format(os.path.basename(pmlzname)) - myZipFile = zipfile.ZipFile(pmlzname,'w',zipfile.ZIP_STORED, False) - list = os.listdir(outdir) - 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): - 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() - # remove temporary directory - shutil.rmtree(outdir, True) - print u"Output is {0}".format(pmlzname) - else : - print u"Output is in {0}".format(outdir) - print "done" - except ValueError, e: - print u"Error: {0}".format(e) - traceback.print_exc() - return 1 - return 0 - - -def usage(): - 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(): - print u"eRdr2Pml v{0}. Copyright © 2009–2012 The Dark Reverser et al.".format(__version__) - - argv=unicode_argv() - try: - opts, args = getopt.getopt(argv[1:], "hp", ["make-pmlz"]) - except getopt.GetoptError, err: - print err.args[0] - usage() - return 1 - make_pmlz = False - for o, a in opts: - if o == "-h": - usage() - return 0 - elif o == "-p": - make_pmlz = True - elif o == "--make-pmlz": - make_pmlz = True - - if len(args)!=3 and len(args)!=4: - usage() - return 1 - - if len(args)==3: - 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, outpath, name, cc = args - - print getuser_key(name,cc).encode('hex') - - return decryptBook(infile, outpath, make_pmlz, getuser_key(name,cc)) - - -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/flatxml2html.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/flatxml2html.py deleted file mode 100644 index 991591b..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/flatxml2html.py +++ /dev/null @@ -1,801 +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'): - # we only handle spaceafter word class - try: - (cname, space) = argres.split('-',1) - if space == '' : space = '0' - if (cname == 'spaceafter') and (int(space) > 0) : - word_class = 'sa' - except: - pass - - elif name.endswith('word.img.src'): - result.append(('img' + word_class, int(argres))) - 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' : - try: - word = self.ocrtext[num] - except: - word = "" - - 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/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/flatxml2svg.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/flatxml2svg.py deleted file mode 100644 index 4dfd6c7..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/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/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/genbook.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/genbook.py deleted file mode 100644 index 4036896..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/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.dedrm import convert2xml - from calibre_plugins.dedrm import flatxml2html - from calibre_plugins.dedrm import flatxml2svg - from calibre_plugins.dedrm 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/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignobleepub.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignobleepub.py deleted file mode 100644 index 1dda116..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignobleepub.py +++ /dev/null @@ -1,455 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# ignobleepub.pyw, version 4.1 -# Copyright © 2009-2010 by i♥cabbages - -# Released under the terms of the GNU General Public Licence, version 3 -# - -# Modified 2010–2013 by some_updates, DiapDealer and Apprentice Alf -# Modified 2015–2017 by Apprentice Harper - -# 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 decrypt from encryption.xml -# 3.3 - On Windows try PyCrypto first, OpenSSL next -# 3.4 - Modify interface to allow use with import -# 3.5 - Fix for potential problem with PyCrypto -# 3.6 - Revised to allow use in calibre plugins to eliminate need for duplicate code -# 3.7 - Tweaked to match ineptepub more closely -# 3.8 - Fixed to retain zip file metadata (e.g. file modification date) -# 3.9 - moved unicode_argv call inside main for Windows DeDRM compatibility -# 4.0 - Work if TkInter is missing -# 4.1 - Import tkFileDialog, don't assume something else will import it. - -""" -Decrypt Barnes & Noble encrypted ePub books. -""" - -__license__ = 'GPL v3' -__version__ = "4.1" - -import sys -import os -import traceback -import zlib -import zipfile -from zipfile import ZipInfo, 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 Decryptor(object): - def __init__(self, bookkey, encryption): - enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag) - self._aes = AES(bookkey) - encryption = etree.fromstring(encryption) - self._encrypted = encrypted = set() - expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'), - enc('CipherReference')) - for elem in encryption.findall(expr): - path = elem.get('URI', None) - if path is not None: - path = path.encode('utf-8') - encrypted.add(path) - - def decompress(self, bytes): - dc = zlib.decompressobj(-15) - bytes = dc.decompress(bytes) - ex = dc.decompress('Z') + dc.flush() - if ex: - bytes = bytes + ex - return bytes - - def decrypt(self, path, data): - if path in self._encrypted: - data = self._aes.decrypt(data)[16:] - data = data[:-ord(data[-1])] - data = self.decompress(data) - return data - -# check file to make check whether it's probably an Adobe Adept encrypted ePub -def ignobleBook(inpath): - with closing(ZipFile(open(inpath, 'rb'))) as inf: - namelist = set(inf.namelist()) - if 'META-INF/rights.xml' not in namelist or \ - 'META-INF/encryption.xml' not in namelist: - return False - try: - rights = etree.fromstring(inf.read('META-INF/rights.xml')) - adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) - expr = './/%s' % (adept('encryptedKey'),) - bookkey = ''.join(rights.findtext(expr)) - if len(bookkey) == 64: - return True - except: - # if we couldn't check, assume it is - return True - return False - -def decryptBook(keyb64, inpath, outpath): - if AES is None: - raise IGNOBLEError(u"PyCrypto or OpenSSL must be installed.") - key = keyb64.decode('base64')[:16] - aes = AES(key) - with closing(ZipFile(open(inpath, 'rb'))) as inf: - namelist = set(inf.namelist()) - if 'META-INF/rights.xml' not in namelist or \ - 'META-INF/encryption.xml' not in namelist: - print u"{0:s} is DRM-free.".format(os.path.basename(inpath)) - return 1 - for name in META_NAMES: - namelist.remove(name) - try: - rights = etree.fromstring(inf.read('META-INF/rights.xml')) - adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) - expr = './/%s' % (adept('encryptedKey'),) - bookkey = ''.join(rights.findtext(expr)) - if len(bookkey) != 64: - print u"{0:s} is not a secure Barnes & Noble ePub.".format(os.path.basename(inpath)) - return 1 - bookkey = aes.decrypt(bookkey.decode('base64')) - bookkey = bookkey[:-ord(bookkey[-1])] - encryption = inf.read('META-INF/encryption.xml') - decryptor = Decryptor(bookkey[-16:], encryption) - kwds = dict(compression=ZIP_DEFLATED, allowZip64=False) - with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf: - zi = ZipInfo('mimetype') - zi.compress_type=ZIP_STORED - try: - # if the mimetype is present, get its info, including time-stamp - oldzi = inf.getinfo('mimetype') - # copy across fields to be preserved - zi.date_time = oldzi.date_time - zi.comment = oldzi.comment - zi.extra = oldzi.extra - zi.internal_attr = oldzi.internal_attr - # external attributes are dependent on the create system, so copy both. - zi.external_attr = oldzi.external_attr - zi.create_system = oldzi.create_system - except: - pass - outf.writestr(zi, inf.read('mimetype')) - for path in namelist: - data = inf.read(path) - zi = ZipInfo(path) - zi.compress_type=ZIP_DEFLATED - try: - # get the file info, including time-stamp - oldzi = inf.getinfo(path) - # copy across useful fields - zi.date_time = oldzi.date_time - zi.comment = oldzi.comment - zi.extra = oldzi.extra - zi.internal_attr = oldzi.internal_attr - # external attributes are dependent on the create system, so copy both. - zi.external_attr = oldzi.external_attr - zi.create_system = oldzi.create_system - except: - pass - outf.writestr(zi, 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(): - sys.stdout=SafeUnbuffered(sys.stdout) - sys.stderr=SafeUnbuffered(sys.stderr) - 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) - 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(): - try: - import Tkinter - import Tkconstants - import tkFileDialog - import tkMessageBox - import traceback - except: - return cli_main() - - class DecryptionDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - self.status = Tkinter.Label(self, text=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: - self.status['text'] = u"File successfully decrypted" - else: - self.status['text'] = u"The was an error decrypting the file." - - root = Tkinter.Tk() - root.title(u"Barnes & Noble ePub Decrypter v.{0}".format(__version__)) - 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/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignoblekey.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignoblekey.py deleted file mode 100644 index dbadc5d..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignoblekey.py +++ /dev/null @@ -1,336 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# ignoblekey.py -# Copyright © 2015 Apprentice Alf and Apprentice Harper - -# Based on kindlekey.py, Copyright © 2010-2013 by some_updates and Apprentice Alf - -# Released under the terms of the GNU General Public Licence, version 3 -# - -# Revision history: -# 1.0 - Initial release -# 1.1 - remove duplicates and return last key as single key - -""" -Get Barnes & Noble EPUB user key from nook Studio log file -""" - -__license__ = 'GPL v3' -__version__ = "1.1" - -import sys -import os -import hashlib -import getopt -import re - -# 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 '?'. So use shell32.GetCommandLineArgvW to get sys.argv - # as a list of Unicode strings and encode them as utf-8 - - from ctypes import POINTER, byref, cdll, c_int, windll - from ctypes.wintypes import LPCWSTR, LPWSTR - - GetCommandLineW = cdll.kernel32.GetCommandLineW - GetCommandLineW.argtypes = [] - GetCommandLineW.restype = LPCWSTR - - CommandLineToArgvW = windll.shell32.CommandLineToArgvW - CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] - CommandLineToArgvW.restype = POINTER(LPWSTR) - - cmd = GetCommandLineW() - argc = c_int(0) - argv = CommandLineToArgvW(cmd, byref(argc)) - if argc.value > 0: - # Remove Python executable and commands if present - start = argc.value - len(sys.argv) - return [argv[i] 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"ignoblekey.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 - -# Locate all of the nookStudy/nook for PC/Mac log file and return as list -def getNookLogFiles(): - logFiles = [] - found = False - if iswindows: - import _winreg as winreg - - # some 64 bit machines do not have the proper registry key for some reason - # or the python interface to the 32 vs 64 bit registry is broken - paths = set() - if 'LOCALAPPDATA' in os.environ.keys(): - # Python 2.x does not return unicode env. Use Python 3.x - path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%") - if os.path.isdir(path): - paths.add(path) - if 'USERPROFILE' in os.environ.keys(): - # Python 2.x does not return unicode env. Use Python 3.x - path = winreg.ExpandEnvironmentStrings(u"%USERPROFILE%")+u"\\AppData\\Local" - if os.path.isdir(path): - paths.add(path) - path = winreg.ExpandEnvironmentStrings(u"%USERPROFILE%")+u"\\AppData\\Roaming" - if os.path.isdir(path): - paths.add(path) - # User Shell Folders show take precedent over Shell Folders if present - try: - regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\") - path = winreg.QueryValueEx(regkey, 'Local AppData')[0] - if os.path.isdir(path): - paths.add(path) - except WindowsError: - pass - try: - regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\") - path = winreg.QueryValueEx(regkey, 'AppData')[0] - if os.path.isdir(path): - paths.add(path) - except WindowsError: - pass - try: - regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\") - path = winreg.QueryValueEx(regkey, 'Local AppData')[0] - if os.path.isdir(path): - paths.add(path) - except WindowsError: - pass - try: - regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\") - path = winreg.QueryValueEx(regkey, 'AppData')[0] - if os.path.isdir(path): - paths.add(path) - except WindowsError: - pass - - for path in paths: - # look for nookStudy log file - logpath = path +'\\Barnes & Noble\\NOOKstudy\\logs\\BNClientLog.txt' - if os.path.isfile(logpath): - found = True - print('Found nookStudy log file: ' + logpath.encode('ascii','ignore')) - logFiles.append(logpath) - else: - home = os.getenv('HOME') - # check for BNClientLog.txt in various locations - testpath = home + '/Library/Application Support/Barnes & Noble/DesktopReader/logs/BNClientLog.txt' - if os.path.isfile(testpath): - logFiles.append(testpath) - print('Found nookStudy log file: ' + testpath) - found = True - testpath = home + '/Library/Application Support/Barnes & Noble/DesktopReader/indices/BNClientLog.txt' - if os.path.isfile(testpath): - logFiles.append(testpath) - print('Found nookStudy log file: ' + testpath) - found = True - testpath = home + '/Library/Application Support/Barnes & Noble/BNDesktopReader/logs/BNClientLog.txt' - if os.path.isfile(testpath): - logFiles.append(testpath) - print('Found nookStudy log file: ' + testpath) - found = True - testpath = home + '/Library/Application Support/Barnes & Noble/BNDesktopReader/indices/BNClientLog.txt' - if os.path.isfile(testpath): - logFiles.append(testpath) - print('Found nookStudy log file: ' + testpath) - found = True - - if not found: - print('No nook Study log files have been found.') - return logFiles - - -# Extract CCHash key(s) from log file -def getKeysFromLog(kLogFile): - keys = [] - regex = re.compile("ccHash: \"(.{28})\""); - for line in open(kLogFile): - for m in regex.findall(line): - keys.append(m) - return keys - -# interface for calibre plugin -def nookkeys(files = []): - keys = [] - if files == []: - files = getNookLogFiles() - for file in files: - fileKeys = getKeysFromLog(file) - if fileKeys: - print u"Found {0} keys in the Nook Study log files".format(len(fileKeys)) - keys.extend(fileKeys) - return list(set(keys)) - -# interface for Python DeDRM -# returns single key or multiple keys, depending on path or file passed in -def getkey(outpath, files=[]): - keys = nookkeys(files) - if len(keys) > 0: - if not os.path.isdir(outpath): - outfile = outpath - with file(outfile, 'w') as keyfileout: - keyfileout.write(keys[-1]) - print u"Saved a key to {0}".format(outfile) - else: - keycount = 0 - for key in keys: - while True: - keycount += 1 - outfile = os.path.join(outpath,u"nookkey{0:d}.b64".format(keycount)) - if not os.path.exists(outfile): - break - with file(outfile, 'w') as keyfileout: - keyfileout.write(key) - print u"Saved a key to {0}".format(outfile) - return True - return False - -def usage(progname): - print u"Finds the nook Study encryption keys." - print u"Keys are saved to the current directory, or a specified output directory." - print u"If a file name is passed instead of a directory, only the first key is saved, in that file." - print u"Usage:" - print u" {0:s} [-h] [-k ] []".format(progname) - - -def cli_main(): - sys.stdout=SafeUnbuffered(sys.stdout) - sys.stderr=SafeUnbuffered(sys.stderr) - argv=unicode_argv() - progname = os.path.basename(argv[0]) - print u"{0} v{1}\nCopyright © 2015 Apprentice Alf".format(progname,__version__) - - try: - opts, args = getopt.getopt(argv[1:], "hk:") - except getopt.GetoptError, err: - print u"Error in options or arguments: {0}".format(err.args[0]) - usage(progname) - sys.exit(2) - - files = [] - for o, a in opts: - if o == "-h": - usage(progname) - sys.exit(0) - if o == "-k": - files = [a] - - if len(args) > 1: - usage(progname) - sys.exit(2) - - if len(args) == 1: - # save to the specified file or directory - outpath = args[0] - if not os.path.isabs(outpath): - outpath = os.path.abspath(outpath) - else: - # save to the same directory as the script - outpath = os.path.dirname(argv[0]) - - # make sure the outpath is the - outpath = os.path.realpath(os.path.normpath(outpath)) - - if not getkey(outpath, files): - print u"Could not retrieve nook Study key." - return 0 - - -def gui_main(): - try: - import Tkinter - import Tkconstants - import tkMessageBox - import traceback - except: - return cli_main() - - class ExceptionDialog(Tkinter.Frame): - def __init__(self, root, text): - Tkinter.Frame.__init__(self, root, border=5) - 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) - - - argv=unicode_argv() - root = Tkinter.Tk() - root.withdraw() - progpath, progname = os.path.split(argv[0]) - success = False - try: - keys = nookkeys() - keycount = 0 - for key in keys: - print key - while True: - keycount += 1 - outfile = os.path.join(progpath,u"nookkey{0:d}.b64".format(keycount)) - if not os.path.exists(outfile): - break - - with file(outfile, 'w') as keyfileout: - keyfileout.write(key) - success = True - tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile)) - except DrmException, e: - tkMessageBox.showerror(progname, u"Error: {0}".format(str(e))) - except Exception: - root.wm_state('normal') - root.title(progname) - text = traceback.format_exc() - ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1) - root.mainloop() - if not success: - return 1 - return 0 - -if __name__ == '__main__': - if len(sys.argv) > 1: - sys.exit(cli_main()) - sys.exit(gui_main()) diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignoblekeyfetch.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignoblekeyfetch.py deleted file mode 100644 index e9637a1..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignoblekeyfetch.py +++ /dev/null @@ -1,258 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# ignoblekeyfetch.pyw, version 1.1 -# Copyright © 2015 Apprentice Harper - -# Released under the terms of the GNU General Public Licence, version 3 -# - -# Based on discoveries by "Nobody You Know" -# Code partly based on ignoblekeygen.py by several people. - -# Windows users: Before running this program, you must first install Python. -# We recommend ActiveState Python 2.7.X for Windows from -# http://www.activestate.com/activepython/downloads. -# Then save this script file as ignoblekeyfetch.pyw and double-click on it to run it. -# -# Mac OS X users: Save this script file as ignoblekeyfetch.pyw. You can run this -# program from the command line (python ignoblekeyfetch.pyw) or by double-clicking -# it when it has been associated with PythonLauncher. - -# Revision history: -# 1.0 - Initial version -# 1.1 - Try second URL if first one fails - -""" -Fetch Barnes & Noble EPUB user key from B&N servers using email and password -""" - -__license__ = 'GPL v3' -__version__ = "1.1" - -import sys -import os - -# 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 '?'. So use shell32.GetCommandLineArgvW to get sys.argv - # as a list of Unicode strings and encode them as utf-8 - - from ctypes import POINTER, byref, cdll, c_int, windll - from ctypes.wintypes import LPCWSTR, LPWSTR - - GetCommandLineW = cdll.kernel32.GetCommandLineW - GetCommandLineW.argtypes = [] - GetCommandLineW.restype = LPCWSTR - - CommandLineToArgvW = windll.shell32.CommandLineToArgvW - CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] - CommandLineToArgvW.restype = POINTER(LPWSTR) - - cmd = GetCommandLineW() - argc = c_int(0) - argv = CommandLineToArgvW(cmd, byref(argc)) - if argc.value > 0: - # Remove Python executable and commands if present - start = argc.value - len(sys.argv) - return [argv[i] 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"ignoblekeyfetch.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 fetch_key(email, password): - # change email and password to utf-8 if unicode - if type(email)==unicode: - email = email.encode('utf-8') - if type(password)==unicode: - password = password.encode('utf-8') - - import random - random = "%030x" % random.randrange(16**30) - - import urllib, urllib2, re - - # try the URL from nook for PC - fetch_url = "https://cart4.barnesandnoble.com/services/service.aspx?Version=2&acctPassword=" - fetch_url += urllib.quote(password,'')+"&devID=PC_BN_2.5.6.9575_"+random+"&emailAddress=" - fetch_url += urllib.quote(email,"")+"&outFormat=5&schema=1&service=1&stage=deviceHashB" - #print fetch_url - - found = '' - try: - req = urllib2.Request(fetch_url) - response = urllib2.urlopen(req) - the_page = response.read() - #print the_page - found = re.search('ccHash>(.+?)(.+?) ".format(progname) - return 1 - email, password, keypath = argv[1:] - userkey = fetch_key(email, password) - if len(userkey) == 28: - open(keypath,'wb').write(userkey) - return 0 - print u"Failed to fetch key." - return 1 - - -def gui_main(): - try: - import Tkinter - import tkFileDialog - import Tkconstants - import tkMessageBox - import traceback - except: - return cli_main() - - class DecryptionDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - self.status = Tkinter.Label(self, text=u"Enter parameters") - self.status.pack(fill=Tkconstants.X, expand=1) - body = Tkinter.Frame(self) - body.pack(fill=Tkconstants.X, expand=1) - sticky = Tkconstants.E + Tkconstants.W - body.grid_columnconfigure(1, weight=2) - Tkinter.Label(body, text=u"Account email address").grid(row=0) - self.name = Tkinter.Entry(body, width=40) - self.name.grid(row=0, column=1, sticky=sticky) - Tkinter.Label(body, text=u"Account password").grid(row=1) - self.ccn = Tkinter.Entry(body, width=40) - self.ccn.grid(row=1, column=1, sticky=sticky) - Tkinter.Label(body, text=u"Output file").grid(row=2) - self.keypath = Tkinter.Entry(body, width=40) - self.keypath.grid(row=2, column=1, sticky=sticky) - self.keypath.insert(2, u"bnepubkey.b64") - button = Tkinter.Button(body, text=u"...", command=self.get_keypath) - button.grid(row=2, column=2) - buttons = Tkinter.Frame(self) - buttons.pack() - botton = Tkinter.Button( - buttons, text=u"Fetch", width=10, command=self.generate) - botton.pack(side=Tkconstants.LEFT) - Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) - button = Tkinter.Button( - buttons, text=u"Quit", width=10, command=self.quit) - button.pack(side=Tkconstants.RIGHT) - - def get_keypath(self): - keypath = tkFileDialog.asksaveasfilename( - parent=None, title=u"Select B&N ePub key file to produce", - defaultextension=u".b64", - filetypes=[('base64-encoded files', '.b64'), - ('All Files', '.*')]) - if keypath: - keypath = os.path.normpath(keypath) - self.keypath.delete(0, Tkconstants.END) - self.keypath.insert(0, keypath) - return - - def generate(self): - email = self.name.get() - password = self.ccn.get() - keypath = self.keypath.get() - if not email: - self.status['text'] = u"Email address not given" - return - if not password: - self.status['text'] = u"Account password not given" - return - if not keypath: - self.status['text'] = u"Output keyfile path not set" - return - self.status['text'] = u"Fetching..." - try: - userkey = fetch_key(email, password) - except Exception, e: - self.status['text'] = u"Error: {0}".format(e.args[0]) - return - if len(userkey) == 28: - open(keypath,'wb').write(userkey) - self.status['text'] = u"Keyfile fetched successfully" - else: - self.status['text'] = u"Keyfile fetch failed." - - root = Tkinter.Tk() - root.title(u"Barnes & Noble ePub Keyfile Fetch 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.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 deleted file mode 100644 index d2917c7..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignoblekeygen.py +++ /dev/null @@ -1,332 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# ignoblekeygen.pyw, version 2.5 -# Copyright © 2009-2010 i♥cabbages - -# Released under the terms of the GNU General Public Licence, version 3 -# - -# Modified 2010–2013 by some_updates, DiapDealer and Apprentice Alf - -# Windows users: Before running this program, you must first install Python. -# We recommend ActiveState Python 2.7.X for Windows (x86) from -# http://www.activestate.com/activepython/downloads. -# You must also install PyCrypto from -# http://www.voidspace.org.uk/python/modules.shtml#pycrypto -# (make certain to install the version for Python 2.7). -# Then 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 (python ignoblekeygen.pyw) or by double-clicking -# it when it has been associated with PythonLauncher. - -# Revision history: -# 1 - Initial release -# 2 - Add OS X support by using OpenSSL when available (taken/modified from ineptepub v5) -# 2.1 - Allow Windows versions of libcrypto to be found -# 2.2 - On Windows try PyCrypto first and then OpenSSL next -# 2.3 - Modify interface to allow use of import -# 2.4 - Improvements to UI and now works in plugins -# 2.5 - Additional improvement for unicode and plugin support -# 2.6 - moved unicode_argv call inside main for Windows DeDRM compatibility -# 2.7 - Work if TkInter is missing -# 2.8 - Fix bug in stand-alone use (import tkFileDialog) - -""" -Generate Barnes & Noble EPUB user key from name and credit card number. -""" - -__license__ = 'GPL v3' -__version__ = "2.8" - -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) - -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 '?'. So use shell32.GetCommandLineArgvW to get sys.argv - # as a list of Unicode strings and encode them as utf-8 - - from ctypes import POINTER, byref, cdll, c_int, windll - from ctypes.wintypes import LPCWSTR, LPWSTR - - GetCommandLineW = cdll.kernel32.GetCommandLineW - GetCommandLineW.argtypes = [] - GetCommandLineW.restype = LPCWSTR - - CommandLineToArgvW = windll.shell32.CommandLineToArgvW - CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] - CommandLineToArgvW.restype = POINTER(LPWSTR) - - cmd = GetCommandLineW() - argc = c_int(0) - argv = CommandLineToArgvW(cmd, byref(argc)) - if argc.value > 0: - # Remove Python executable and commands if present - start = argc.value - len(sys.argv) - return [argv[i] for i in - xrange(start, argc.value)] - # if we don't have any arguments at all, just pass back script name - # this should never happen - return [u"ignoblekeygen.py"] - else: - argvencoding = sys.stdin.encoding - if argvencoding == None: - argvencoding = "utf-8" - return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv] - - -class IGNOBLEError(Exception): - pass - -def _load_crypto_libcrypto(): - from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \ - Structure, c_ulong, create_string_buffer, cast - from ctypes.util import find_library - - if iswindows: - libcrypto = find_library('libeay32') - else: - libcrypto = find_library('crypto') - - if libcrypto is None: - raise IGNOBLEError('libcrypto not found') - libcrypto = CDLL(libcrypto) - - AES_MAXNR = 14 - - c_char_pp = POINTER(c_char_p) - c_int_p = POINTER(c_int) - - class AES_KEY(Structure): - _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), - ('rounds', c_int)] - AES_KEY_p = POINTER(AES_KEY) - - def F(restype, name, argtypes): - func = getattr(libcrypto, name) - func.restype = restype - func.argtypes = argtypes - return func - - AES_set_encrypt_key = F(c_int, 'AES_set_encrypt_key', - [c_char_p, c_int, AES_KEY_p]) - AES_cbc_encrypt = F(None, 'AES_cbc_encrypt', - [c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p, - c_int]) - - class AES(object): - def __init__(self, userkey, iv): - self._blocksize = len(userkey) - self._iv = iv - key = self._key = AES_KEY() - rv = AES_set_encrypt_key(userkey, len(userkey) * 8, key) - if rv < 0: - raise IGNOBLEError('Failed to initialize AES Encrypt key') - - def encrypt(self, data): - out = create_string_buffer(len(data)) - rv = AES_cbc_encrypt(data, out, len(data), self._key, self._iv, 1) - if rv == 0: - raise IGNOBLEError('AES encryption failed') - return out.raw - - return AES - -def _load_crypto_pycrypto(): - from Crypto.Cipher import AES as _AES - - class AES(object): - def __init__(self, key, iv): - self._aes = _AES.new(key, _AES.MODE_CBC, iv) - - def encrypt(self, data): - return self._aes.encrypt(data) - - return AES - -def _load_crypto(): - AES = None - cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto) - if sys.platform.startswith('win'): - cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto) - for loader in cryptolist: - try: - AES = loader() - break - except (ImportError, IGNOBLEError): - pass - return AES - -AES = _load_crypto() - -def normalize_name(name): - return ''.join(x for x in name.lower() if x != ' ') - - -def generate_key(name, ccn): - # remove spaces and case from name and CC numbers. - if type(name)==unicode: - name = name.encode('utf-8') - if type(ccn)==unicode: - ccn = ccn.encode('utf-8') - - name = normalize_name(name) + '\x00' - ccn = normalize_name(ccn) + '\x00' - - name_sha = hashlib.sha1(name).digest()[:16] - ccn_sha = hashlib.sha1(ccn).digest()[:16] - both_sha = hashlib.sha1(name + ccn).digest() - aes = AES(ccn_sha, name_sha) - crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c)) - userkey = hashlib.sha1(crypt).digest() - return userkey.encode('base64') - - - - -def cli_main(): - sys.stdout=SafeUnbuffered(sys.stdout) - sys.stderr=SafeUnbuffered(sys.stderr) - argv=unicode_argv() - progname = os.path.basename(argv[0]) - if AES is None: - print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \ - "separately. Read the top-of-script comment for details." % \ - (progname,) - return 1 - if len(argv) != 4: - print u"usage: {0} ".format(progname) - return 1 - name, ccn, keypath = argv[1:] - userkey = generate_key(name, ccn) - open(keypath,'wb').write(userkey) - return 0 - - -def gui_main(): - try: - import Tkinter - import Tkconstants - import tkMessageBox - import tkFileDialog - import traceback - except: - return cli_main() - - class DecryptionDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - self.status = Tkinter.Label(self, text=u"Enter parameters") - self.status.pack(fill=Tkconstants.X, expand=1) - body = Tkinter.Frame(self) - body.pack(fill=Tkconstants.X, expand=1) - sticky = Tkconstants.E + Tkconstants.W - body.grid_columnconfigure(1, weight=2) - Tkinter.Label(body, text=u"Account Name").grid(row=0) - self.name = Tkinter.Entry(body, width=40) - self.name.grid(row=0, column=1, sticky=sticky) - Tkinter.Label(body, text=u"CC#").grid(row=1) - self.ccn = Tkinter.Entry(body, width=40) - self.ccn.grid(row=1, column=1, sticky=sticky) - Tkinter.Label(body, text=u"Output file").grid(row=2) - self.keypath = Tkinter.Entry(body, width=40) - self.keypath.grid(row=2, column=1, sticky=sticky) - self.keypath.insert(2, u"bnepubkey.b64") - button = Tkinter.Button(body, text=u"...", command=self.get_keypath) - button.grid(row=2, column=2) - buttons = Tkinter.Frame(self) - buttons.pack() - botton = Tkinter.Button( - buttons, text=u"Generate", width=10, command=self.generate) - botton.pack(side=Tkconstants.LEFT) - Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) - button = Tkinter.Button( - buttons, text=u"Quit", width=10, command=self.quit) - button.pack(side=Tkconstants.RIGHT) - - def get_keypath(self): - keypath = tkFileDialog.asksaveasfilename( - parent=None, title=u"Select B&N ePub key file to produce", - defaultextension=u".b64", - filetypes=[('base64-encoded files', '.b64'), - ('All Files', '.*')]) - if keypath: - keypath = os.path.normpath(keypath) - self.keypath.delete(0, Tkconstants.END) - self.keypath.insert(0, keypath) - return - - def generate(self): - name = self.name.get() - ccn = self.ccn.get() - keypath = self.keypath.get() - if not name: - self.status['text'] = u"Name not specified" - return - if not ccn: - self.status['text'] = u"Credit card number not specified" - return - if not keypath: - self.status['text'] = u"Output keyfile path not specified" - return - self.status['text'] = u"Generating..." - try: - userkey = generate_key(name, ccn) - except Exception, e: - self.status['text'] = u"Error: (0}".format(e.args[0]) - return - open(keypath,'wb').write(userkey) - self.status['text'] = u"Keyfile successfully generated" - - root = Tkinter.Tk() - if AES is None: - root.withdraw() - tkMessageBox.showerror( - "Ignoble EPUB Keyfile Generator", - "This script requires OpenSSL or PyCrypto, which must be installed " - "separately. Read the top-of-script comment for details.") - return 1 - root.title(u"Barnes & Noble ePub Keyfile Generator v.{0}".format(__version__)) - root.resizable(True, False) - root.minsize(300, 0) - DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1) - root.mainloop() - return 0 - -if __name__ == '__main__': - if len(sys.argv) > 1: - sys.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 deleted file mode 100644 index ae30c04..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptepub.py +++ /dev/null @@ -1,601 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# ineptepub.pyw, version 6.6 -# Copyright © 2009-2010 by i♥cabbages - -# Released under the terms of the GNU General Public Licence, version 3 -# - -# Modified 2010–2013 by some_updates, DiapDealer and Apprentice Alf -# Modified 2015–2017 by Apprentice Harper - -# Windows users: Before running this program, you must first install Python 2.7 -# from and PyCrypto from -# (make sure to -# install the version for Python 2.7). 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 - Rename to INEPT, fix exit code -# 5 - Version bump to avoid (?) confusion; -# Improve OS X support by using OpenSSL when available -# 5.1 - Improve OpenSSL error checking -# 5.2 - Fix ctypes error causing segfaults on some systems -# 5.3 - add support for OpenSSL on Windows, fix bug with some versions of libcrypto 0.9.8 prior to path level o -# 5.4 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml -# 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 -# 5.9 - Fixed to retain zip file metadata (e.g. file modification date) -# 6.0 - moved unicode_argv call inside main for Windows DeDRM compatibility -# 6.1 - Work if TkInter is missing -# 6.2 - Handle UTF-8 file names inside an ePub, fix by Jose Luis -# 6.3 - Add additional check on DER file sanity -# 6.4 - Remove erroneous check on DER file sanity -# 6.5 - Completely remove erroneous check on DER file sanity -# 6.6 - Import tkFileDialog, don't assume something else will import it. - -""" -Decrypt Adobe Digital Editions encrypted ePub books. -""" - -__license__ = 'GPL v3' -__version__ = "6.6" - -import sys -import os -import traceback -import zlib -import zipfile -from zipfile import ZipInfo, 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 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 iswindows: - libcrypto = find_library('libeay32') - else: - libcrypto = find_library('crypto') - - if libcrypto is None: - raise ADEPTError('libcrypto not found') - 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 - - 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) - - 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) - -AES, RSA = _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 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.encode('utf-8') 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 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: - 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) - 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') - zi.compress_type=ZIP_STORED - try: - # if the mimetype is present, get its info, including time-stamp - oldzi = inf.getinfo('mimetype') - # copy across fields to be preserved - zi.date_time = oldzi.date_time - zi.comment = oldzi.comment - zi.extra = oldzi.extra - zi.internal_attr = oldzi.internal_attr - # external attributes are dependent on the create system, so copy both. - zi.external_attr = oldzi.external_attr - zi.create_system = oldzi.create_system - except: - pass - outf.writestr(zi, inf.read('mimetype')) - for path in namelist: - data = inf.read(path) - zi = ZipInfo(path) - zi.compress_type=ZIP_DEFLATED - try: - # get the file info, including time-stamp - oldzi = inf.getinfo(path) - # copy across useful fields - zi.date_time = oldzi.date_time - zi.comment = oldzi.comment - zi.extra = oldzi.extra - zi.internal_attr = oldzi.internal_attr - # external attributes are dependent on the create system, so copy both. - zi.external_attr = oldzi.external_attr - zi.create_system = oldzi.create_system - except: - pass - outf.writestr(zi, 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(): - sys.stdout=SafeUnbuffered(sys.stdout) - sys.stderr=SafeUnbuffered(sys.stderr) - 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) - 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(): - try: - import Tkinter - import Tkconstants - import tkFileDialog - import tkMessageBox - import traceback - except: - return cli_main() - - class DecryptionDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - self.status = Tkinter.Label(self, text=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.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() - 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) - root.mainloop() - return 0 - -if __name__ == '__main__': - if len(sys.argv) > 1: - sys.exit(cli_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 deleted file mode 100644 index 0da2993..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptpdf.py +++ /dev/null @@ -1,2350 +0,0 @@ -#! /usr/bin/python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# ineptpdf.pyw, version 8.0.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 -# Modified 2015-2017 by Apprentice Harper - -# Windows users: Before running this program, you must first install Python 2.7 -# from and PyCrypto from -# (make sure to -# install the version for Python 2.7). Save this script file as -# ineptpdf.pyw and double-click on it to run it. -# -# Mac OS X users: Save this script file as ineptpdf.pyw. You can run this -# program from the command line (pythonw ineptpdf.pyw) or by double-clicking -# it when it has been associated with PythonLauncher. - -# 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 -# 5 - Automated decryption of a complete directory -# 6.1 - backward compatibility for 1.7.1 and old adeptkey.der -# 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 -# 7.2 - Correct malformed Mac OS resource forks for Stanza (anon2) -# - Support for cross ref streams on output (decreases file size) -# 7.3 - Correct bug in trailer with cross ref stream that caused the error -# "The root object is missing or invalid" in Adobe Reader. (anon2) -# 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) -# 7.5 - allow support for OpenSSL to replace pycrypto on all platforms -# implemented ARC4 interface to OpenSSL -# fixed minor typos -# 7.6 - backported AES and other fixes from version 8.4.48 -# 7.7 - On Windows try PyCrypto first and OpenSSL next -# 7.8 - Modify interface to allow use of import -# 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 -# 7.13 - Fixed erroneous mentions of ineptepub -# 7.14 - moved unicode_argv call inside main for Windows DeDRM compatibility -# 8.0 - Work if TkInter is missing -# 8.0.1 - Broken Metadata fix. -# 8.0.2 - Add additional check on DER file sanity -# 8.0.3 - Remove erroneous check on DER file sanity -# 8.0.4 - Completely remove erroneous check on DER file sanity -# 8.0.5 - Do not process DRM-free documents -# 8.0.6 - Replace use of float by Decimal for greater precision, and import tkFileDialog - - -""" -Decrypts Adobe ADEPT-encrypted PDF files. -""" - -__license__ = 'GPL v3' -__version__ = "8.0.6" - -import sys -import os -import re -import zlib -import struct -import hashlib -from decimal import * -from itertools import chain, islice -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) - -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"ineptpdf.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 - - -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) -ARC4, RSA, AES = _load_crypto() - - -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_decimal, 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_decimal, j+1) - try: - self.add_token(int(self.token)) - except ValueError: - pass - return (self.parse_main, j) - - def parse_decimal(self, s, i): - m = END_NUMBER.search(s, i) - if not m: - self.token += s[i:] - return (self.parse_decimal, len(s)) - j = m.start(0) - self.token += s[i:j] - self.add_token(Decimal(self.token)) - 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, Decimal) 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: - print "Incomplete dictionary construct" - objs.append("") # this isn't necessary. - # temporary fix. is this due to rental books? - # raise PSSyntaxError( - # 'Invalid dictionary construct: %r' % objs) - d = dict((literal_name(k), v) \ - for (k,v) in choplist(2, objs)) - self.push((pos, d)) - except PSTypeError: - if STRICT: raise - else: - self.do_keyword(pos, token) - if self.context: - continue - else: - if direct: - return self.pop(1)[0] - self.flush() - obj = self.results.pop(0) - return obj - - -LITERAL_CRYPT = PSLiteralTable.intern('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 decimal_value(x): - x = resolve1(x) - if not isinstance(x, Decimal): - if STRICT: - raise PDFTypeError('Decimal required: %r' % x) - return 0.0 - return x - -def num_value(x): - x = resolve1(x) - if not (isinstance(x, int) or isinstance(x, Decimal)): - if STRICT: - raise PDFTypeError('Int or Float required: %r' % x) - return 0 - return x - -def str_value(x): - x = resolve1(x) - if not isinstance(x, 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 - raise PDFEncryptionError('Document is not encrypted.') - return - (docid, param) = self.encryption - type = literal_name(param['Filter']) - if type == 'Adobe.APS': - return self.initialize_adobe_ps(password, docid, param) - if type == 'Standard': - return self.initialize_standard(password, docid, param) - if type == 'EBX_HANDLER': - return self.initialize_ebx(password, docid, param) - raise PDFEncryptionError('Unknown filter: param=%r' % param) - - def initialize_adobe_ps(self, password, docid, param): - global KEYFILEPATH - self.decrypt_key = self.genkey_adobe_ps(param) - self.genkey = self.genkey_v4 - self.decipher = self.decrypt_aes - self.ready = True - return - - def genkey_adobe_ps(self, param): - # nice little offline principal keys dictionary - # global static principal key for German Onleihe / Bibliothek Digital - principalkeys = { '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 - 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) - 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, userkey): - global GEN_XREF_STM, gen_xref_stm - gen_xref_stm = GEN_XREF_STM > 1 - self.version = inf.read(8) - inf.seek(0) - self.doc = doc = PDFDocument() - parser = PDFParser(doc, inf) - doc.initialize(userkey) - self.objids = objids = set() - for xref in reversed(doc.xrefs): - trailer = xref.trailer - for objid in xref.objids(): - objids.add(objid) - trailer = dict(trailer) - trailer.pop('Prev', None) - trailer.pop('XRefStm', None) - if 'Encrypt' in trailer: - objids.remove(trailer.pop('Encrypt').objid) - self.trailer = trailer - - def dump(self, outf): - self.outf = outf - self.write(self.version) - self.write('\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)): - if self.last.isalnum(): - self.write(' ') - self.write(str(obj)) - elif isinstance(obj, Decimal): - 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 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, userkey) - #except: - # 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 - try: - serializer.dump(outf) - except Exception, e: - print u"error writing pdf: {0}".format(e.args[0]) - return 2 - return 0 - - -def cli_main(): - sys.stdout=SafeUnbuffered(sys.stdout) - sys.stderr=SafeUnbuffered(sys.stderr) - argv=unicode_argv() - progname = os.path.basename(argv[0]) - if len(argv) != 4: - print u"usage: {0} ".format(progname) - return 1 - keypath, inpath, outpath = argv[1:] - userkey = open(keypath,'rb').read() - result = decryptBook(userkey, inpath, outpath) - if result == 0: - print u"Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath)) - return result - - -def gui_main(): - try: - import Tkinter - import Tkconstants - import tkFileDialog - import tkMessageBox - import traceback - except: - return cli_main() - - class DecryptionDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - self.status = Tkinter.Label(self, text=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() - tkMessageBox.showerror( - "INEPT PDF", - "This script requires OpenSSL or PyCrypto, which must be installed " - "separately. Read the top-of-script comment for details.") - return 1 - 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) - root.mainloop() - return 0 - - -if __name__ == '__main__': - if len(sys.argv) > 1: - sys.exit(cli_main()) - sys.exit(gui_main()) diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ion.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ion.py deleted file mode 100644 index 40433ca..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ion.py +++ /dev/null @@ -1,985 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# Pascal implementation by lulzkabulz. Python translation by apprenticenaomi. DeDRM integration by anon. -# BinaryIon.pas + DrmIon.pas + IonSymbols.pas - -from __future__ import with_statement - -import collections -import hashlib -import hmac -import os -import os.path -import struct - -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO - -from Crypto.Cipher import AES -from Crypto.Util.py3compat import bchr, bord - -try: - # lzma library from calibre 2.35.0 or later - import lzma.lzma1 as calibre_lzma -except ImportError: - calibre_lzma = None - try: - import lzma - except ImportError: - # Need pip backports.lzma on Python <3.3 - try: - from backports import lzma - except ImportError: - # Windows-friendly choice: pylzma wheels - import pylzma as lzma - - -TID_NULL = 0 -TID_BOOLEAN = 1 -TID_POSINT = 2 -TID_NEGINT = 3 -TID_FLOAT = 4 -TID_DECIMAL = 5 -TID_TIMESTAMP = 6 -TID_SYMBOL = 7 -TID_STRING = 8 -TID_CLOB = 9 -TID_BLOB = 0xA -TID_LIST = 0xB -TID_SEXP = 0xC -TID_STRUCT = 0xD -TID_TYPEDECL = 0xE -TID_UNUSED = 0xF - - -SID_UNKNOWN = -1 -SID_ION = 1 -SID_ION_1_0 = 2 -SID_ION_SYMBOL_TABLE = 3 -SID_NAME = 4 -SID_VERSION = 5 -SID_IMPORTS = 6 -SID_SYMBOLS = 7 -SID_MAX_ID = 8 -SID_ION_SHARED_SYMBOL_TABLE = 9 -SID_ION_1_0_MAX = 10 - - -LEN_IS_VAR_LEN = 0xE -LEN_IS_NULL = 0xF - - -VERSION_MARKER = b"\x01\x00\xEA" - - -# asserts must always raise exceptions for proper functioning -def _assert(test, msg="Exception"): - if not test: - raise Exception(msg) - - -class SystemSymbols(object): - ION = '$ion' - ION_1_0 = '$ion_1_0' - ION_SYMBOL_TABLE = '$ion_symbol_table' - NAME = 'name' - VERSION = 'version' - IMPORTS = 'imports' - SYMBOLS = 'symbols' - MAX_ID = 'max_id' - ION_SHARED_SYMBOL_TABLE = '$ion_shared_symbol_table' - - -class IonCatalogItem(object): - name = "" - version = 0 - symnames = [] - - def __init__(self, name, version, symnames): - self.name = name - self.version = version - self.symnames = symnames - - -class SymbolToken(object): - text = "" - sid = 0 - - def __init__(self, text, sid): - if text == "" and sid == 0: - raise ValueError("Symbol token must have Text or SID") - - self.text = text - self.sid = sid - - -class SymbolTable(object): - table = None - - def __init__(self): - self.table = [None] * SID_ION_1_0_MAX - self.table[SID_ION] = SystemSymbols.ION - self.table[SID_ION_1_0] = SystemSymbols.ION_1_0 - self.table[SID_ION_SYMBOL_TABLE] = SystemSymbols.ION_SYMBOL_TABLE - self.table[SID_NAME] = SystemSymbols.NAME - self.table[SID_VERSION] = SystemSymbols.VERSION - self.table[SID_IMPORTS] = SystemSymbols.IMPORTS - self.table[SID_SYMBOLS] = SystemSymbols.SYMBOLS - self.table[SID_MAX_ID] = SystemSymbols.MAX_ID - self.table[SID_ION_SHARED_SYMBOL_TABLE] = SystemSymbols.ION_SHARED_SYMBOL_TABLE - - def findbyid(self, sid): - if sid < 1: - raise ValueError("Invalid symbol id") - - if sid < len(self.table): - return self.table[sid] - else: - return "" - - def import_(self, table, maxid): - for i in range(maxid): - self.table.append(table.symnames[i]) - - def importunknown(self, name, maxid): - for i in range(maxid): - self.table.append("%s#%d" % (name, i + 1)) - - -class ParserState: - Invalid,BeforeField,BeforeTID,BeforeValue,AfterValue,EOF = 1,2,3,4,5,6 - -ContainerRec = collections.namedtuple("ContainerRec", "nextpos, tid, remaining") - - -class BinaryIonParser(object): - eof = False - state = None - localremaining = 0 - needhasnext = False - isinstruct = False - valuetid = 0 - valuefieldid = 0 - parenttid = 0 - valuelen = 0 - valueisnull = False - valueistrue = False - value = None - didimports = False - - def __init__(self, stream): - self.annotations = [] - self.catalog = [] - - self.stream = stream - self.initpos = stream.tell() - self.reset() - self.symbols = SymbolTable() - - def reset(self): - self.state = ParserState.BeforeTID - self.needhasnext = True - self.localremaining = -1 - self.eof = False - self.isinstruct = False - self.containerstack = [] - self.stream.seek(self.initpos) - - def addtocatalog(self, name, version, symbols): - self.catalog.append(IonCatalogItem(name, version, symbols)) - - def hasnext(self): - while self.needhasnext and not self.eof: - self.hasnextraw() - if len(self.containerstack) == 0 and not self.valueisnull: - if self.valuetid == TID_SYMBOL: - if self.value == SID_ION_1_0: - self.needhasnext = True - elif self.valuetid == TID_STRUCT: - for a in self.annotations: - if a == SID_ION_SYMBOL_TABLE: - self.parsesymboltable() - self.needhasnext = True - break - return not self.eof - - def hasnextraw(self): - self.clearvalue() - while self.valuetid == -1 and not self.eof: - self.needhasnext = False - if self.state == ParserState.BeforeField: - _assert(self.valuefieldid == SID_UNKNOWN) - - self.valuefieldid = self.readfieldid() - if self.valuefieldid != SID_UNKNOWN: - self.state = ParserState.BeforeTID - else: - self.eof = True - - elif self.state == ParserState.BeforeTID: - self.state = ParserState.BeforeValue - self.valuetid = self.readtypeid() - if self.valuetid == -1: - self.state = ParserState.EOF - self.eof = True - break - - if self.valuetid == TID_TYPEDECL: - if self.valuelen == 0: - self.checkversionmarker() - else: - self.loadannotations() - - elif self.state == ParserState.BeforeValue: - self.skip(self.valuelen) - self.state = ParserState.AfterValue - - elif self.state == ParserState.AfterValue: - if self.isinstruct: - self.state = ParserState.BeforeField - else: - self.state = ParserState.BeforeTID - - else: - _assert(self.state == ParserState.EOF) - - def next(self): - if self.hasnext(): - self.needhasnext = True - return self.valuetid - else: - return -1 - - def push(self, typeid, nextposition, nextremaining): - self.containerstack.append(ContainerRec(nextpos=nextposition, tid=typeid, remaining=nextremaining)) - - def stepin(self): - _assert(self.valuetid in [TID_STRUCT, TID_LIST, TID_SEXP] and not self.eof, - "valuetid=%s eof=%s" % (self.valuetid, self.eof)) - _assert((not self.valueisnull or self.state == ParserState.AfterValue) and - (self.valueisnull or self.state == ParserState.BeforeValue)) - - nextrem = self.localremaining - if nextrem != -1: - nextrem -= self.valuelen - if nextrem < 0: - nextrem = 0 - self.push(self.parenttid, self.stream.tell() + self.valuelen, nextrem) - - self.isinstruct = (self.valuetid == TID_STRUCT) - if self.isinstruct: - self.state = ParserState.BeforeField - else: - self.state = ParserState.BeforeTID - - self.localremaining = self.valuelen - self.parenttid = self.valuetid - self.clearvalue() - self.needhasnext = True - - def stepout(self): - rec = self.containerstack.pop() - - self.eof = False - self.parenttid = rec.tid - if self.parenttid == TID_STRUCT: - self.isinstruct = True - self.state = ParserState.BeforeField - else: - self.isinstruct = False - self.state = ParserState.BeforeTID - self.needhasnext = True - - self.clearvalue() - curpos = self.stream.tell() - if rec.nextpos > curpos: - self.skip(rec.nextpos - curpos) - else: - _assert(rec.nextpos == curpos) - - self.localremaining = rec.remaining - - def read(self, count=1): - if self.localremaining != -1: - self.localremaining -= count - _assert(self.localremaining >= 0) - - result = self.stream.read(count) - if len(result) == 0: - raise EOFError() - return result - - def readfieldid(self): - if self.localremaining != -1 and self.localremaining < 1: - return -1 - - try: - return self.readvaruint() - except EOFError: - return -1 - - def readtypeid(self): - if self.localremaining != -1: - if self.localremaining < 1: - return -1 - self.localremaining -= 1 - - b = self.stream.read(1) - if len(b) < 1: - return -1 - b = bord(b) - result = b >> 4 - ln = b & 0xF - - if ln == LEN_IS_VAR_LEN: - ln = self.readvaruint() - elif ln == LEN_IS_NULL: - ln = 0 - self.state = ParserState.AfterValue - elif result == TID_NULL: - # Must have LEN_IS_NULL - _assert(False) - elif result == TID_BOOLEAN: - _assert(ln <= 1) - self.valueistrue = (ln == 1) - ln = 0 - self.state = ParserState.AfterValue - elif result == TID_STRUCT: - if ln == 1: - ln = self.readvaruint() - - self.valuelen = ln - return result - - def readvarint(self): - b = bord(self.read()) - negative = ((b & 0x40) != 0) - result = (b & 0x3F) - - i = 0 - while (b & 0x80) == 0 and i < 4: - b = bord(self.read()) - result = (result << 7) | (b & 0x7F) - i += 1 - - _assert(i < 4 or (b & 0x80) != 0, "int overflow") - - if negative: - return -result - return result - - def readvaruint(self): - b = bord(self.read()) - result = (b & 0x7F) - - i = 0 - while (b & 0x80) == 0 and i < 4: - b = bord(self.read()) - result = (result << 7) | (b & 0x7F) - i += 1 - - _assert(i < 4 or (b & 0x80) != 0, "int overflow") - - return result - - def readdecimal(self): - if self.valuelen == 0: - return 0. - - rem = self.localremaining - self.valuelen - self.localremaining = self.valuelen - exponent = self.readvarint() - - _assert(self.localremaining > 0, "Only exponent in ReadDecimal") - _assert(self.localremaining <= 8, "Decimal overflow") - - signed = False - b = [bord(x) for x in self.read(self.localremaining)] - if (b[0] & 0x80) != 0: - b[0] = b[0] & 0x7F - signed = True - - # Convert variably sized network order integer into 64-bit little endian - j = 0 - vb = [0] * 8 - for i in range(len(b), -1, -1): - vb[i] = b[j] - j += 1 - - v = struct.unpack(" 0: - result = result[:-1] - return result - - def ionwalk(self, supert, indent, lst): - while self.hasnext(): - if supert == TID_STRUCT: - L = self.getfieldname() + ":" - else: - L = "" - - t = self.next() - if t in [TID_STRUCT, TID_LIST]: - if L != "": - lst.append(indent + L) - L = self.gettypename() - if L != "": - lst.append(indent + L + "::") - if t == TID_STRUCT: - lst.append(indent + "{") - else: - lst.append(indent + "[") - - self.stepin() - self.ionwalk(t, indent + " ", lst) - self.stepout() - - if t == TID_STRUCT: - lst.append(indent + "}") - else: - lst.append(indent + "]") - - else: - if t == TID_STRING: - L += ('"%s"' % self.stringvalue()) - elif t in [TID_CLOB, TID_BLOB]: - L += ("{%s}" % self.printlob(self.lobvalue())) - elif t == TID_POSINT: - L += str(self.intvalue()) - elif t == TID_SYMBOL: - tn = self.gettypename() - if tn != "": - tn += "::" - L += tn + self.symbolvalue() - elif t == TID_DECIMAL: - L += str(self.decimalvalue()) - else: - L += ("TID %d" % t) - lst.append(indent + L) - - def print_(self, lst): - self.reset() - self.ionwalk(-1, "", lst) - - -SYM_NAMES = [ 'com.amazon.drm.Envelope@1.0', - 'com.amazon.drm.EnvelopeMetadata@1.0', 'size', 'page_size', - 'encryption_key', 'encryption_transformation', - 'encryption_voucher', 'signing_key', 'signing_algorithm', - 'signing_voucher', 'com.amazon.drm.EncryptedPage@1.0', - 'cipher_text', 'cipher_iv', 'com.amazon.drm.Signature@1.0', - 'data', 'com.amazon.drm.EnvelopeIndexTable@1.0', 'length', - 'offset', 'algorithm', 'encoded', 'encryption_algorithm', - 'hashing_algorithm', 'expires', 'format', 'id', - 'lock_parameters', 'strategy', 'com.amazon.drm.Key@1.0', - 'com.amazon.drm.KeySet@1.0', 'com.amazon.drm.PIDv3@1.0', - 'com.amazon.drm.PlainTextPage@1.0', - 'com.amazon.drm.PlainText@1.0', 'com.amazon.drm.PrivateKey@1.0', - 'com.amazon.drm.PublicKey@1.0', 'com.amazon.drm.SecretKey@1.0', - 'com.amazon.drm.Voucher@1.0', 'public_key', 'private_key', - 'com.amazon.drm.KeyPair@1.0', 'com.amazon.drm.ProtectedData@1.0', - 'doctype', 'com.amazon.drm.EnvelopeIndexTableOffset@1.0', - 'enddoc', 'license_type', 'license', 'watermark', 'key', 'value', - 'com.amazon.drm.License@1.0', 'category', 'metadata', - 'categorized_metadata', 'com.amazon.drm.CategorizedMetadata@1.0', - 'com.amazon.drm.VoucherEnvelope@1.0', 'mac', 'voucher', - 'com.amazon.drm.ProtectedData@2.0', - 'com.amazon.drm.Envelope@2.0', - 'com.amazon.drm.EnvelopeMetadata@2.0', - 'com.amazon.drm.EncryptedPage@2.0', - 'com.amazon.drm.PlainText@2.0', 'compression_algorithm', - 'com.amazon.drm.Compressed@1.0', 'priority', 'refines'] - -def addprottable(ion): - ion.addtocatalog("ProtectedData", 1, SYM_NAMES) - - -def pkcs7pad(msg, blocklen): - paddinglen = blocklen - len(msg) % blocklen - padding = bchr(paddinglen) * paddinglen - return msg + padding - - -def pkcs7unpad(msg, blocklen): - _assert(len(msg) % blocklen == 0) - - paddinglen = bord(msg[-1]) - _assert(paddinglen > 0 and paddinglen <= blocklen, "Incorrect padding - Wrong key") - _assert(msg[-paddinglen:] == bchr(paddinglen) * paddinglen, "Incorrect padding - Wrong key") - - return msg[:-paddinglen] - - -class DrmIonVoucher(object): - envelope = None - voucher = None - drmkey = None - license_type = "Unknown" - - encalgorithm = "" - enctransformation = "" - hashalgorithm = "" - - lockparams = None - - ciphertext = b"" - cipheriv = b"" - secretkey = b"" - - def __init__(self, voucherenv, dsn, secret): - self.dsn,self.secret = dsn,secret - - self.lockparams = [] - - self.envelope = BinaryIonParser(voucherenv) - addprottable(self.envelope) - - def decryptvoucher(self): - shared = "PIDv3" + self.encalgorithm + self.enctransformation + self.hashalgorithm - - self.lockparams.sort() - for param in self.lockparams: - if param == "ACCOUNT_SECRET": - shared += param + self.secret - elif param == "CLIENT_ID": - shared += param + self.dsn - else: - _assert(False, "Unknown lock parameter: %s" % param) - - sharedsecret = shared.encode("UTF-8") - - key = hmac.new(sharedsecret, sharedsecret[:5], digestmod=hashlib.sha256).digest() - aes = AES.new(key[:32], AES.MODE_CBC, self.cipheriv[:16]) - b = aes.decrypt(self.ciphertext) - b = pkcs7unpad(b, 16) - - self.drmkey = BinaryIonParser(StringIO(b)) - addprottable(self.drmkey) - - _assert(self.drmkey.hasnext() and self.drmkey.next() == TID_LIST and self.drmkey.gettypename() == "com.amazon.drm.KeySet@1.0", - "Expected KeySet, got %s" % self.drmkey.gettypename()) - - self.drmkey.stepin() - while self.drmkey.hasnext(): - self.drmkey.next() - if self.drmkey.gettypename() != "com.amazon.drm.SecretKey@1.0": - continue - - self.drmkey.stepin() - while self.drmkey.hasnext(): - self.drmkey.next() - if self.drmkey.getfieldname() == "algorithm": - _assert(self.drmkey.stringvalue() == "AES", "Unknown cipher algorithm: %s" % self.drmkey.stringvalue()) - elif self.drmkey.getfieldname() == "format": - _assert(self.drmkey.stringvalue() == "RAW", "Unknown key format: %s" % self.drmkey.stringvalue()) - elif self.drmkey.getfieldname() == "encoded": - self.secretkey = self.drmkey.lobvalue() - - self.drmkey.stepout() - break - - self.drmkey.stepout() - - def parse(self): - self.envelope.reset() - _assert(self.envelope.hasnext(), "Envelope is empty") - _assert(self.envelope.next() == TID_STRUCT and self.envelope.gettypename() == "com.amazon.drm.VoucherEnvelope@1.0", - "Unknown type encountered in envelope, expected VoucherEnvelope") - - self.envelope.stepin() - while self.envelope.hasnext(): - self.envelope.next() - field = self.envelope.getfieldname() - if field == "voucher": - self.voucher = BinaryIonParser(StringIO(self.envelope.lobvalue())) - addprottable(self.voucher) - continue - elif field != "strategy": - continue - - _assert(self.envelope.gettypename() == "com.amazon.drm.PIDv3@1.0", "Unknown strategy: %s" % self.envelope.gettypename()) - - self.envelope.stepin() - while self.envelope.hasnext(): - self.envelope.next() - field = self.envelope.getfieldname() - if field == "encryption_algorithm": - self.encalgorithm = self.envelope.stringvalue() - elif field == "encryption_transformation": - self.enctransformation = self.envelope.stringvalue() - elif field == "hashing_algorithm": - self.hashalgorithm = self.envelope.stringvalue() - elif field == "lock_parameters": - self.envelope.stepin() - while self.envelope.hasnext(): - _assert(self.envelope.next() == TID_STRING, "Expected string list for lock_parameters") - self.lockparams.append(self.envelope.stringvalue()) - self.envelope.stepout() - - self.envelope.stepout() - - self.parsevoucher() - - def parsevoucher(self): - _assert(self.voucher.hasnext(), "Voucher is empty") - _assert(self.voucher.next() == TID_STRUCT and self.voucher.gettypename() == "com.amazon.drm.Voucher@1.0", - "Unknown type, expected Voucher") - - self.voucher.stepin() - while self.voucher.hasnext(): - self.voucher.next() - - if self.voucher.getfieldname() == "cipher_iv": - self.cipheriv = self.voucher.lobvalue() - elif self.voucher.getfieldname() == "cipher_text": - self.ciphertext = self.voucher.lobvalue() - elif self.voucher.getfieldname() == "license": - _assert(self.voucher.gettypename() == "com.amazon.drm.License@1.0", - "Unknown license: %s" % self.voucher.gettypename()) - self.voucher.stepin() - while self.voucher.hasnext(): - self.voucher.next() - if self.voucher.getfieldname() == "license_type": - self.license_type = self.voucher.stringvalue() - self.voucher.stepout() - - def printenvelope(self, lst): - self.envelope.print_(lst) - - def printkey(self, lst): - if self.voucher is None: - self.parse() - if self.drmkey is None: - self.decryptvoucher() - - self.drmkey.print_(lst) - - def printvoucher(self, lst): - if self.voucher is None: - self.parse() - - self.voucher.print_(lst) - - def getlicensetype(self): - return self.license_type - - -class DrmIon(object): - ion = None - voucher = None - vouchername = "" - key = b"" - onvoucherrequired = None - - def __init__(self, ionstream, onvoucherrequired): - self.ion = BinaryIonParser(ionstream) - addprottable(self.ion) - self.onvoucherrequired = onvoucherrequired - - def parse(self, outpages): - self.ion.reset() - - _assert(self.ion.hasnext(), "DRMION envelope is empty") - _assert(self.ion.next() == TID_SYMBOL and self.ion.gettypename() == "doctype", "Expected doctype symbol") - _assert(self.ion.next() == TID_LIST and self.ion.gettypename() in ["com.amazon.drm.Envelope@1.0", "com.amazon.drm.Envelope@2.0"], - "Unknown type encountered in DRMION envelope, expected Envelope, got %s" % self.ion.gettypename()) - - while True: - if self.ion.gettypename() == "enddoc": - break - - self.ion.stepin() - while self.ion.hasnext(): - self.ion.next() - - if self.ion.gettypename() in ["com.amazon.drm.EnvelopeMetadata@1.0", "com.amazon.drm.EnvelopeMetadata@2.0"]: - self.ion.stepin() - while self.ion.hasnext(): - self.ion.next() - if self.ion.getfieldname() != "encryption_voucher": - continue - - if self.vouchername == "": - self.vouchername = self.ion.stringvalue() - self.voucher = self.onvoucherrequired(self.vouchername) - self.key = self.voucher.secretkey - _assert(self.key is not None, "Unable to obtain secret key from voucher") - else: - _assert(self.vouchername == self.ion.stringvalue(), - "Unexpected: Different vouchers required for same file?") - - self.ion.stepout() - - elif self.ion.gettypename() in ["com.amazon.drm.EncryptedPage@1.0", "com.amazon.drm.EncryptedPage@2.0"]: - decompress = False - ct = None - civ = None - self.ion.stepin() - while self.ion.hasnext(): - self.ion.next() - if self.ion.gettypename() == "com.amazon.drm.Compressed@1.0": - decompress = True - if self.ion.getfieldname() == "cipher_text": - ct = self.ion.lobvalue() - elif self.ion.getfieldname() == "cipher_iv": - civ = self.ion.lobvalue() - - if ct is not None and civ is not None: - self.processpage(ct, civ, outpages, decompress) - self.ion.stepout() - - self.ion.stepout() - if not self.ion.hasnext(): - break - self.ion.next() - - def print_(self, lst): - self.ion.print_(lst) - - def processpage(self, ct, civ, outpages, decompress): - aes = AES.new(self.key[:16], AES.MODE_CBC, civ[:16]) - msg = pkcs7unpad(aes.decrypt(ct), 16) - - if not decompress: - outpages.write(msg) - return - - _assert(msg[0] == b"\x00", "LZMA UseFilter not supported") - - if calibre_lzma is not None: - with calibre_lzma.decompress(msg[1:], bufsize=0x1000000) as f: - f.seek(0) - outpages.write(f.read()) - return - - decomp = lzma.LZMADecompressor(format=lzma.FORMAT_ALONE) - while not decomp.eof: - segment = decomp.decompress(msg[1:]) - msg = b"" # Contents were internally buffered after the first call - outpages.write(segment) diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py deleted file mode 100644 index 1ce1f35..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py +++ /dev/null @@ -1,353 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# k4mobidedrm.py -# Copyright © 2008-2017 by Apprentice Harper et al. - -__license__ = 'GPL v3' -__version__ = '5.5' - -# Engine to remove drm from Kindle and Mobipocket ebooks -# 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, 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. -# 4.9 - Missed some invalid characters in cleanup_name -# 5.0 - Extraction of info from Kindle for PC/Mac moved into kindlekey.py -# - tweaked GetDecryptedBook interface to leave passed parameters unchanged -# 5.1 - moved unicode_argv call inside main for Windows DeDRM compatibility -# 5.2 - Fixed error in command line processing of unicode arguments -# 5.3 - Changed Android support to allow passing of backup .ab files -# 5.4 - Recognise KFX files masquerading as azw, even if we can't decrypt them yet. -# 5.5 - Added GPL v3 licence explicitly. -# 5.x - Invoke KFXZipBook to handle zipped KFX files - -import sys, os, re -import csv -import getopt -import re -import traceback -import time -import htmlentitydefs -import json - -class DrmException(Exception): - pass - -if 'calibre' in sys.modules: - inCalibre = True -else: - inCalibre = False - -if inCalibre: - from calibre_plugins.dedrm import mobidedrm - from calibre_plugins.dedrm import topazextract - from calibre_plugins.dedrm import kgenpids - from calibre_plugins.dedrm import androidkindlekey - from calibre_plugins.dedrm import kfxdedrm -else: - import mobidedrm - import topazextract - import kgenpids - import androidkindlekey - import kfxdedrm - -# 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"\'").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, kDatabases, androidFiles, 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 - magic8 = open(infile,'rb').read(8) - if magic8 == '\xeaDRMION\xee': - raise DrmException(u"The .kfx DRMION file cannot be decrypted by itself. A .kfx-zip archive containing a DRM voucher is required.") - - magic3 = magic8[:3] - if magic3 == 'TPZ': - mobi = False - - if magic8[:4] == 'PK\x03\x04': - mb = kfxdedrm.KFXZipBook(infile) - elif mobi: - mb = mobidedrm.MobiBook(infile) - else: - mb = topazextract.TopazBook(infile) - - bookname = unescape(mb.getBookTitle()) - print u"Decrypting {1} ebook: {0}".format(bookname, mb.getBookType()) - - # copy list of pids - totalpids = list(pids) - # extend list of serials with serials from android databases - for aFile in androidFiles: - serials.extend(androidkindlekey.get_serials(aFile)) - # extend PID list with book-specific PIDs from seriala and kDatabases - md1, md2 = mb.getPIDMetaInfo() - totalpids.extend(kgenpids.getPidList(md1, md2, serials, kDatabases)) - # remove any duplicates - totalpids = list(set(totalpids)) - print u"Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(totalpids)) - #print totalpids - - try: - mb.processBook(totalpids) - except: - mb.cleanup - raise - - print u"Decryption succeeded after {0:.1f} seconds".format(time.time()-starttime) - return mb - - -# kDatabaseFiles is a list of files created by kindlekey -def decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids): - starttime = time.time() - kDatabases = [] - for dbfile in kDatabaseFiles: - kindleDatabase = {} - try: - with open(dbfile, 'r') as keyfilein: - kindleDatabase = json.loads(keyfilein.read()) - kDatabases.append([dbfile,kindleDatabase]) - except Exception, e: - print u"Error getting database from file {0:s}: {1:s}".format(dbfile,e) - traceback.print_exc() - - - - try: - book = GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime) - except Exception, e: - print u"Error decrypting book after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime) - traceback.print_exc() - return 1 - - # Try to infer a reasonable name - orig_fn_root = os.path.splitext(os.path.basename(infile))[0] - if ( - re.match('^B[A-Z0-9]{9}(_EBOK|_EBSP|_sample)?$', orig_fn_root) or - re.match('^{0-9A-F-}{36}$', orig_fn_root) - ): # Kindle for PC / Mac / Android / Fire / iOS - clean_title = cleanup_name(book.getBookTitle()) - outfilename = '{}_{}'.format(orig_fn_root, clean_title) - else: # E Ink Kindle, which already uses a reasonable name - outfilename = orig_fn_root - - # avoid excessively long file names - if len(outfilename)>150: - outfilename = outfilename[:99]+"--"+outfilename[-49:] - - 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() - return 0 - - -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 ] [ -a ] ".format(progname) - -# -# Main -# -def cli_main(): - argv=unicode_argv() - progname = os.path.basename(argv[0]) - print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2017 Apprentice Harper et al.".format(__version__) - - try: - opts, args = getopt.getopt(argv[1:], "k:p:s:a:") - 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] - kDatabaseFiles = [] - androidFiles = [] - serials = [] - pids = [] - - for o, a in opts: - if o == "-k": - if a == None : - raise DrmException("Invalid parameter for -k") - kDatabaseFiles.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(',') - if o == '-a': - if a == None: - raise DrmException("Invalid parameter for -a") - androidFiles.append(a) - - # try with built in Kindle Info files if not on Linux - k4 = not sys.platform.startswith('linux') - - return decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids) - - -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/kfxdedrm.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kfxdedrm.py deleted file mode 100644 index c2b9bb1..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kfxdedrm.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# Engine to remove drm from Kindle KFX ebooks - -import os -import shutil -import zipfile - -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO - -try: - from calibre_plugins.dedrm import ion -except ImportError: - import ion - - -__license__ = 'GPL v3' -__version__ = '1.0' - - -class KFXZipBook: - def __init__(self, infile): - self.infile = infile - self.voucher = None - self.decrypted = {} - - def getPIDMetaInfo(self): - return (None, None) - - def processBook(self, totalpids): - with zipfile.ZipFile(self.infile, 'r') as zf: - for filename in zf.namelist(): - data = zf.read(filename) - if data.startswith('\xeaDRMION\xee'): - if self.voucher is None: - self.decrypt_voucher(totalpids) - print u'Decrypting KFX DRMION: {0}'.format(filename) - outfile = StringIO() - ion.DrmIon(StringIO(data[8:-8]), lambda name: self.voucher).parse(outfile) - self.decrypted[filename] = outfile.getvalue() - - if not self.decrypted: - print(u'The .kfx-zip archive does not contain an encrypted DRMION file') - - def decrypt_voucher(self, totalpids): - with zipfile.ZipFile(self.infile, 'r') as zf: - for info in zf.infolist(): - if info.file_size < 0x10000: - data = zf.read(info.filename) - if data.startswith('\xe0\x01\x00\xea') and 'ProtectedData' in data: - break # found DRM voucher - else: - raise Exception(u'The .kfx-zip archive contains an encrypted DRMION file without a DRM voucher') - - print u'Decrypting KFX DRM voucher: {0}'.format(info.filename) - - for pid in [''] + totalpids: - for dsn_len,secret_len in [(0,0), (16,0), (16,40), (32,40), (40,40)]: - if len(pid) == dsn_len + secret_len: - break # split pid into DSN and account secret - else: - continue - - try: - voucher = ion.DrmIonVoucher(StringIO(data), pid[:dsn_len], pid[dsn_len:]) - voucher.parse() - voucher.decryptvoucher() - break - except: - pass - else: - raise Exception(u'Failed to decrypt KFX DRM voucher with any key') - - print u'KFX DRM voucher successfully decrypted' - - license_type = voucher.getlicensetype() - if license_type != "Purchase": - raise Exception((u'This book is licensed as {0}. ' - 'These tools are intended for use on purchased books.').format(license_type)) - - self.voucher = voucher - - def getBookTitle(self): - return os.path.splitext(os.path.split(self.infile)[1])[0] - - def getBookExtension(self): - return '.kfx-zip' - - def getBookType(self): - return 'KFX-ZIP' - - def cleanup(self): - pass - - def getFile(self, outpath): - if not self.decrypted: - shutil.copyfile(self.infile, outpath) - else: - with zipfile.ZipFile(self.infile, 'r') as zif: - with zipfile.ZipFile(outpath, 'w') as zof: - for info in zif.infolist(): - zof.writestr(info, self.decrypted.get(info.filename, zif.read(info.filename))) diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kgenpids.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kgenpids.py deleted file mode 100644 index 9b4373e..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kgenpids.py +++ /dev/null @@ -1,310 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# kgenpids.py -# Copyright © 2008-2017 Apprentice Harper et al. - -__license__ = 'GPL v3' -__version__ = '2.1' - -# Revision history: -# 2.0 - Fix for non-ascii Windows user names -# 2.1 - Actual fix for non-ascii WIndows user names. -# x.x - Return information needed for KFX decryption - -import sys -import os, csv -import binascii -import zlib -import re -from struct import pack, unpack, unpack_from -import traceback - -class DrmException(Exception): - pass - -global charMap1 -global charMap3 -global charMap4 - - -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 getKindlePids(rec209, token, serialnum): - if rec209 is None: - return [serialnum] - - pids=[] - - if isinstance(serialnum,unicode): - serialnum = serialnum.encode('utf-8') - - # Compute book PID - pidHash = SHA1(serialnum+rec209+token) - bookPID = encodePID(pidHash) - bookPID = checksumPid(bookPID) - pids.append(bookPID) - - # compute fixed pid for old pre 2.5 firmware update pid as well - kindlePID = pidFromSerial(serialnum, 7) + "*" - kindlePID = checksumPid(kindlePID) - pids.append(kindlePID) - - 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'] - -def getK4Pids(rec209, token, kindleDatabase): - global charMap1 - pids = [] - - try: - # Get the kindle account token, if present - kindleAccountToken = (kindleDatabase[1])['kindle.account.tokens'].decode('hex') - - except KeyError: - kindleAccountToken="" - pass - - try: - # Get the DSN token, if present - DSN = (kindleDatabase[1])['DSN'].decode('hex') - print u"Got DSN key from database {0}".format(kindleDatabase[0]) - except KeyError: - # See if we have the info to generate the DSN - try: - # Get the Mazama Random number - MazamaRandomNumber = (kindleDatabase[1])['MazamaRandomNumber'].decode('hex') - #print u"Got MazamaRandomNumber from database {0}".format(kindleDatabase[0]) - - try: - # Get the SerialNumber token, if present - IDString = (kindleDatabase[1])['SerialNumber'].decode('hex') - print u"Got SerialNumber from database {0}".format(kindleDatabase[0]) - except KeyError: - # Get the IDString we added - IDString = (kindleDatabase[1])['IDString'].decode('hex') - - try: - # Get the UsernameHash token, if present - encodedUsername = (kindleDatabase[1])['UsernameHash'].decode('hex') - print u"Got UsernameHash from database {0}".format(kindleDatabase[0]) - except KeyError: - # Get the UserName we added - UserName = (kindleDatabase[1])['UserName'].decode('hex') - # encode it - encodedUsername = encodeHash(UserName,charMap1) - #print u"encodedUsername",encodedUsername.encode('hex') - except KeyError: - print u"Keys not found in the database {0}.".format(kindleDatabase[0]) - return pids - - # Get the ID string used - encodedIDString = encodeHash(IDString,charMap1) - #print u"encodedIDString",encodedIDString.encode('hex') - - # concat, hash and encode to calculate the DSN - DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1) - #print u"DSN",DSN.encode('hex') - pass - - if rec209 is None: - pids.append(DSN+kindleAccountToken) - return pids - - # Compute the device PID (for which I can tell, is used for nothing). - table = generatePidEncryptionTable() - devicePID = generateDevicePID(table,DSN,4) - devicePID = checksumPid(devicePID) - pids.append(devicePID) - - # Compute book PIDs - - # book pid - pidHash = SHA1(DSN+kindleAccountToken+rec209+token) - bookPID = encodePID(pidHash) - bookPID = checksumPid(bookPID) - pids.append(bookPID) - - # variant 1 - pidHash = SHA1(kindleAccountToken+rec209+token) - bookPID = encodePID(pidHash) - bookPID = checksumPid(bookPID) - pids.append(bookPID) - - # variant 2 - pidHash = SHA1(DSN+rec209+token) - bookPID = encodePID(pidHash) - bookPID = checksumPid(bookPID) - pids.append(bookPID) - - return pids - -def getPidList(md1, md2, serials=[], kDatabases=[]): - pidlst = [] - - if kDatabases is None: - kDatabases = [] - if serials is None: - serials = [] - - for kDatabase in kDatabases: - try: - pidlst.extend(getK4Pids(md1, md2, kDatabase)) - except Exception, e: - print u"Error getting PIDs from database {0}: {1}".format(kDatabase[0],e.args[0]) - traceback.print_exc() - - for serialnum in serials: - try: - pidlst.extend(getKindlePids(md1, md2, serialnum)) - except Exception, e: - print u"Error getting PIDs from serial number {0}: {1}".format(serialnum ,e.args[0]) - traceback.print_exc() - - return pidlst diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kindlekey.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kindlekey.py deleted file mode 100644 index e20b7c9..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kindlekey.py +++ /dev/null @@ -1,1727 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# kindlekey.py -# Copyright © 2008-2017 Apprentice Harper et al. - -__license__ = 'GPL v3' -__version__ = '2.5' - -# Revision history: -# 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc. -# 1.1 - Added Tkinter to match adobekey.py -# 1.2 - Fixed testing of successful retrieval on Mac -# 1.3 - Added getkey interface for Windows DeDRM application -# Simplified some of the Kindle for Mac code. -# 1.4 - Remove dependency on alfcrypto -# 1.5 - moved unicode_argv call inside main for Windows DeDRM compatibility -# 1.6 - Fixed a problem getting the disk serial numbers -# 1.7 - Work if TkInter is missing -# 1.8 - Fixes for Kindle for Mac, and non-ascii in Windows user names -# 1.9 - Fixes for Unicode in Windows user names -# 2.0 - Added comments and extra fix for non-ascii Windows user names -# 2.1 - Fixed Kindle for PC encryption changes March 2016 -# 2.2 - Fixes for Macs with bonded ethernet ports -# Also removed old .kinfo file support (pre-2011) -# 2.3 - Added more field names thanks to concavegit's KFX code. -# 2.4 - Fix for complex Mac disk setups, thanks to Tibs -# 2.5 - Final Fix for Windows user names with non-ascii characters, thanks to oneofusoneofus - - -""" -Retrieve Kindle for PC/Mac user key. -""" - -import sys, os, re -from struct import pack, unpack, unpack_from -import json -import getopt - -# Routines common to Mac and PC - -# 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 '?'. So use shell32.GetCommandLineArgvW to get sys.argv - # as a list of Unicode strings and encode them as utf-8 - - from ctypes import POINTER, byref, cdll, c_int, windll - from ctypes.wintypes import LPCWSTR, LPWSTR - - GetCommandLineW = cdll.kernel32.GetCommandLineW - GetCommandLineW.argtypes = [] - GetCommandLineW.restype = LPCWSTR - - CommandLineToArgvW = windll.shell32.CommandLineToArgvW - CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] - CommandLineToArgvW.restype = POINTER(LPWSTR) - - cmd = GetCommandLineW() - argc = c_int(0) - argv = CommandLineToArgvW(cmd, byref(argc)) - if argc.value > 0: - # Remove Python executable and commands if present - start = argc.value - len(sys.argv) - return [argv[i] 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"kindlekey.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 - -# 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 K4M/PC 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 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 aescbc_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' - - 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, noPadding(), len(userkey)) - - def decrypt(self, data): - iv = self._iv - cleartext = self.aes.decrypt(iv + data) - return cleartext - - import hmac - - 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] - - 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 - - # Various character maps used to decrypt kindle info values. - # Probably supposed to act as obfuscation - charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_" - charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE" - # New maps in K4PC 1.9.0 - testMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" - testMap6 = "9YzAb0Cd1Ef2n5Pr6St7Uvh3Jk4M8WxG" - testMap8 = "YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD" - - # interface with Windows OS Routines - class DataBlob(Structure): - _fields_ = [('cbData', c_uint), - ('pbData', c_void_p)] - DataBlob_p = POINTER(DataBlob) - - - 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 = GetSystemDirectory().split('\\')[0] + '\\'): - vsn = c_uint(0) - GetVolumeInformationW(path, None, 0, byref(vsn), None, None, None, 0) - return str(vsn.value) - return GetVolumeSerialNumber - GetVolumeSerialNumber = GetVolumeSerialNumber() - - def GetIDString(): - vsn = GetVolumeSerialNumber() - #print('Using Volume Serial Number for ID: '+vsn) - return vsn - - def getLastError(): - GetLastError = kernel32.GetLastError - GetLastError.argtypes = None - GetLastError.restype = c_uint - def getLastError(): - return GetLastError() - return getLastError - getLastError = getLastError() - - def GetUserName(): - GetUserNameW = advapi32.GetUserNameW - GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)] - GetUserNameW.restype = c_uint - def GetUserName(): - buffer = create_unicode_buffer(2) - size = c_uint(len(buffer)) - while not GetUserNameW(buffer, byref(size)): - errcd = getLastError() - if errcd == 234: - # bad wine implementation up through wine 1.3.21 - return "AlternateUserName" - # double the buffer size - buffer = create_unicode_buffer(len(buffer) * 2) - size.value = len(buffer) - - # replace any non-ASCII values with 0xfffd - for i in xrange(0,len(buffer)): - if buffer[i]>u"\u007f": - #print u"swapping char "+str(i)+" ("+buffer[i]+")" - buffer[i] = u"\ufffd" - # return utf-8 encoding of modified username - #print u"modified username:"+buffer.value - return buffer.value.encode('utf-8') - return GetUserName - GetUserName = GetUserName() - - 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, flags): - 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, flags, byref(outdata)): - # raise DrmException("Failed to Unprotect Data") - return 'failed' - return string_at(outdata.pbData, outdata.cbData) - return CryptUnprotectData - CryptUnprotectData = CryptUnprotectData() - - # Returns Environmental Variables that contain unicode - def getEnvironmentVariable(name): - import ctypes - name = unicode(name) # make sure string argument is unicode - n = ctypes.windll.kernel32.GetEnvironmentVariableW(name, None, 0) - if n == 0: - return None - buf = ctypes.create_unicode_buffer(u'\0'*n) - ctypes.windll.kernel32.GetEnvironmentVariableW(name, buf, n) - return buf.value - - # Locate all of the kindle-info style files and return as list - def getKindleInfoFiles(): - kInfoFiles = [] - # some 64 bit machines do not have the proper registry key for some reason - # or the python interface to the 32 vs 64 bit registry is broken - path = "" - if 'LOCALAPPDATA' in os.environ.keys(): - # Python 2.x does not return unicode env. Use Python 3.x - path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%") - # this is just another alternative. - # path = getEnvironmentVariable('LOCALAPPDATA') - if not os.path.isdir(path): - path = "" - else: - # User Shell Folders show take precedent over Shell Folders if present - try: - # this will still break - regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\") - path = winreg.QueryValueEx(regkey, 'Local AppData')[0] - if not os.path.isdir(path): - path = "" - try: - regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\") - path = winreg.QueryValueEx(regkey, 'Local AppData')[0] - if not os.path.isdir(path): - path = "" - except RegError: - pass - except RegError: - pass - - found = False - if path == "": - print ('Could not find the folder in which to look for kinfoFiles.') - else: - # Probably not the best. To Fix (shouldn't ignore in encoding) or use utf-8 - print(u'searching for kinfoFiles in ' + path.encode('ascii', 'ignore')) - - # look for (K4PC 1.9.0 and later) .kinf2011 file - kinfopath = path +'\\Amazon\\Kindle\\storage\\.kinf2011' - if os.path.isfile(kinfopath): - found = True - print('Found K4PC 1.9+ kinf2011 file: ' + kinfopath.encode('ascii','ignore')) - kInfoFiles.append(kinfopath) - - # look for (K4PC 1.6.0 and later) rainier.2.1.1.kinf file - kinfopath = path +'\\Amazon\\Kindle\\storage\\rainier.2.1.1.kinf' - if os.path.isfile(kinfopath): - found = True - print('Found K4PC 1.6-1.8 kinf file: ' + kinfopath) - kInfoFiles.append(kinfopath) - - # look for (K4PC 1.5.0 and later) rainier.2.1.1.kinf file - kinfopath = path +'\\Amazon\\Kindle For PC\\storage\\rainier.2.1.1.kinf' - if os.path.isfile(kinfopath): - found = True - print('Found K4PC 1.5 kinf file: ' + kinfopath) - kInfoFiles.append(kinfopath) - - # look for original (earlier than K4PC 1.5.0) kindle-info files - kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info' - if os.path.isfile(kinfopath): - found = True - print('Found K4PC kindle.info file: ' + kinfopath) - kInfoFiles.append(kinfopath) - - if not found: - print('No K4PC kindle.info/kinf/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',\ - 'build_version',\ - 'SerialNumber',\ - 'UsernameHash',\ - 'kindle.directedid.info',\ - 'DSN',\ - 'kindle.accounttype.info',\ - 'krx.flashcardsplugin.data.encryption_key',\ - 'krx.notebookexportplugin.data.encryption_key',\ - 'proxy.http.password',\ - 'proxy.http.username' - ] - DB = {} - with open(kInfoFile, 'rb') as infoReader: - data = infoReader.read() - # assume 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 - data = data[:-1] - items = data.split('/') - - # starts with an encoded and encrypted header blob - headerblob = items.pop(0) - encryptedValue = decode(headerblob, testMap1) - cleartext = UnprotectHeaderData(encryptedValue) - #print "header cleartext:",cleartext - # 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 - #print "keyname found from hash:",keyname - break - if keyname == "unknown": - keyname = keyhash - #print "keyname not found, hash is:",keyname - - # 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) - #print "encrypted data:",encdata - contlen = len(encdata) - noffset = contlen - primes(int(contlen/3))[-1] - pfx = encdata[0:noffset] - encdata = encdata[noffset:] - encdata = encdata + pfx - #print "rearranged data:",encdata - - - # decode using new testMap8 to get the original CryptProtect Data - encryptedValue = decode(encdata,testMap8) - #print "decoded data:",encryptedValue.encode('hex') - cleartext = CryptUnprotectData(encryptedValue, entropy, 1) - if len(cleartext)>0: - #print "cleartext data:",cleartext,":end data" - DB[keyname] = cleartext - #print keyname, cleartext - - if len(DB)>6: - # store values used in decryption - DB['IDString'] = GetIDString() - DB['UserName'] = GetUserName() - print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(GetIDString(), GetUserName().encode('hex')) - else: - print u"Couldn't decrypt file." - DB = {} - return DB -elif isosx: - import copy - import subprocess - - # 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(u"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(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(u"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(u"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() - - # 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' - - # uses a sub process to get the Hard Drive Serial Number using ioreg - # returns serial numbers of all internal hard drive drives - def GetVolumesSerialNumbers(): - sernums = [] - sernum = os.getenv('MYSERIALNUMBER') - if sernum != None: - sernums.append(sernum.strip()) - cmdline = '/usr/sbin/ioreg -w 0 -r -c AppleAHCIDiskDriver' - cmdline = cmdline.encode(sys.getfilesystemencoding()) - p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) - out1, out2 = p.communicate() - #print out1 - reslst = out1.split('\n') - cnt = len(reslst) - for j in xrange(cnt): - resline = reslst[j] - pp = resline.find('\"Serial Number\" = \"') - if pp >= 0: - sernum = resline[pp+19:-1] - sernums.append(sernum.strip()) - return sernums - - def GetDiskPartitionNames(): - names = [] - cmdline = '/sbin/mount' - cmdline = cmdline.encode(sys.getfilesystemencoding()) - p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) - out1, out2 = p.communicate() - reslst = out1.split('\n') - cnt = len(reslst) - for j in xrange(cnt): - resline = reslst[j] - if resline.startswith('/dev'): - (devpart, mpath) = resline.split(' on ')[:2] - dpart = devpart[5:] - names.append(dpart) - return names - - # uses a sub process to get the UUID of all disk partitions - def GetDiskPartitionUUIDs(): - uuids = [] - uuidnum = os.getenv('MYUUIDNUMBER') - if uuidnum != None: - uuids.append(strip(uuidnum)) - cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver' - cmdline = cmdline.encode(sys.getfilesystemencoding()) - p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) - out1, out2 = p.communicate() - #print out1 - reslst = out1.split('\n') - cnt = len(reslst) - for j in xrange(cnt): - resline = reslst[j] - pp = resline.find('\"UUID\" = \"') - if pp >= 0: - uuidnum = resline[pp+10:-1] - uuidnum = uuidnum.strip() - uuids.append(uuidnum) - return uuids - - def GetMACAddressesMunged(): - macnums = [] - macnum = os.getenv('MYMACNUM') - if macnum != None: - macnums.append(macnum) - cmdline = 'networksetup -listallhardwareports' # en0' - cmdline = cmdline.encode(sys.getfilesystemencoding()) - p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) - out1, out2 = p.communicate() - reslst = out1.split('\n') - cnt = len(reslst) - for j in xrange(cnt): - resline = reslst[j] - pp = resline.find('Ethernet Address: ') - if pp >= 0: - #print resline - macnum = resline[pp+18:] - macnum = macnum.strip() - maclst = macnum.split(':') - n = len(maclst) - if n != 6: - continue - #print 'original mac', macnum - # now munge it up the way Kindle app does - # by xoring it with 0xa5 and swapping elements 3 and 4 - for i in range(6): - maclst[i] = int('0x' + maclst[i], 0) - mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] - mlst[5] = maclst[5] ^ 0xa5 - mlst[4] = maclst[3] ^ 0xa5 - mlst[3] = maclst[4] ^ 0xa5 - 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]) - #print 'munged mac', macnum - macnums.append(macnum) - return macnums - - - # uses unix env to get username instead of using sysctlbyname - def GetUserName(): - username = os.getenv('USER') - #print "Username:",username - return username - - def GetIDStrings(): - # Return all possible ID Strings - strings = [] - strings.extend(GetMACAddressesMunged()) - strings.extend(GetVolumesSerialNumbers()) - strings.extend(GetDiskPartitionNames()) - strings.extend(GetDiskPartitionUUIDs()) - strings.append('9999999999') - #print "ID Strings:\n",strings - return strings - - - # 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 - class CryptUnprotectData(object): - def __init__(self, entropy, IDString): - sp = GetUserName() + '+@#$%+' + IDString - 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 from 1.10 - 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 from 1.6 - 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 .kindle-info files from 1.4 - 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 - # check for .kindle-info file from 1.2.2 - testpath = home + '/Library/Application Support/Amazon/Kindle/storage/.kindle-info' - if os.path.isfile(testpath): - kInfoFiles.append(testpath) - print('Found k4Mac kindle-info file: ' + testpath) - found = True - # check for .kindle-info file from 1.0 beta 1 (27214) - testpath = home + '/Library/Application Support/Amazon/Kindle for Mac/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',\ - 'build_version',\ - 'SerialNumber',\ - 'UsernameHash',\ - 'kindle.directedid.info',\ - 'DSN' - ] - with open(kInfoFile, 'rb') as infoReader: - filedata = infoReader.read() - - data = filedata[:-1] - items = data.split('/') - IDStrings = GetIDStrings() - for IDString in IDStrings: - #print "trying IDString:",IDString - try: - DB = {} - 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 = CryptUnprotectData(entropy,IDString) - - # 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 - if len(cleartext) > 0: - DB[keyname] = cleartext - - if len(DB)>6: - break - except: - pass - if len(DB)>6: - # store values used in decryption - print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString, GetUserName()) - DB['IDString'] = IDString - DB['UserName'] = GetUserName() - else: - print u"Couldn't decrypt file." - DB = {} - return DB -else: - def getDBfromFile(kInfoFile): - raise DrmException(u"This script only runs under Windows or Mac OS X.") - return {} - -def kindlekeys(files = []): - keys = [] - if files == []: - files = getKindleInfoFiles() - for file in files: - key = getDBfromFile(file) - if key: - # convert all values to hex, just in case. - for keyname in key: - key[keyname]=key[keyname].encode('hex') - keys.append(key) - return keys - -# interface for Python DeDRM -# returns single key or multiple keys, depending on path or file passed in -def getkey(outpath, files=[]): - keys = kindlekeys(files) - if len(keys) > 0: - if not os.path.isdir(outpath): - outfile = outpath - with file(outfile, 'w') as keyfileout: - keyfileout.write(json.dumps(keys[0])) - print u"Saved a key to {0}".format(outfile) - else: - keycount = 0 - for key in keys: - while True: - keycount += 1 - outfile = os.path.join(outpath,u"kindlekey{0:d}.k4i".format(keycount)) - if not os.path.exists(outfile): - break - with file(outfile, 'w') as keyfileout: - keyfileout.write(json.dumps(key)) - print u"Saved a key to {0}".format(outfile) - return True - return False - -def usage(progname): - print u"Finds, decrypts and saves the default Kindle For Mac/PC encryption keys." - print u"Keys are saved to the current directory, or a specified output directory." - print u"If a file name is passed instead of a directory, only the first key is saved, in that file." - print u"Usage:" - print u" {0:s} [-h] [-k ] []".format(progname) - - -def cli_main(): - sys.stdout=SafeUnbuffered(sys.stdout) - sys.stderr=SafeUnbuffered(sys.stderr) - argv=unicode_argv() - progname = os.path.basename(argv[0]) - print u"{0} v{1}\nCopyright © 2010-2016 by some_updates, Apprentice Alf and Apprentice Harper".format(progname,__version__) - - try: - opts, args = getopt.getopt(argv[1:], "hk:") - except getopt.GetoptError, err: - print u"Error in options or arguments: {0}".format(err.args[0]) - usage(progname) - sys.exit(2) - - files = [] - for o, a in opts: - if o == "-h": - usage(progname) - sys.exit(0) - if o == "-k": - files = [a] - - if len(args) > 1: - usage(progname) - sys.exit(2) - - if len(args) == 1: - # save to the specified file or directory - outpath = args[0] - if not os.path.isabs(outpath): - outpath = os.path.abspath(outpath) - else: - # save to the same directory as the script - outpath = os.path.dirname(argv[0]) - - # make sure the outpath is canonical - outpath = os.path.realpath(os.path.normpath(outpath)) - - if not getkey(outpath, files): - print u"Could not retrieve Kindle for Mac/PC key." - return 0 - - -def gui_main(): - try: - import Tkinter - import Tkconstants - import tkMessageBox - import traceback - except: - return cli_main() - - class ExceptionDialog(Tkinter.Frame): - def __init__(self, root, text): - Tkinter.Frame.__init__(self, root, border=5) - 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) - - - argv=unicode_argv() - root = Tkinter.Tk() - root.withdraw() - progpath, progname = os.path.split(argv[0]) - success = False - try: - keys = kindlekeys() - keycount = 0 - for key in keys: - while True: - keycount += 1 - outfile = os.path.join(progpath,u"kindlekey{0:d}.k4i".format(keycount)) - if not os.path.exists(outfile): - break - - with file(outfile, 'w') as keyfileout: - keyfileout.write(json.dumps(key)) - success = True - tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile)) - except DrmException, e: - tkMessageBox.showerror(progname, u"Error: {0}".format(str(e))) - except Exception: - root.wm_state('normal') - root.title(progname) - text = traceback.format_exc() - ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1) - root.mainloop() - if not success: - return 1 - return 0 - -if __name__ == '__main__': - if len(sys.argv) > 1: - sys.exit(cli_main()) - sys.exit(gui_main()) diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kindlepid.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kindlepid.py deleted file mode 100644 index 8bbcf69..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kindlepid.py +++ /dev/null @@ -1,144 +0,0 @@ -#!/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 -# 0.4 Added support for serial numbers starting with '9', fixed unicode bugs. -# 0.5 moved unicode_argv call inside main for Windows DeDRM compatibility - -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"kindlepid.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(): - print u"Mobipocket PID calculator for Amazon Kindle. Copyright © 2007, 2009 Igor Skochinsky" - argv=unicode_argv() - if len(argv)==2: - serial = argv[1] - else: - print u"Usage: kindlepid.py /" - return 1 - if len(serial)==16: - if serial.startswith("B") or serial.startswith("9"): - 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_Macintosh_Application/DeDRM.app/Contents/Resources/libalfcrypto.dylib b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/libalfcrypto.dylib deleted file mode 100644 index 01c348cc8a638e243754aea2cd2681ad30e629a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 87160 zcmeFa2UrwYyKq}TOA={RR8&;VVn7iCC?cSsq8Mo~qhJ6r3)*V8WTkCU%sJjjU@A$&8FlF?mg)U0m&t65QY>Tm{rMe%-x2s7f!`7M9f98w_#J`Y5%?W} z-x2s7f!`7M9f98w`2TPOPJemvBk#svGNhg+3|Dv>5Gz?o-0Mr3{P6ep@#yOFOCwzK z<{69a3o(f8#mC=2w7)VmcRF|euiJN|pie1D5@Xq)#l%cR{;a=$;Hc1%!9xe;E%>z# ztIA2zJl2V^e*Q*`3r@3-VM9m#S&?7&;aXmjYE+aYF&6dzr;oq?sIfx=h7I-~6&e;m zfnWC#)I9egi5QD`<$sI|<+eq{gnh8Fe^CFy!~2ia&HptX5RZNG#`*mr zKbHRfLur3*7>0+A{KxIZ=3@K&ZN#i-yLhl9KK}lL`~KTLEL?LRB#CjseTceWJRor7*x{kW?D~ym zR+7r*m0RTAEcQv;Gl6aPOdZ=$9`d-7Bt_I?oj8UiBTL54Vm|NFk@=QP&tklcv1o%h zUV~1OL>HCzbe5!kRH`u36w!U;{1k&R133KThsnQv64;&xaew=Whi?rS#IeLN3+t=- zsVCJ98#S^nEfPbQv7)@>uM?ht}IztG0+e zE{|DB=a0>i$1G$VsWyvXZD=JXlEp;nJwI}Ca+nj`%4381srm7&g1iEwyu$LBp}AX% z4dgNX;QZL6Y^Ke{{H{(0@?Clid*CsFPdMxxUP7y zyy6y=u*@w{BELi_U5S!;B`ov8L(p<}Xq8*Mbbj%&y5eQ>iqpJK*109h<(H_SD^WhL zgmqqoLmLi|SBN2Ql4PG8omQnsZ~x#(s*P&8zZj}A-2jJX);qDTWJ;dwX&$H zEX|Sc^3K$F-_WWb%Xc|Xv;XAanXOf1raZNXyqm*%jpByZJ4@?4J@OHSCHqfW?@alw zi(2(_t>xUvyDXCIGc}$T4xaBco|X=tpERCU4xZmMp4M7lYmFi^`lyZ8H%se%L95Po zu$(5}<&mlJxT5ubu62%NmtylTS`Set-C`Y0ajIWmGCdQ_{irzQ$MP;QLtPYwY0roFi*|&2l_0M5`|t$zzR(>601%ixK>5 z{ItGix@K8u2~=*gWYY@7iiU|;iH3dB1=30vLea44_UXTdRrPoV zr%aQnYi3^4jODTYMbomr(KMZ7#8vjm+^;z~3Hg5OnddhT3r))^###?EMl!93u|`(q z@ux32IkCy|xYkgsRaj`Fgr?%nV)+Rq7qH>fO-!a_$@Ee=J5X}WTA94Q$%YyO$$1D%xPrPwCWq0Qd;kmdat!k zTE$5NMV3}yqo--2^-j-Sl>T#(LD){MxmK|=cY&eCG}`07EPBWVh8q^qL$WNi9v7TE z?yF|}tbJ?wR3-Y=dRu6`uN?CbcLh@X4*d!=C~QysK7Bk8AFW?pAfFaLQ=e=`qeza| z>XS}sy!Y$Qbgo~tVOO;6QD2IDSA$8KbikAEvYe##NYh#-Xg&5gSZ);ybJ>&cl1V6SG`<$v_Ll$iy;y47bNv5lU97b2t^enHvCi>8R~8xqq{!sF zJ0*y2BcGOmH-x2o>MWh#|JvpCKZSAZIeE-^u7+A~GjW|w7n2?q^uL$`xsw`?bonmR zlsi@$@00Rf?gsWLOwiS|jqG3NF4A2u#Zp%5E}41LMjDSyPPw*?%s!n-*2=^sRyU=) zkm{y%*HAGvvM`;}VT>{5j77PlOz5trVnTNvWx`AD(7}v(Gf{%byX&r|VnTNvP0{Lr zJNrrFaUU?^XGU3wzLXZfFN%MY)~NTyM@Nx=Ydn%!MbfKL?0+JoH~!>9aUta?=M-&+ z?a|66YQ596!h9MJnw(CPvzgL-%I{)YMY7g1%3g62kmS3LXv6kv8%(qZK*bf^Zc?<~ z?1e6urfo1sY@4Z7q-#6NxactHapC8Fypy$DIk=X6k?%T6Kh2%fDsHeF%_rTZMC__T zBwgL1K>~Ym7>PEn6xe|SD-{=VH)lHK70pQ1D)wm2qQpjKd&EXwW+^aA`?zE#JD5$P zqGAj6e%c$PrlfL)_Ib>`hw<7?mlwo@-7?M zxt0E9PjpV&Zz=Y4MeA|$XZ7=TMP-g?75jBDy`p)O7sFrnaYMHcE3uCo1unJu)nPRq zQM5@{pBpJp@&nXV1uArpu(X9`ufPEOO9oC6Y>qqPeL| zm|2{mn6ilT)DvUlI8XgJUjyB#{pC39BmX!Bx=#K=-GMZp{&t*BvY!X?)J<}vLU~6r zjPo_3ZaRzpY*Ai!%iphkYJqN+zc8;=^tNAG)ro7N$k1q(r*4v?ndBYKG|soMu2;@# z`e*RiMSi35duINUyn_(lUk_rIdk}B4I5*v-MVwoa+=IB8$90lNr?u0`ENS*3d{W$n9eta%Ws5TS#A9uuLm__UddCaB% z6Z{w;v?%--z%^Ls#{fFE&W{0A#H#&I4FBfG0!At*H?)_oEhe};S)k8o^W4T-dhk!V z{d6RD`9-yvLbao1xxN(LGxvI)FWXGb=K`+9s6vuDW&h2PDX%Pk);0NWg5*krLH@yz z7la|n`Tr|Jru>^A|3OLrXh;r0qV`wO7Sy9A`)9-d#g1G?e%VJsJ*GUNCDb-o)PFal zalQcw0(J~bb8w!j^+SCW4@)KknI_t!FId=)-wC5qD z5NA+zN&3~7+`(}6!puk^wF+*>XeW8&Chg2G6xNz*yzko|*Q$34lRoD5Mrbrk+~#<1 z70KsCF{#)jQql}2qt(`W@=346LiHwj%x3zNQ`k;<%wpZG#7>Pjr*41rC;8}ZTE$k4 zPZeWLC^u2wJ2hri9v6R~!@~iidWK1C{-@vNyIMBMsn@c8j(hZ%qG7M2hwP^_?u<^h z;KqaOnW{yr_sd6`bKd-E7MX}C4N#{s$`l|pK8VBSL>bo~p z>ASaWjh^lKf|MlI3Hx zT90HA9e=75&n6!0I1Aj-QVVxoE!=graM#tsdn>KW?L8jrL<=vz$!#GAmzmbn1vEJ< zTdQ7YfJ(V{Ff-KfpwK8z&%Kpf^cVT)x6~@PcTh2U2Nk1tP%(G2CpXb#eDt=a35RoBR)E#KymSm|Jj)Y-6x;7{QI{sL{uswL4%^KYg)|C@*=s$lW0S-DkbIW0UZ(vQY-i71 zP{sgzk1P4`{>e*N`T+ZuzCXa%BW8Z6J}~48Y=K%&8>8!M!3y|J+?}{Iy}?vGm?0cXBe08H%5= zITP&cuZ!o+?PXep8NH^sd`oh0vXM=KT$7RV)F|bNzI@ABQlk`&VGF0PVV85CndUtY z;=zx$p0*?X#xgc}f|q^HGn4%1owr09U=VM1QB=e}g(pSz&5Xp;0(O5zw|mwbiCZNO zkZOOJ+mtx7Qmr&^nyM|sQglZN9%@sqn$57VP%#SWKUV77f0Tb*!eo2IDvCo$bLNAb^} zckkOz9NlJc|DgjzgDQ&0!u(s-+IOA0^mm(ZcvYz~b9z^Aaw247)7Ku0D{SogWz$T9 zdue+l4GJ5m$(I?}ow$p$A?ec>Tmrv9@64A0@wP`B@{%F&E+pU3Rld8@$f9i23 z`A0APu)2qfWw(o5IoC7(=;!kxkNl?@%w9C3#Itcj4qfZkd3K%XgBQ+M4DD(goYkgf z`thlQG^t_Au~XlLxOAO=bHv{2yE@!CSZZ@z_JB<`$9Fb4t+w*I9T;U_=0P8aPIEoN zy98v7>9;K=F=U_hvdAB^Y8ij-8q|7t>F*xXCJnwdYxk$|ohzRowehZg+mU|LD%`8N zQ8B=%#=gAdani<}llUT^*rssi&-hagVOfS?cZN#t(hO(Rz9s5-II*1yGB*E4Ky{AKk}P-ZqCb{OQtU@ z+u=xZ#0r}meS_PM>0Yu-F>BMLnRg%5T$bD-tCDlMQe}%3E@EQ7`Iz?2-gUd`q^~b; z8C&|bUpu3)E&FcHfBC&n_{yc#^gix?KeFeMrUP0wnDDj7XxkRr8%Haj2>SM7SK#aW zdopg=U$%%1ba$;Zx42EwqcQ3#Ua_B=?K@Yr{^!IY%WaRft-GY$+j`q?JnuZQNanqi zF@sC)s5O7@@bzC>&njA}-;-8pQ@*V$(>#0GmD+0GR|yW4jN8*F4$lkJp~NnRh%DUiZWFQt_9(M|*F5)xyVn!^zvS zIrs1Vadt`BxIyjqRH&U2-l@N*>}l`Cy&6u@8x=a~i=A8HuieWH+|l{m^%)Mn7Eg+| zx-?`;-^&ZenJsTT)$&s}jdSs5FRQOV-SBwd-7g+k-*Mj7YFxM5=N2^|^+qxFw58Rd z#>YlIJ@v6mj}71bEr&I`J@I-=W#ro}87^r-duunS;^^bfd5VK-v$1qz<&Vv?*xA<@Q(ujKH%>G{#oE3 z1OC?FKMVZ3f`4i7p9KE9!M`&2-vxg^@UIE}M&NG%{tLi=HTb^)|Lx$P1pYn2zZUq< z0RL?8Uk?7K!2crnR{;OX;J+CBmw|tM@DBrjC-C0@{)XV+3H(2Ue>3n;1b7SMc`* z|MK8p9sK)&e?{;=3;s>Oe;fGA!T%iiF9H7!;J*U=gTcQf_?v?N1MqJF{^h{GF!*l< z|GnU!4*s#=-wyouf&X{#Ukd*F!T$*OHvs?9;C}=BgTOx!{4>Dc0{mUU-v<2E;QtBy zi-P|U@NWzLZ^8dL_-BIuVDO(0{$Ieq68NWqe;M$<0{*YS{}}kEfd3EhZv_5lz`q9g zR|Ef5;C~z%|2XiU z3jP}Ke+m8#!T$yLJA;2W@NW+OW5NFr_&){z9^h{Y{u9AJ68v4jzc%>$ga1hIHv@ky z_`8As2=KoJ{)@mr0sQ-d|8VdR1%G$&Ukm>I!G9I__XhtE@b>`!uHbI~{$Id<2>6c% z|90Si8T?Oxe@pOR3I5%|zbyEhgMTvkhk<`K_|FCZ&*1M5{)@nW9Qbzw|7h^92>!v~ zpAP;S@E;5QF5o``{5ydEX7Jwx{!PH&3;gZD-vRu?!G8?+CxZVn@UI2_LE!%#{0D>o zC-6TH{`%lQ4g5EPe+}?Y1^@BjZw&sEz<)LP4+8%v@V5s4o#6il{O^MQJMcdU{sG|s z1N^&yzXJU4fd4b_e-8eyz<(+DTY&#$@HYhiLg0S@{NusD7x-TQ{}SN;75txoe+%$m z0{-scuLb`e;Qtc*O~GFd{(Os#FDaQsPXzxL;6EJvM}mJ2_&b9CMeuh5|Eu7?2K;@% z-wyl-g8w7%p9B8W!T$*O+kk&t@Gk@YN#I`-{IkHn6!;eb|6}044*b`He`)YH0{@%f z-v|7wf&Y8(Zwmesz~2`9kAnX<@P7^dH^4s@{40TfQSh$<{>{L@KKL&O|GMB`5BxiW z|2^<83I2P*zcu*x1OF-D-yHmFgMR|}w+H{T;J*U=3xj_o_}>Emso;MI{1=1&4Dde* z{+{4p9Q^ly|3~oO0{#);KNI}xfPXRYpAY_O@DBw4ec-PIe=G3c4gL$je<=7r1phPO z{}}uif`23MHvxYY_-_aQ%HTf>{0D&le(=u#|4i_|0{-*B{~Gvr1pg1dfnDI{2ze- zHt@d<{-ePEH260L|5M<<0sM!7|8?+x3;t=~-vInw!T&J$R|Wqz;6EGu^}v4z_}>Tr z6!4D$|6SnU6a4+a{~h?dfqyCRuMYkj!GAIMe*yn{;9n2?M}z-K@Ye_b2jE{5{2PLQ z0QheM|0M8#3jRO9zb^Q<1AkBOKM(%Xz<&n#4*~zq;C~SOL%}}_{EvfwD)>(Y|E}P_ z7yR#le;oMRfd6UmzYYFnz`qmtcLD!?;2#41k>GC({;k2^1N?7+|9J2p1^#WpzXJFx z!2cQe{{j9-z`rs0*8u-R;BNr_CBgp;_>TpDBk&If|7+l12>cs>{~GYO1OK<+KNu?b31pgH9-vR!^!G9L`KLP)5 z;GYftYVdae|2E+72>u!1e*yg8ga0`2-wpno!M_RkE5Y9%{3n2aJosM$e<$!i4F0{q zKNI{ff`3);-vs_v;J*m`n}UBe@b3ox3&6hv_|FFa9Pr-{{w2Ww5%>=S|E1u+75rC$ ze{=9(4gM>@zdiUzfqw${uLXY@_&*2#Q{X=x{Jp{d75Hxe|2g1)7W@Z+e{JyZ5B|Nu ze+u|d0{_C`KM?${gTDp%UjqNj;J+OFKY@R7@Lv!9eZk)v{9A$lIq-i2{+8f>4E#TW z|99|j2L3Ix1OBv89%H3n$*h8UbdTE*!|1l_QrP0 z2h_M)W&7wJE7}avo~=6a(}#W&gXWdIu&U?DUj28?7(Bdn$L}xpUh7@L({FA3;+TXh z8>K1x&zpX{-?h-59Ug|6waZV-XfD@J*>b=@ulVGUCew?$br^W#>D)JGPG|3YIBL$O z&`OUsH;ldex$$G`u-ijhy)HANob3_Ku-yUX8>|kN{pRb^_UO5>HVdvN|4^Rl{;tB! zbq;Yx$LzNiv*=bm;<tmp&r@wySz|-nlwR}te`0*&<^5s7sZrtd-VD#wc9vK( zSI@e2+kU)NtEq*vv%fyyyxDEwg$p~ruUO&#bjucxLyHzw{HA3F5L%kJHGeqFY#$A`|HS9v#WI=b(rOS@0+*r6V8Yunl7-o2Y6>elT&Hzua| z`z~G91f4y*SGcS8ix(+}r;)e3 zyk=c!*KXe9u3fi!ojDU5l9{=$=Brmn9&Fgqq2$-EFOIZmG4Zmu_X353!+=?_u|>~# zcucnV{P~HYzP>CnJp4y?$BxTdIy)yW07q3?L`SZhf{QVEEm^-)O$(AkSoJy6l^Gi#c`DX1}Mbv}|-&!?mHf~eD ze(8^T^-8K$x^(^7qeeYk)~8SMyvWGXXFGIQ5`N>x$^n%swVY71rq9NNgr?DPafLHH zJ*U{de0l8ro;@MXmX9s8#K zqemB8q^E1UHEuj~_mCmiro_kV^}Bd++qw4b7iFzq-)86F!B@Wc`ff6M_wH@gr%%sp z>EAzN>bP<5o+Ku=vNATdxR{hQEKaG+9#*xg*GN0No>i=^9bd0m<9BT5&QYC?9-UkH z@#7P!n>Uv)3JrZ+u|frxn{C_9xpCk?--%jnk=V(TKKRFje-ZFM0RG#+ z-v|6Vg1-UyuLplM_?HI%0PueZ{tLk01N;|*zbp7p0{;@={|x+(gMVT0e-8cw!T&q> zKL!6o;QtZ)=YW3?@J|JQYw%A5|3Tp29sJ$EUmyHCfPYW$e+T{zz~31BXM+DI@b3ct z&B5Oc{5ykxfAH@O{x!gVIrz)L{}1pt0sltezYzSJfqymd4+j5J;J+07%Yy$B@DBxl zJMgaq{@cO-CHQ{@{}15r4gP(>|1|iI2Y(mv9|8Vz!T&w@2MPYczc%>y0sji%pAP;n z!2clldxHN}@NWzLx4{1b_&)}JFYpfm|C-?c0Q^gW{}J%N4E_r6p9TJBz~2J=4Z%MW z{IkKoCHN{e=YEz4gSl(e;)Xs1^;mH z9{~Onz<(q7M}vO`_}ha2dGL1z|FPhI7yQeC|7h@U3jV&}-yZzSga38#HwFK};C~}B8vKuee<$#-4E`$cUj+UY!T%=s-vIxK;2#VA z3E=Mz{%PR90sOCke3 z5B!INzXtr*g8we?uLS-xz~2V^+kk%#_$$GG9QaQI|4-l_0sf`H{~Gw)ga1+R-vj=K z!GA0Gj{*Op;C}-Azk&ZH@NWYCf#9zK|1sb{9{k^ce+c*w1OFr7Ul08Cz<(h4*8=|^ z;C~tXH-i6Y@XrAMDDXcH{%Y|50{-2=zZLjrga2mmzX1L#z<&$)F9QEn;6D=llfi#C z_?bxfxij(M}vQD@Gk@YWx;9{0spf){!?nd0Dl|s{|5fsz`qCh4+8%a;C~SOoxtA< z{M&(lSMWar{+Zza3j8;K|5xyD0sh|L?*RU>;O_zcpTS=r{KLV&BltUm|5ET@2mYPF ze)@XS{`JAX3HY0We;e>O1pg}F-xK^(!T&t?Zvy`S@ShI; zv%&v9_>TquN8q0h{*A$Z2>8c?|3&a`5B}@He=zv_g8w`4e+vHn!G9e1CxX8*_$PtC z68x)zza98ngZ~=v-wFOl!T&M%-vs|q@UH;=ZNdKl_-ny`GWZV%e;4ro1N;Yoe{1kJ z0RIo*Zw~%_!M`E+`+@&n@Sh0&Dd1lY{Fi|L9q|7I{`0`UDEJ41|5fmx3jVXeec4@Sg(y%fVj*{zbt55cod?e|PW?0)IE~pAY_Zz+VpjTfyH4 z{P%%>N$?K@e--$T0sryf{|5X+z<(I{9|8Y*;I9Y%1Hr!*`2PU^%izBe{6~X-2KYyT z|8ekFgZ~%s?*{&@z&{)OH-rBL@LvJ`Tfl!2_^$&0k>H;U{=32dJNVB8|7zgh2>hFa z|3~mY3H}Yhe>M0&0RPwEKLY%(fWJNXmjHhm_#XrR%HV$({H?%$4)_-W{{`T01pdXq zKLY&kg8wt{uMYm(!9NWAO~5}I{A+`M8SpO){zJjPJNPdH|IXmw6#OrN{|@lC1^;{C zUl;siz`qOl>-Y!%7vOIL{@=iV8~FDC|3Tn?0{jnxzZ3X-fqy&j?+X5Bz&{iGUxEJy z@c#<_Ex_L!{2jnQ7W_TH|1qcLaZD@Lvl4>%hMg_^$+iSMZ+({xiUTG59-z z|9kM?5B|Hr{~GvL1b++g&jJ6p;C~MMPl3M~_!kHN=iu)T{&T^V8v2mi6){|Nlk!M`#1 z4*~yp@V^ND?ZJON_zwnuU+{ki{!hWbKlqOW|3vUN2LB}RSAu_4@V5hhYw%wK{yV|{ zDEL1H|C`_+3jP(qzb*J50Dmp`PX_wPTz`qFi9|He};O`FpLE!HO{`0}V4*1K#e=GR=fd4-5FUfUFeCn6IaMunKdtx~B-2fbVhKTfg;{#?lyANSi7%`?$r4jc zNKY;?B_{Ge;$VE^SNAD678MYm26@Bh=DJzkJYWCgC4G8SOlgz~?+E;k!0!nBj==8-{Eooy2>gz~?+E;k!2b>rC|c2?OEH_L6Nc;a zpWbkNURNd+b@dmotm_*X;vX0k;y*B~@5p{qrxC^pxy$?YA31Wkl;Fx6)B5GU*)D(= z)A44z5mJIQ!a}!o-%+Fbj|}x689G!fv;1X`1NsIJ7R#+zK48qq;L!e66z$t|tzWO6 z1B-d0;?Mi|48-rHNH!f?*h{L=VZ-~5YoU8ln-r-h$wu`L^_N=Z|I*5k{NGR+S@73Y z#z^{k%l*ag%mn6@*3ASNNSS85n{I1K@uMt1GOO}~H@q20*^PLd(`IOJs5DEjW!|sF z%-3VN6DyOOP(AUirF`C_6!EymoCOAa`?7N0w4CYsdDCT?b`#S)gc0u<^vs{GBuQ=a zr>o_@6HZsACcl%JzuZoey5~>V<-K?L(+<2#E`PeQBn9VBH7E<)wDn zdDGcEh|*&_J>AdTedJG9)Gg0_@YJBdbn^n`I~G_j-h)RKbq~!Lxr(7QKguu^TxY{u z0EYzm>)zVuAJo6E=zsLu0bxS}{r&A4*w<@NVAjFWzJ9@32M1<-`qdLZ?8Oh=tb>?h zGoSuqMSroPzgW?q6(f0*q+}yr<7kun8plzRzv!O@JDhGoexKCMPEVxY9IcaPG$o>MX4zIr~$wgSVD3{*9VfQzfiTCf%?+E;k!0!nB zj==8-{Eooy2>gz~?+E;k!2ekh`0?V)X`N%L1`Xi#xN{zrORH z<$PRqKl8@omFwJT>b?WPoz}m<|DU4ExXa@^4`Q6V4P(A6A-+P9`|BHOA#aKUgE8RZCcQC}q=0E)n0q%~)_b!&_jeqWgWk09?)87za+e}HS zSR;4*=XRn31wJB)&Gd&V}Uox4$8vdn>#uQNDiu=4>90{48>~@B1S*?N;T8H*nsilb4A!X34D#ny?IUD1YHgshZBEX~pW6lH zZr3b?Z9PuN;wlSf6Val8ONR)RM$H?D5;BDL7f2_a zdC1y~=cUcU8N_r2-QOHBZMwoK%`wB~C%FD(s z(lh6sB1C4VFgA1%ZC2Py=8*$(KKZ!omsd&dx)?-O%lW2@iD_qL0(Mrn?XH~eDaq8q zUdrhMB;)DTBr|tc{cO&^iZ)vir=sGV(e~oBz1Y9RN|d!^n-Y{QMcI=75+_U7CDM)m zB2LEtDNe-4lK5B^h|lByW_+IiLwvG-iO;+L93Q>R2*aFD5uQ2U)Hd`-{bC}P775gg zxb-6jR>Y#T=;OqqEU_r_FL4uPt=Xm=<;qjO!oS4L+I5MZ&wmj&qyH2)(YM4v{H%$e zO@a7j{Ws%x{~zM__?P%S|Ht?Z%IjbBZFg5GLG)!s+Y(8UhB?_P6Z&sk`gEJwNxJ?V zY-6XcX{GDWKJJDtKK#>%*ry^yzdSMjq_CC2wdl{?mGp1=Gd{@e&lYO;Vxm7=s_568 z4=eh#HRr@av^#&mJ2|cS(>s68>;CRhZ5(8t@1V9hUsXTXla~}&-ftg{Yab!m=y8!L~VH9@I9#ff4#0BP^tBADKl{*GMVBN3v<;$PS!b zaYLpgQx!H)sGV={kB!zF1?0oyoPVyafF)2JsoNIj6R*^Je z2h)a37g8ko+%J*jbC0sYeeA;N58>4q3gx;^VS?|{`q-&f_ObJMMn5sWn53E_*{K3p zj{|)kT}+zrq>tT%6MgLb4wOjpTT~)xLJ;$lnIF%5_Yz5J6a6(4TCx36jaIa{zN47$%=W8> z+l8+jZl{{dFnYM1I%Kj^ZQDetI;XT#y=9O(b3JiasufB*wIxGHP1Zl9|5jpo8OD@# zudh^xbY{7`a{2_mKi!)?M_i}7NU{XtDb7yj*Dt45R;o7FQo2P+mUx|Ovt%Z&(XI!j+RouXv6SI9`(I|T)iKcGlVglU zg5yz(MXe?Ill(pzXT|Ffm6CyaQ;0I+0q47rWUNt2MsZ!G!f|_;c9i6i-dr=UNM^!i z+C7qqcAiv3+m(JUU89|%z3xi(VIKEMGF97lR{E??HjkSp>BT9fLOw^6&7WN;GKFrgVe+wu`Y}nj*=UeiB_bYbUbyBkSoy`0?`o>yC zk{^97JSxIYJxsC-U&pwn(#|(%c9LHP{drA*C0Rh)McIa)A0u-+_!X66{p#66jmRz&4fmCeg39 zE7h%7CKJoclHs>A9l)ebe4Dj)i0d3U9|t8_)zu9dXoB>&d?T$1@Nqa6XX z(+r*mF&#&nCjA#}T1Xq#6=>5S+IXx$oBCZ$iWn@Jg?q(Om%B>e$#@`+Y1$;(5xzFJ zO}=Y$+Z5iE`JZi~9k#T`opwpI<7b??giv-__(vCLI&iZ7dQ6Pl7kKN!LVQ?NJw>Ry7)kzN>1} zHBsReQQq-UPL{al__)*GWR5zfeFu5EsA8H}iffZPxMQMjy}qZ5k7b=;C+O(?uQPu2engnCQEK z^o?MhiIk`gDW>!dm`I&162*4vHuVw}#>wVt<0i`d@(M38udh@a7gI*u zC$EDwMITQWML>+p{GpPOVqmI^I=Dt6^^;MzLcS5iCE%#byqA)p>bR7sdSa2N_Uf+m zISNC9EiYo~)kGN~lZ?fF6|_$wu`hF}u);*wK4bP}GT%-zrHq-{i!!b;E(&+bwb89N zV|@`~Ag7GE@RjgZL1z`be*WUCKB{T>D)GPND`&?*Gatv8!dZ?-3qNly6?V#J_NT8- zj#BE})IQfu|Ku*6uku_ayd>NuoF$y3^A(N~F%_N??h+mo?uxb}mlj?Up2AteRk@C; zsf;!k&cadnOn6E-O1Mk(*?d>&VvVare=X=K;U(cJ;i;K=Qaj-^C)=tq!c+NRb>W^Ghv}>z~RJ+&1{GnVsbOnD*Ugz7TE7VE)ML zq=+f>dFma`^*(%Zkj_=UM{w0T+Q4<&cO7jQAllPJ>38&ElJ6tYX4<3kQ6;4+jy4!F zi1vhBQ>q>?wk?x)?(fp}RQ&6XhiOlVCQ9{$O5AtwYq%j_cD zq;|eHXD8vQW$HHMDy?fY@LeJD<=p(($v7uVU5Wa)rCsD3z8e)us&8E57P(vcr7MyY zw)))TioCgr()U;it{YxTbuD6!yL^PR3gav}&N7AB4mfL=r017Ro5;21ciOWf|mN?EHUjvYY# zS}(Ixgh=K(uVsmROVo>fy^!pD9I_g$+KbiFfMauo~1+ zF3G4@A>U;j|EPZX`EtonZ6U#l%r3%OoHraOu6ZGqa{U*~w!?7Npj5FB6Sg;IdsE7q zP$l()?rqP zc6gz~%J7>^FOn>{2dY?Wp(Jm5OR{iVoU^FzLeWmWcH!)ws>*UbH0hRKkKAMD)kV~yZO-TA-0$58Ut}ZVY?a?; zx5ZNJ@CEmjtc%bsJHT~jv2>+1yB6)tAHsW{SB6L5)3r^st8LD^<#EI$uGc@uiu+mO zLyUfjkBEziLDQa+Mfl=-N)Z#{u_`wnl5^8YshuuvEjV7xJ#ieT{}iL}Ugwn&Jr(ETwE;~CzO`V61qv!bSFtReXL|md-|wo8+~1hfUW$tL^?~xkt%XB+C+bBm*6IK zoL*7#nci3G+BWCy0bLu}&wuU@qP^*VZ|{MeY`oPml6^+9Ppa74kpCx5;02M(|(`23W1iRTE*#CcQwXsT2laZu8CYm4i)xKDFjCRsQ} zNO}?ORgxV84TeXgif0)5u`Fk-*D;Rz$F?w9q&l4YFmRfK9VcWFRt9GC%LK$ zTP3Pmbhp%0lJs(yw^gbNH??yLk@RWz{kltZoXYC*uLFWZrwERl%)q}FsB5%U>YiP& zOvFvJoBwr#BG}kjRXD(s{lI7wJ8P+ZL-AaO{MfQwCB5#}?(G}$497)0FW@?4>Ek7H z@iCLRsNDmUs+eL*cUQ?m?4Rd0d&EA~10E;xT(zGng1j^;)umjB9@~lQmZ1xEF;WbY zxrjE3dMu+|YFB+-`^2_lP!(=sCzcgx8_!H^6L|)v7d|mta$S;B4+mK1?>~Rv8f6cX6<;wnzZ4;f0iMzYg zC6%j}h_#u@z(B-$zwTJ{MbQt{LHcXN^M29RSZyU|@$UrUnZM|(|FGWXKdhJKumAJU z4)C4hU;icn?fG@t&p$@My8j!0kKljw2MId8TibDhTmLR)O07EFq^q7sck$Lq5cv96kNLe!Hq29$fx*q~?js)f%>p_#o^Lf2~B;Pq)H%vdWU3Rf}&$%l@&VN2?FwOr_i5ZJ#9~v_5+3e2Uu3b17U8iek z#q({lf^DZBPj4BPsu}cd>R9Fct}Y?f_l~%E@J@$a*>Rgo9kOYwu1O7CLzGY=eLbX={E=ZeZ7C@rt*KWrZf+slV#Ov#`R^&z)TR zyxP!v#8SfpLoGVFRGj>I&P~H+H(d%PdOtkiIo&6oSF`jgBs07)tWU=hIi=@)t#awW zlerq#7AFT@T{33tWA_6_8g1O8(mfom(w-yV?{7Jv>5(2^Cp6HuupND(^3fZ+ zUVIDMbN_YVW&0Z$?t!ru#phOXjX7G>Cf2Kp`rN)|pAtXUFM7;&`H*r;>bAYHz24g* z6FWa2lX5S!){c^c*AL%2zv!&iUs^rsS83h1DQU~Ho0sub*S=E8A>q}eHtmmHbv&Cg zC1b^pc^3*dTJt{g%<6HsYFyhrwOaGdhgQ{YviN>S<%}YO{7)|Qncx}lA-;HO!j(Ob z?VLXLjyk;M{nB0$2ZJ+bs)k*xv;IugVms0|&2MU9rC$4ZQQ*+)P4|sIRZVF!w42qt z=L>f0MR!=R?&0jA{Ws)1oKSK98RJ(a9zRTawD79eutukOGus8*t*T4*tF~{c*}Ss- z(bco+HC!S$hS`iHu_sV%<6tF7KA zH+cKppCh}yN9?$-r!WZ{`D$|?Nu-BJKp`*SQcC*_tuw3^U*Uxo+ zV&UsBt0tU0~D`peyYk2kzy{ouv8R@k~7!yp7x&l;+}Cr9th~m9G9J%N-uM|87;Y z8qRIBIYnl>Ijz$hVQjYJ)|H+27xnW^Nf>`3rtii2yN2_&v(U^ve(oI+KoEGoA%HswKpz1VLjWrg zz)=M72m#DS0M8J>H3U!x0h~twwg{jl0vLn_h-o2p|dp z^g#d~2p|gq#2^4`1TYH$bVUHA5x^t_up0qXMgVsafFA;=i2#fcfB^znfB;q_fHw$W zI|4{T06h^vEd($F0c0b9j_97h0X zUy4Qmtr5T`1kfGoA%Kbq;4A`Yf&jK50679UhX9r!fDQ;?1p)|0 z03{KCDFS$a09qh`atNR>0@#cI_9B3E1Q3e=+97~_2;e&cSc(AlBY-0apaB9HjR0;S zfFJ}AhyXGWfCU0@MF2JkK#c%CA%LO?U- zG6>)b0(gZ0jv;^)1n>g^G(rGp5CAWlxm67TtU>_y5kL_Hun++RAb?Z^@E8H~MgZ>- zz(E9{LICR#zzzh^6alP707DVLcm!aA0Nx=0Jp`~00rW=z6A*wg0(gi3t|EZb2;c$& zxP$<KraL^3IW(5fbIyOGXiix0L2l&6a+910Zc^z8U*kX0W?GaFA#t;0_cVS znj?U*2;dL`c!~geAOK4QFcASnA^;ZzP#XdGBY=?zzzhLs5r7*47=Zw8A%H~)AOQjN zMF7JQKqvy>g*|?25kP+gunGb6MgSoQzykqvMF0i};0pp6f&fM%fOZJrG6Fb(09qn| zl?b3a0w{|B%n?8`0tiC@*$7}R0{Dyo{1Lz+1TYQ(bVC5q2%sVY2u1+u2tb1X#v%Y0 z1TX>tbU*-`5x^z{&;$W^Apm;>;D7+a5x^J(kca@5A%I#4AP50`M*xEnz$XN79s%eh zfN2O|BLb*_08$aacm!aK045=T)d*k^0*FEY)(BuH0(gS}?jnG92;d+B2tWWo5I`3M zpg;h35Wq78@EifWLI6t6z(oY$gaEE0fHerf2Lae2 zfPo0$5dxTl0Hz~=BM86-0klN`We`9T0;q`qvJgNi1W*J4976!>5Wso_P#OUkA%L3* zpbrA5h5+6pfTjpw0s^o_07ntPHw5q+0o*_Uu?V0N0w{_Asvv-72%tU!SdIYdB7k}b zpfduvhX6_-<_MrR0!Tmr?GeCP1h4`D6h;7%2;de1n2G=nA%MjQ zUi2ytiKyd`H2LXIU09z111Ok|e0O}xsVhCV90#GA>Km@Q40VolG6$03e02UyC zp$On10yu*J9wUH-2%r%HFhKw+1h5?eR7L=^5WoNgupa?rAb?B+a0LO(LjczhKt}}d z0Rdb>09z4&Hv*6$fIkpG90I6-06HOnrwE`S0tiI_ZU~?p0yu{Nd=Wq^1ke`&m>~d5 z1mKJSsw05o2;cz%*oFXZBY;r|;4}hgi~vp{fDH&>7y`JC0Nx^iGz8E90k|T7!w8@% z0%(H(W+MPS1h4}E+(!T@2p|Rl>_PxN5r7{8ApP$|`agm6zYOVr6Vm^tr2iF2|2L8T zw`X5L7|DN=}1L^+}(*J6t|9_DF-y;35O8W0X`rnK6|1jx)B3<06 zzX9og3DW;Vr2n%?|1XgKcP0IAL;63J^goRB{~hW7eA55wr2hv=|FcQ|kCXnZN&f>$ z{~wV4&n5j2ApPG)`oE9#{|D*+XVU-Sr2o@M|7Vf@cP9PcNcumL^#2~|{{Yhe?WF&8 zN&hF3{+}oPuSfb{ne<Hi4Qe;Mh2AJYHQr2m&l|23rl z14;k4lKvZ!{y!r9A4~ebob+Es`u|DrPx@~{`d^>)zdY&xVAB8Zr2lf#|7N8BjY$7Z zN&kaL{}+<}S0Vj3BmJLA`oEL(zbxs01nGZY(*N$H|JJ1ccS-+~N&lTm|BI3So0I;( zA^opI`fo}4???K-h4lX=>HkX7|Bs~qJxTvtlK%G~{nwKIpCJ9;Mf$&o^#3yHzdPxF zank=7(*Ib}|8u1OiKPF>NdL=`{@)<|FGBi1hV;J{>Hm7t|DvS-tw{gZk^V0u{r4sP zuSEJkiS+*}>Hiec|9PbUYe@fBlm1^L{cleCUz_y5BkBJj(tjV){|}`938epar2kQ* z|4T{#gGv8~k^Y|{{ZA+Tw;=t0O!|MF^#2s;|4`EZ=cND9r2h{||2L5SS0w#^Mf#sa z`tL>h|CIFKmh|7A^uH$Q|54KahNSWiP5R%0 z^#3I3|9#T`C8YoDNdHqv|2;|n7nA<$k^X-n{r^h(zk~FD2I>D3(*Gf({|iX}8ds>9n$}Cr2mUZ{}rVFR;2%Ax}Q{}s~zex(27N&hdB{GdMkP;CkO$wbYM({3!j$<;wxy z8#g|DK6>*s|qmCQsX?oSE3JTD3ug8#U@ap?Pz+ z_a8s%N1r^|A*exvo}E^&es}i4g9e{pzc#)*V#LgiSFVf-u($70vqXvJ#xj}N>|@6| zdseR8fAQhNz3W+7)o46t&T`X2h2(V?EcnCR$jGEhv0{zNMno*UefMs&Q_r4NYhS&3 zaQOD^r=Es|EiGnZQnp-l^pdW%Ylk){Q^u}Q*|K$R3>~_CXZP+edn{Y_byeriA4WHA z>b?8YrM~JNJ5G1DwH<%+-aVJzb?c5O9uqTnO_wh3_nti)B;1wq;zey&8=F4&zJ04O zcH6e}T|IidxG-qY!T1v=JO>^;cy*+cQ`=cyUbp78Yj2Sw4?*jbZ3JkC7%{MkaLuW$GxJUnt)$Bx-bXXlnx zmo80eyKddR6`eZ0?X_~{j|f-SRD8#^#EP7w;eL=-B%8`}eZI{rhi^*|jTW z%e8A^?iDKr+E`fBdytb;=KI^X=7-Lm8xncyR68p(Gs|kli#I&{{CVAj{{D9w&Yim= zu4T)UcBM)=%}h)4Q>mX^+IZ`~TZzjyDu zB?}iWxB2>YUPzqPv|s)DzM)N;w6`=hEkC18o9kwVhNi8nR2jUdXV3dfQ&W2#IDdX< zi%pwer3D16ahg7To&D_DtIprQ-}uegvD%A|9@W>Tr?(i|xN)~@Lx$|ui;thO?c&9L zi`ut8*Jl0ttSf^D@7(0;`{nJscSdKPKCPP3zyFqZlk1H>~c~kW`G<1AMwW@X(tB^*pSe=^%X+{gF+uae9*HtH!qgjx9`e94IBC= z`uTkh-n%!m)WnI?=clA}ZNlqFc_I1Ut9R}=KKt~kc&&N!c2p`_)Ztuk@V@m|uMRhw zI#uH^Yu4I23m5KkE?>SQEqOBZRgM5+PzMlG3Dj*qT{!2J+aQm=i88d z`!3ZkS+dD$1Q3G&<|BZ31W*J496$iu5P%N?=!gIe5Wso_phf_t5kLR}c!&TNAOH^p zuowZjB7jK0{Dmk<{*F`2p|;!SR;T$1TY8z zbVmSg2tXeJbU*+-5x_eH&;S7#BY>F*U=#xAf&iK$05b&883FW10KE}F4Fs?p0mu;m ze}#2{2?A(@02U&EW(c4f0tiL`rx3tW1W*0s+iL0Phh%5CRbV*G2$+5I_Y4kd6ReAb^7iz!L#nMF4FPz%2xD z0RcQl0A2_n1Oe1U01psANd#~N0bE7^3Is3<0h~bq76`x)0YoBzYy{8}0VE-SdkEky z0{DRdk`cgU1h5$a>_-5t5r7N<+(rN?2p|jr1R{WX2%roCFh>AG5I{QwV2J=4B7nLG z;0^*LY*_2%sAR*o^?DAb@@d;2Z+TLI67vz!wBygaE1{fGr4M zDgtm4Lg@4TeXsBJ`~R=!_x%5v z>zcgIedf%WGxxdg_v@1*W(a^$1i*6w;6nnS9szKV09Zi)947!45&$a+fO!PKw*I(0EG#FiUdF{0^n}~;8y~mJOS_>0q`dQaGwAeKmeR30Q?AmtOS5N z0kDSv_<#V|K>+v=0J8{yR0P161V9=BATt3lmH@ay0Nf$~iVy%l5daYcKw1JIk^m@4 z0OTY9auEQ134o6XfCU6VQv#qG0dSH4_?ZAGMgaUl0F)sB#uEU|2!JC5fad=l0>GC5 zcu4@PCIH$H06htS0|dZU0-y>3P@ez@Aplwv0EY;G!vw%x0$?ct@PYuSLjZ&l0R99( z6af%S06ZfAQV;-R2!O@}Ky?D(YXV>i0nmg1SVRB>5CGE%fX@hkuLyw31i)hgU^4-* zmH;?Q0OThCya<2<0^lJ5aFhTzNC0>e09got+XO%u0q_L@5JUiECjfR50N)b;qX~c- z1V9%8pdA5_g8&#r0CXS##t{IG2!QhhKmh`vH~}!70GLDo)FlA^A^_GC0Nw<^?*u@5 z0w6sBaE<`DL;#c{0ICuI=?DNj0g##iC`bUbB>=V)0LKV`XP5KRC~CII>q05u7KH3UFc z0>DN9q$B{I5C9nofQ|$}1p=TI0kDApm_Pu;5de7zfcXT#Wdh(S0Wg~Y$V342AOOw~ z08++l0JIvXkc|MK{NGLa|CI9oFUtQXl>Z5o|0gN`f2aKal=8m= z<$nsw|KBM8TT=dCr~F?-`5#L8f1C1u8s&dJ%6|vt|8vU!yOjSwQT|V*{C`3DA4vKC zGv$96<$o~c{|A)+%_#q`QvT1S{EwjgzfAdGgYrLw^1nCb|7gnp$CUq(l>gl+|C><$ zAEEqzM)`k*@_!lSe<#ZS;*|d$l>eVo{@11a|BCXzEaiVC%Kvnf|79ruGgAH+r2NlC z`G1k}{~+amL(2a#l>av=|1(qm=b`*>P5ED*^1mYG|9Q&)Unu|EQ2u{Q`9Fm6e;wuj zD9Zn)l>Zkf|JzgkXQBLGO!>dT%zpxa@;`v`{}0Ol;gtVtDgTdC{>M=Mccc6tNclgL z@_#nv|0>G=D9Zm$l>fUZ{~J;M-=X}UK>7bA<$qVo|4fwslPUjiQ2x79{=cUDUqJak zjPkz_<$nXp|AmzQ?I{1pQvRo+{I5m%pP%x-Kjr^@%Ky%k|3fMNS5p4hru_G%{Qr~k z{}tu`Hp>5Tl>a`I|Ai_4cToOsrTniz`9F#Bzcl6l49fo&l>aX&|3^^%@1^|TNcmrm z^8Xs;e=OyH4$A-el>dt;|AQ$1n^XQ*qx?@#`9Fp7e-GtgsT{%@xI&qn#b zg7W_y<$o8-|G||1-jx5JQT}^U{@W=3*Hiv~P5Hlt^1lw{|4z#PDwO|zl>f&l{~u8P zpP>AYru^?q`G1!3-;MHrHRb<2%6~iM|7ptq<&^&qDgO^q{_m&!e?<9TkMiG(^1mPD z|2@k8`jr1|DgP@|{C>%KtNz|FgN!|Fcs5@1y*mNcq2%^1m_V|96!CDJlOyr2J1y z`Ja>WKb-Ra6y^VT%KyWZ|DRC)7o+_Di1I%#<^RW&|4S(U`%wOu)PGpke=_Dzp)CCA zkHw3-FZTfMW4Y(z&IjDRxlOy|W#O((UAI&Voc}LhEOXdmsm49|mfQo$J&@c3$vu$V z1Iay*+yluyklX{wJ&@c3$vu$V1Iaz`e|HZAaEZVzfGY%il)a&ki~Nr>cdhqzv7mFk zB}L+A-}An`GwOnaCDj6clyWWWdO@&j`83O|^||(ZKHf}zbaO2)V0nmZc{$5b8Fj(J ze3qVNvU9nB_A|}7Tv*F9oy*0wJjc0QTFdjD%eq!!S>#;y*YXnQawRRVa4wr`89zCf zYia%ZjA{Al{JgmR=JWB*r_5W`tsjj@S~B41EAyiccoz0L2k ze*Zi^TM6gZ`XF`P)K2^@TWt^5{`8)8`*+_J(80C8dCtu(YMaX%uD1T>BI)g)pLgFp zxOMX+TaS2ukjV??LKU*iAyejE33K9i5KGq=RHtNFj_zlgp=`P#;? z9(}v^W25)a?^xIV5>Kz%@0^b}J9aLFg@tzy>pHMwpYX)?yYKg&YkyB&CqeC`{g>Wi z>5z1n8h7#B|M40Bi3jB6+WNh=#+~+?cs~B^_aC^P@9Wx~#&hCxwMye7>2dzM??Ui( z?T?FS=B9Si{pt90PrT`U+PMGY`v}Imw(xS%)gIr0f61_Y*k3 zpnz>FZQ1ssScz^ms@6Q0yKs$JYYJHOEtuTKI{$vi0|wAcyxGP&+mqk|R_eX!HH(gO zUX=2UPl%0FxoMZ7Hx2K;tzlh}h2RvC(nY&&~ubDo@D1N$G zlE2e{Zb-f*_ds$FB={EAz|EWUDdEgea%n1 zv#ymNemDcegTqfBw?h=xt22b*GJt@=MCuMOyDf8PW`Ldz6Ys0K>H_YzbFx%S=i#of~ zI)0#Y?>??4<#_v~T+S!ueEX!b>^-+@!#r;{%;(%N@7oP4JI~GF^45Go7K>kOo4~e# z?E>2ehV9#7&1YH9$xBGG%6U@KaZ+w{<;40uvBlIF){@U$j;S$#|CbgLQ=w-~p;YI( zjER^$ZTSwCP{+$q$HP#^O}pdFt-;oOV|lj41)F29EiT^29__xp5Ni``*0o3M*0tWp zY<`FB{sEV4j{UZ{n_lCtB=D#`+WW}3D{N)qQR~_hw2#x~A#Lpl@Z}ltuqeC-%fhFy zvOU^X*&Z1iV2^GbYxDR1n^%eBCHE9^K9|AcJPPYt_uX`mv*}gH`I_JABgV=tzJ-QRZF?y&jT5_a1nU#GD~`Ot+o7SUU_m&w|=LTa!XE1Rc+Z`OKNcW&waW?f+94)?&w zo9?#AJ-+tH8{Ff4gR2}HIoEll345(mKQ#GzI&cfrSh`SEC#ulNVnUunnS`u+xo8~{ zlOZlYUxmT>@zreLUVe{j8nEoXhq<-G?(h95>0EF=;O}kb0UI4+gE;!0(P=CC-`}^M3?JYNZOSPUF z!{(ox_@MJb9H*1+ZJya%ZjSAU*<03Vv$x#rUhhF~^eFQz!tBlS6z`^|*_-Ef)4?G| zEQ#;5MnkhV&po^`>)PEsuZG#1RuC%Y*tE6?dkcy7<}g#`W^W<>-iJf|n-}y=Iu}Fz zz4!1=z;2u4f$PaTLZWl?tQyz&=cEw-+^0f$M@2&%v2Wf{sDEyrwF~ep)467<#MJl& zr-ej&>Lq_LPxj;`LkgC)IqoHSvN=$WJk;NFw=M35-hHesy1Je^w>1=pSszcUloOn#C zlVVb{l+M%41W0@pM!jAo>0o;EDmKT9f4oXdkEAbC$NkOgqS>rhhYxSemOj_Xxnz3K##Q_qE7i?KmKlD<`Pakv zr7U-y{$JSWgno7u7551k6dqSg`G>$VN{+@)OXThXFLz@U-pfjICc3E7x z$7tCd8tuKC`G3T`!*^#g`QUhNCS!9w#RQ0^i#?jj?Vp=ElsHQ|YwS#Zo8vz7pNDo? z-X_>#d$i|C&fCi3n!}!ZOmrok`F3-B$}Aqzi#d}trp72e!!c?VhZ=_z`qoyFUY?gP ze@fZxn{+zij)vt_A(mHg@hd_A#R*$!e~HqF3+3S7B$q8o7Qaa@bL5F$*d7|~&i3vk zT@YavcSEtmXmO`I_nW?>Hvg&&Vq5j8e%}X7k2a*^NJF;RW4y@H>LdKU2~(&7<}} zJ;i-T;%^bo6VC%>bztTJ^BWC`_jcm{n;hfW5BTTxcaB8w^#Xqj@b*ZWcqAP`JiIj^ z@fDrF&^UvR2fizs_`p98<-f=}iAKHB^_^qUpk!r}caP~WqUjwO~C+g0YBej)qry@^Slv$&Rd`}}_B?3WP78D}jKV`>ncyZ8F# zKg-O;smRRg&dglKGB24THJLet<#+3;_w&VB z(Ln_{iM2g!(W%Oqx6)#-&ELx!Re)E)Hnt?TNj$EW97wPwu$+6rNzQ|L`}>@Zr+7l` zavap8BnK7mdM7r=#rPi25)$l=^Eym>w<(b$H`D@++g?QFh~AymmSF0+ZZx z^2S1{JczjMceH+tr@uYM-CO;TdXjrO9;qMWUd|Tz)H>vPl2=IMe1#iX*_SOQbQNc7 zw)5fNLL3S4dmrj0Y|#<3*-yE5U*!fDOKw~sbnK4f<{U(@V2=(aWrJ5ix+-|R>!oyB zMzSwkbm#)>TDNykOyc#G=m;JQD)-JQx@n6J=AbCO*q5WOhduIklCF4Z9Xd5M>Z)~o zPjksaTL#bm7l&Yr9yL4u=>zBc(NVakE9>g9D-n0}7DL>w(QO}uIIdXNrk>=cQNneR zggEo&Zkq$)5R)9tjfYf+)D>TG{x?$ns;-!*u@F<^CWvp!kGJPL-fkYsFa>PXA1bl+ zhh|lc%^UTA}<6y93cd+A7&uW1&?qz%M@!tGR6eZ2NxE@EfoUq-xZogx1 zkY8*dCNbBOJR&yhv~e6MgQF56nmZ12{nPDmkl)e3xSJlpx^}>WQ}Ua2n}(>zgN}0voptDy};gEOPQdvg6ZIBkBwfu z$*t-VPD(h8-)G+Som)Sf{&gJLF#mGGeup*c=;$i+M3Q>Q(a4=~U|kv8BQ}libgN^3 z+|4v`H&R$v9`QTuh;yU~Oo-)u23Oe|al%}G)c&~6OyHdibQ}nZP7~eAJ!;?RP`WhO zZ{MwyfsUs^j@Y>Plrf$sI0lPfTwpoR90*-6@l-Ei9!?YF*dBM2m)Y*OCvcLdUtq#^ zdqNy5gR7j4*wkjaj|DmK`m-e50Uz+~UWS8*H z1Bdq?Y`!`zyzkrP&Ye1kCAMpK?Bd%ie7JA#@V?y!ch66G&8_kGkD7?h!UIQ(JHE}X z|5s|6)xHSTmBn(P*RpEj{ot?iiOX6)UoOr_`9}k>#q+Yh;*&l@R*0Vt`E`<5C?v&W z@#U#M8;Pe56yGI|4XVH|r~Jp>MV;1&_K#NoE_Tbcv5+`B<4wkh#d10JbvrRdM4268 z=6if|9=Anb)Qu5$KReb-ydE|U4O%Rp&-*N^cxyzTZQ|Jvn|>~qirji!JeGfOYq405 zOGtuwvv=QAv8Q8ugg9*Y)JI~knyqz_&T?VE22^datZj68tC)Soq6y+&lJ}^{kW<4!LdQh#49Np3=~^U%lC&^d|8XGVw$42eiBn|UqeY^v3&XQ zt_k9I9+i%X4<;5ZEB^G|whzROJyVis7E9YtvfU9&F#7WVPG-Bm8Ij)Q6{@OcSte0-& zH{uOz?UCZXyyKsWiMPlJW7BHccBcftdsr;X`)A54HXQg-Z}A16yezh?93^{L@}Bsy zhFGLZsfuEkGgdRv@-J@NUF{>y+sqZKmabS(T-`05t}0us*IUgHkIwq*7jgclb90G} zcEyer7y6#>DE4SD^dm7_&dj;RbQ@<}5&tZ{AXcn%sQ}m3`CHuVxiX9CGo;NZuGka( zK-{ooZ7Ff@4|%=Cs2ulOi6LnsR*IW0%=t@v)nUvc@$15Fe~X(R4{R&$s@63~EI;~1 z8*xanI?>{JK3Q4p{_@URvGe__>&5-&{Z5HqQJrgx0R_IuBKl_9JzgADuzr;Iw8o~R zVy1G>eiZwBQ*4jepv?R{;={5(ofmI6osdC1{72kSv3ItgONete^#4J8Zkw4&EYRgf zJ#puhmrKN)weBqtPnR6kQoK9ezks->p?#tlcck(e@%6&}Q^bntkIxnljeERUEc*M1 z@5I7W*PRu=-L`@n-(tD?Rn?MWhR=pI7U%vN)>G_McXV@cdzP3dV*L8k)5Z3GS1Bz% zUb*8FaqHN2UyCDW9PT0hQ0hcK@#oBi4vE#~EZ-}BKg!2T?Av)>y!i2^YUjkX!wMf1 zANhRPS9DwVbV&cl5zV#FRKB!e(EIaeN-C}Ilg%!j~n;Q-m7w`Y%rMT+w^h;va)8G4x^=G@^ z7ymwcJ5bEn_~00^%#%;Ei!modhlru8?$!~Thc5k9yy!mbs`$s6BlE>vlX`}T>+_Y2 z6UQ_OuPeIWZ2y(muEG>IanRt2&&AS#>0gK+<>~gb*!1YR&qV*0UN^)n^-lH?r*u5^ zr8vU#n@ZwT@24M%j_O%%iGLOO;Wx3uzK-j}JAdZBELLA#Z-n^a#iR4YT7w?65QqQf z?IUihv}cfb^Wfjj#5PM`g^Aw%YFrd2oC_Kzj(fOrzgTmp^D}2lmM>UY?XZAF+r@!9 z@?;Y|3tibPM%$|w6$3xdkRT3lTT(^*{bx@Pao*KmPK(pyT6PgTjz93R*ne%g6XM{u zy=`LcR_hOo=--T6^ly$6{qN9v^gl~P|7KpJe{&wu|1zyd|1(7NZ{9chH{%@rf2#H9 zzmSOj&A34Sr_@IOV@32oO+^285&d@)(f@1_{hNM9|0&c)|0d7SzsV)^Z^j||zpls8 z|K}q5H|GugpH&yFm5z&7u5&fI_i2l>4js8=L=--TI^l$PC{hQoJ|3B$*^uJL=|7}I|UqVFxW`3am zdupTqZ$$KO@&)~y@q_;JX+8R%ETVri|Iq&ewb6e$5&cJq=--S_^uJVX^q*Qp|4l^n z|4c;xHAM7p@*n-1`HlY9Xg&H*C8B?mkLcfwfAn8a>(T!gBKkM^f&Pc8jsCZY=zo%k z{ws^<-{dFyuctQpH@SfR_o?l@|M}HM|3^ghUsXi^t3~v07194u5&fIoL;sD`M*j;%^xs27|Jg+JpH4*oe~Re8 zj)?yAi0D7Pi2hfI=zoKV{`ZRLKT1UZCSTG2CbiN3D-r#FEuw#uKj?p#+UUQ$i2jF& z=>NQk{=19lzq5$`_lxM?_zV39sEz)8Mf5*PME_4k^q)yY|0bW&e*?AA|3eY|-xkro z$uIQZTW$0|M@0Y6Mf6`lME^TQ^q*5i|EERte^*5RdqngfC!+t?BKof=qW?o8`Y$S? z|H2~r|5il*S4H%nK}7#^MfBfEME~1G^dB#x|MnvKe=MT^ts?p#DWd-$MD+i&i2kdI z=>K~W{Tt7t|Buy1|7k_^|42mtZX)_$BBK9r5&e%A(Z7d?{;!GX|BQ(Kjpxw+akbI^ zNfG_86w$w*i2lop=s#9O|CL1azgR^7t3>plRYd>wMfCr-i2gH*=)a7J{$oV+A1b2% z<|6vPD5C#AMD(9aME~nW^gl*K|L!9CZzrPvK_dDuEu#OAMD*WOMF0LG`p+Vw|0yE+ zA0eXusUrG!i0J<>5&c&X(f=J0{Z|*!|A!*_uO*`Y;UfCqCZhkFBKmJ5qJM7@{ZA0l z|2Pr-*A&rzNfG^riRgczi2glA^dBvv|3DG_4-nD+?;`r2C!+u9BKq$rqW}IP`X4N! z|Jow@H{%NZn{k5v&A3MY?e#eN?T&cRA)^0#BKrSAME}o3^l$nJ{hRlV{>^zr z|K_})|440z{_~6I-{dFy->WwIH|H7soAZYL&3QooreD#2BW;KNSBU6;xrqL&is;{r zZ}e~G2l_YT3;mDLR(f(BKm(M zqW`TT`tKy7|JNe=ZziJu79#q;ETaEgBKp5AqW`-h`u|!)|6U^cpDd#P)FS#%C8GZ= zBKnUJ(Z8AB=>NFd=--(yISRf|8~xu9(SIEg{m&QCe{B){M~mp+%tQ2V<~90Hr}gOH zDx&|qV&V<`n|X@<&AdndW*(t`GcVD9g0@Bfl|}S_LPY;nMD%|~ME@oq(SLKb(SK{5&bt1(SJ@6{cjY}e{m80UlP%Ob`kw&5Yhi05&bU_ z(f`y%>3FQWe_ z5&ahs(SIfp{TCF`e+?1+mlM(dHzN8kBclJZBKmJCqW?cc^q);c{~JW~Zxhjf7ZLqW z5z&7w5&f4G(f@Q2{WlcR{}B=WFBH*#dJ+AP6Vdyf@eKMm-a`L>Yd!j3DWdLT{eLN<|Gpynzb2ypLn8W*7t#M*5&c&b(SKSI{f`vU|4$!S3{)dR@-;96spF(Z)Z~6=Un|wn5<~*VQPI?^un|?(9 z8`Vbt=6s_6*J`7GGylcSe{=rPe@?AO|7KjG|7L2V z|1Ki>?Lhqg8sK?J^Bw2(f=6{{l|;w-yx#^93uMPE24jsbLhXe+UVcR7cJ|C{>O^w zzl4bXGl}T`BN6>)6w!Z_i2i>R(SIos{aZ!!Z{{8PZ=*K)uPCDbbRzmU^AY|3r8fFE zxs3i})kgp4Mf5*ZME{vZ^glyH{|iL)UqD3vxkU7zRz&~NBKlt|qW`=i`oAxt{|FKN z&k@o87!m!uiRgczi2l2Z=>LU?{_BY7{{s>I-x1OORT2IBiRizxi2lD2(f@7{{nr=K z|0WUrKNHb^F%kXG7t#MuBKn^oqW?G%{r@bY|NbKSpDCjM8zTCDDWd;-BKjXCqJMu8 z{o6(KUs*)|`$hDBTtxqmMf5*HME~nV^uIzx|5ZiwKTJgbVIuk;Eu#Mz5&fSQ(SH>Y z{qGRbe>)NV9~RO72@(Al64C#15&ip!=zpGw{;P@Tzp#k@KNQjbmm>OaB%=S%Mf9H_ zqW{ez`p+t&|LY?9?jb{WlcR|0g2)pDv>R??v?Q zE~5Y2BKkilqW@1t^dBms|GOgkUn-*iSt9yBBBKAEBKj{WqW^Fa{kIp<{}d7ZPZZIA zdJ+A16Vd-U5&e6K=>MdM{!fYM{~HngKNZn`77_jbAfo?{BKprQqW^j#`ade7{|6%a z_ZHFr9ufWjEu#NdBKof(qW>Tf{jU_!e|Hi67ZlO|A`$)P5z+q@5&c&e(SHUJ{Vx&G zzo&@)e-Y7tOA-Aa5Yc}*5&icT(f@k!|L@FU#@e}n= zh6fMKyxG#Lml-ds`}O->>znV|WyWvgvSmA{ozl%Mh4@3aZoi46N|Z4DpX2rG>uPs8 zb!v@xZP_w2-oG3&1zxw2pd}6@9eGYNrs8P?wtk0jDd2#E*4^95zeb8}%a=Ff_R)9W zHBr0#pMRS9>T&==AAjdc2vRUnj9v)~v-vPj`0@vFV;YpNruid{9?x zzhlQ&Vo@KTvf?+hW>pfcsZymA*L?Y988K~|G#SN;nKPStGk5IRTx!?2a^<2}_|`3x zH$93JX{h$WpMDx6ejO2UQ_Ph%ZDw(PWTcsAgG-idt#;|0Im?Tsa^9=BNwQ57ejweqVKm7Ld&!g1-xLC2KV$DDPxF8NFQ^w@g7vsm9 ze0kig*f7_3Gi`oi=UOid%d3JTBHfa3DrJ zvvsS3IrcRN460sz zn7HxluM3HPELqY(eAuMPLh<#YMeW4cfPk^$tFQ8lw#t?Ji|&sf z-xn`#-rQM?Tf26s7;*OOO0jeP{I$ihUS7Up&V+ubm$N(&K@@|hj^q>qxs^P^XC_dT?-Tl5=R#= z-dtQZeR?%9a?&K@xBYeNPEotqUw;|j99zG>m)h06y^D&&fB*e^@k;ylo5eio(`OTh zoIAHdtajHf%Ugy!68lrf&PCckk0`KX2J`xtQkBqlaRln>P=ME5pP0 zi&IC8cqHEV@yB|ikB5h;15Rw**iY?A!-m}x`xPowUmRGvbX&1t?%b8d`->O168HS_ z%OJ7I?%iLAAH>HW5Jz6P@QpZc@Zfkcf4+P*#S0A@%o5LU+0s#*5FMRCjG8>zDo*d; z|B_g{X3de}&NXWWh)cV6{Z%||vrQLMrA(PxbbIpTZ!vSmjJd@{9XoCp!zxthDL!k} z>PPYLh7CQ$PbW;sF1C(~nS?%K7-i|f00KP>JD z4E#h~HD^vSacHShABmZ)*1Y0@RjWQ0Up8;PL_E1^Qy;NvwrnLu^ly#>{hR%wf74Is zKSqzEe{;UjzZu`?-;6`_Z{{QVH}41ioA-hKO+TUkQF=c5&mp4!P9pj@^B(V~RzY@{^O%eT@oJ0R++@OCm z9?^drJ&yj(_(A_>JfVLxe$c-ePw3zDC;AW6_UJ!_i2fUi=)bLq{>}VC|K-(2{~jXx zpCO`uGjGs;Gqusb$#wMasW$p=Dx!aLzR`bswb6f35&fI_h5pUFLH}#C9{ro~g8nP2 zjsE9~=)Z=D{tJufzlVta4~ppDtBw9mzN7zAYNP+3MD+ici2h%Q z=>Lg`{zFCdZ{|7r|4nW5|FMYvO&*|sGr!UQ7g~@0AB*U}yO?-G|7Lum{|;J*{_~0G zf3JxC?}+GstBC&Vis;|WXY}7dZS?=Ui2jd@=s#XW|Mf-m-%CXQ#YOc0r-=TuiRgcq zi2lv|L;o$*M*lNK^nXZ1|6U^cH#vp=$El6}?}_L?NJRe|MfCrNi2fgn=>N5d{$oY- zKUqZoD@63aSw#Of5&gT1=>MXK{^LaSA0eXu&La9RE296LBKprLqW?Z3`VSG&zqg3~ zD~Ra7jEMd(i|Bu$i2i>S(SH>Y{kIa)zwsIR|3q!{Ur$8;%SH5mT}1ySMD+i;i2fId z=zq3|{*Q?0e~gI!yNc+4w21zfiReF4MF0Cm^j}Ow|Hnl1Z}J%Z4_6!gUlGxN9ufTy z5z&7&5&gFm(SJh`{pS_Y|2Yx;rxVeCZxQ`p714h?5&icS(f?f${Vx{L{}K`Xe=DN@ zN+SA?7SVq>5&hQ@(f@}c`d=ra|0yE+?;@iAqaymhB%=RcMD+h$ME_|-^j}Cs|0_lG zKUGBkH$?RBBclHkBKn^sqW^v(`X4Bw|AHd=zb~TyJtF#VBBK8fMD#yWME~V~RXBN@_A`$(EiRk~C zi2e_Y=>Jm@{kIm;e-RP=ZxGRcWfA>n5z+t8BKr3i(f=k9{r4Bqze7a--;3yft%&{$ zi0J<_5&ip$=-)1){{#{J4-?V<2oe2H6Vd-u5&e%9(SLRk{ht-lzn_TycZ=wMy@>vI zi0FTni2jF)=s%N){tt-g|D}ljPm1Wjs)+tOi|F5+KlE>&kN!<=qyGnb9R2qa(Z3lV z=zo{m=)bIp{>^zo|J~F^|0P89Z*mR&pHdtBFB8$fnSbct%wP0xas&PE)8puWl!*S# zxJLhGUZDSaT95wkiRjL_7 z{%45jzp#k@D~jm9mWclU7SX@SLG)iH}ro;%jn-%ME_>|qW{%uqyIJ{`tK>C{{tfW-zuX2 zDkA!?FQWet5&gFo(f=V4{T~+5|6LLNo7_SFFVsf=bwu}$Rz&}2Mf7j-7yWywjs6ov^#4#q|3^jie^5mKo+A3sBBKA>BKi*#(f=1B`VSJ( ze|8c5?-bGh_agcqEu#M#BKq$lqW^Xx`p+Sv|3M=9?;xW8aU%L}B%=TGBKj{NqW|I| z`kyYMf8#6kUsrAP|CfmV*Nf=i)HCS+ceT-fdlCJo7t#MY5&d5h(SJD+{Z|#ye>xHU zoBT%qsntgR1x57VRz&~XMf876ME}c0^xsKD|DTHJ|8o)jUlq~+a1s4q7t#M-5&c&Z z(SIKi{l|#t|AdJC8;a=v2NC`E7SVr85&b_B(f>^m{fCR_e}stse-zQbhlu_+is*ls zi2e(S=)bgx{&S1yf3b-Ee-Y9DZV~;*i|GG?i2etQ=s%x`{u_wse~XCzqeb*TSw#Q+ zMf6`&ME`3<^xsuP|27f*rxelu6A}Gq6w!Z25&c&X(SIuu{cjM_{{#{J$BF1akBI)~ zi|GHdi2k37=zq3|{xgZ_zlVta&xq)Ms)+t)is*l?i2n16=zpS!{%?rr|2Gl+w-C{P zQ4#%L6w&__5&eH7qJM{o{xgW^f18N@uZiftwut__i|9X4ME`R{^j}Iu|5g$GuM*LJ za}oV-648G)5&d@-(f>#h{SOt<|HmTwe;}g&ULyMMC!+sdBKj{YqJK9L{dW`5e+d!& zzZTK|DG~iI6Vd+=5&iEM(f=nR`rjv_|4}0Pe=ef`4@LA}PelLsMD)KxME}P{^uJI< z|0_lGKTkye--_sepososMfAT;ME|cu^glyH|Aj^LUr|K=wM6v)w}}3K714ir5&eHB zqW?cd^nYJO{{uwye_BNUej@tMDx!aP5&iEG(f7(f=S3{dW-2|2Pr- zHxkkRc@g~=5Yc~e5&cgW(f=e7{nr)I|6d~dUoWD6ZxQ|fE~5YTBKl7+qW^Ou`oAQi z|8gSwuPUPdbRzn9eBKl7$qW>o%`p+n$|BfR1uOOoTRwDY}Afo>X zBKnUL(SIHh{m&QC|78*VKNZpcY!Uru648GT5&fSL(f?Et{m&H9|6CFM=M~ZaL=pYp z5Yhi{BKmJ3qW_{I`oAcm|0yE+|3*ar4iWul5YhiO5&d5i(SL0b{dX79f1rr|=ZNUP zl!*SVBKlt?qW|V1`rjm?|7;>RHzzD9Ty%HQ!$p%HEc?3J0WOYpF_()KMLs1njvWOy zODMD&PB#a6;~F26(cPPeZ=BasNi7{^$@$_MV0v*gv0O|%m?N!Ind7U@dfjjI;|Dh_ zYe0Bf(pZX`C9=`m;ues@;?BQ-yG<`{NqMQbC57{Q&rEINcErQoO$VWy+lZzXw_#px zUY=<@rt=ErS*PRZu$aT*wmY#G-1YJqy(Xk^*UM!{?Vc7yrkRpDCe>8;X({!*=G>ny zbw00B;(`C;d1>^#)KgN$xKCwIdY(D)E1b{En~D{>{o{EW2JgP!PS(V)c0RA%fAGAQ zJn!CG=ktQhZ2I@F=glU%{j2`Ex52r;4(2Se;lDZlf3-gi3H85alXHKeTGs9T{W;fK zw0#J7U)T0#oawRmw>Ry`#CNML{-axmKF-a1x;WUy|7a)48z=Wbat|c;KynWx_ds$F zB=ReXgy}2hJ9h3B*13DHux=3@2X?VE z8DN?2T;C;p;K2Sgx(x5##raM2o%r(j@PU@;uA?8|YxdA_(4g>vgTn?6?#m+sioUnk zt{r>y*7~xn?>cl~kHO(@o^Nprv!t@5)MuGmd>hyCvy2?vuYbo8b@ZvG{Ve0$EbfEC z2ZveeC4SIppTti!9oRX^N1P6|q;Rbd+|+lkk8XzLnWV^{kV+E(80r9v!lo55Xe=l`zZ z-*sH=1->kBTftqAbN$iWw!7L~lQg$euC_j-Ubpky&Hl~5YH(f7)#kdTx#^0fo5ftW zG;M90w4L9yS>?Kwf3IEZz3uD0_qhJ?#)rEu-s+HZa_1JHd(y{z`3ulK-NP97JvxPT z5AUck&rInW(YJG0Sn2Y9{^gQ1{VV%dN!s+UP}vgJyh~aA@Y4@AZr;UTKg#nXTn~in zfp9$#&I1EG_v>r%9W*$ilW+I%-u=S|4zh%4P9z=VW<}yWFpb2yU>ceyNyq)0l}Tqt rn5I}*k|~xr>XWWB6M{FJWKzEq5@tHTv&I=4W|cEB5~uk;a^rsiEh{Hw diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/libalfcrypto32.so b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/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/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/libalfcrypto64.so b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/libalfcrypto64.so deleted file mode 100644 index a08ac28930fdd54e0eb4e8973a398e5e951d3825..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33417 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 Ko^Mvd(mwtG diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py deleted file mode 100644 index 501aa2d..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py +++ /dev/null @@ -1,543 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# mobidedrm.py -# Copyright © 2008 The Dark Reverser -# Portions © 2008–2017 Apprentice Harper et al. - -__license__ = 'GPL v3' -__version__ = u"0.42" - -# 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 -# 0.38 - Unicode used wherever possible, cope with absent alfcrypto -# 0.39 - Fixed problem with TEXtREAd and getBookType interface -# 0.40 - moved unicode_argv call inside main for Windows DeDRM compatibility -# 0.41 - Fixed potential unicode problem in command line calls -# 0.42 - Added GPL v3 licence. updated/removed some print statements - -import sys -import os -import struct -import binascii -try: - from alfcrypto import Pukall_Cipher -except: - print u"AlfCrypto not found. Using python PC1 implementation." - -# 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] - - -class DrmException(Exception): - pass - - -# -# MobiBook Utility Routines -# - -# Implementation of Pukall Cipher 1 -def PC1(key, src, decryption=True): - # if we can get it from alfcrypto, use that - try: - return Pukall_Cipher().PC1(key,src,decryption) - except NameError: - pass - except TypeError: - 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' - 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 cleanup(self): - # to match function in Topaz book - pass - - def __init__(self, infile): - print u"MobiDeDrm v{0:s}.\nCopyright © 2008-2017 The Dark Reverser, Apprentice Harper 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(u"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]) - - # det default values before PalmDoc test - self.print_replica = False - self.extra_data_flags = 0 - self.meta_array = {} - self.mobi_length = 0 - self.mobi_codepage = 1252 - self.mobi_version = -1 - - if self.magic == 'TEXtREAd': - print u"PalmDoc format book detected." - 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 u"MOBI header version {0:d}, header length {1:d}".format(self.mobi_version, self.mobi_length) - if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5): - self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4]) - #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. - self.extra_data_flags &= 0xFFFE - - # if exth region exists parse it for metadata array - try: - exth_flag, = struct.unpack('>L', self.sect[0x80:0x84]) - exth = '' - if exth_flag & 0x40: - exth = self.sect[16 + self.mobi_length:] - if (len(exth) >= 12) 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: - pass - - 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) - - 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 getFile(self, outpath): - file(outpath,'wb').write(self.mobi_data) - - def getBookType(self): - if self.print_replica: - return u"Print Replica" - if self.mobi_version >= 8: - return u"Kindle Format 8" - if self.mobi_version >= 0: - return u"Mobipocket {0:d}".format(self.mobi_version) - return u"PalmDoc" - - 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 u"Crypto Type is: {0:d}".format(crypto_type) - self.crypto_type = crypto_type - if crypto_type == 0: - 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(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(u"Cannot decode library or rented ebooks.") - - goodpids = [] - for pid in pidlist: - if len(pid)==10: - if checksumPid(pid[0:-2]) != pid: - 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) - else: - print u"Warning: PID {0} has wrong number of digits".format(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(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(u"No key found in {0:d} keys tried.".format(len(goodpids))) - # 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 u"File has default encryption, no specific key needed." - else: - print u"File is encoded with PID {0}.".format(checksumPid(pid)) - - # clear the crypto type - self.patchSection(0, "\0" * 2, 0xC) - - # decrypt sections - 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 u".", - # 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 u"done" - return - -def getUnencryptedBook(infile,pidlist): - if not os.path.isfile(infile): - raise DrmException(u"Input File Not Found.") - book = MobiBook(infile) - book.processBook(pidlist) - return book.mobi_data - - -def cli_main(): - argv=unicode_argv() - progname = os.path.basename(argv[0]) - if len(argv)<3 or len(argv)>4: - print u"MobiDeDrm v{0:s}.\nCopyright © 2008-2017 The Dark Reverser, Apprentice Harper 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(progname) - return 1 - else: - infile = argv[1] - outfile = argv[2] - if len(argv) is 4: - pidlist = argv[3].split(',') - else: - pidlist = [] - try: - stripped_file = getUnencryptedBook(infile, pidlist) - file(outfile, 'wb').write(stripped_file) - except DrmException, e: - print u"MobiDeDRM v{0} Error: {1:s}".format(__version__,e.args[0]) - return 1 - return 0 - - -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/openssl_des.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/openssl_des.py deleted file mode 100644 index 9a84e58..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/openssl_des.py +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab - -# implement just enough of des from openssl to make erdr2pml.py happy - -def load_libcrypto(): - from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_char, c_int, c_long, \ - Structure, c_ulong, create_string_buffer, cast - from ctypes.util import find_library - import sys - - if sys.platform.startswith('win'): - libcrypto = find_library('libeay32') - else: - libcrypto = find_library('crypto') - - if libcrypto is None: - return None - - libcrypto = CDLL(libcrypto) - - # typedef struct DES_ks - # { - # union - # { - # DES_cblock cblock; - # /* make sure things are correct size on machines with - # * 8 byte longs */ - # DES_LONG deslong[2]; - # } ks[16]; - # } DES_key_schedule; - - # 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), - ('DES_cblock2', c_char * 16), - ('DES_cblock3', c_char * 16), - ('DES_cblock4', c_char * 16), - ('DES_cblock5', c_char * 16), - ('DES_cblock6', c_char * 16), - ('DES_cblock7', c_char * 16), - ('DES_cblock8', c_char * 16), - ('DES_cblock9', c_char * 16), - ('DES_cblock10', c_char * 16), - ('DES_cblock11', c_char * 16), - ('DES_cblock12', c_char * 16), - ('DES_cblock13', c_char * 16), - ('DES_cblock14', c_char * 16), - ('DES_cblock15', c_char * 16), - ('DES_cblock16', c_char * 16)] - - DES_KEY_SCHEDULE_p = POINTER(DES_KEY_SCHEDULE) - - def F(restype, name, argtypes): - func = getattr(libcrypto, name) - func.restype = restype - func.argtypes = argtypes - return func - - 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 : - raise Exception('DES improper key used') - return - self.key = key - self.keyschedule = DES_KEY_SCHEDULE() - DES_set_key(self.key, self.keyschedule) - def desdecrypt(self, data): - ob = create_string_buffer(len(data)) - DES_ecb_encrypt(data, ob, self.keyschedule, 0) - return ob.raw - def decrypt(self, data): - if not data: - return '' - i = 0 - result = [] - while i < len(data): - block = data[i:i+8] - processed_block = self.desdecrypt(block) - result.append(processed_block) - i += 8 - return ''.join(result) - - return DES diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/plugin-import-name-dedrm.txt b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/plugin-import-name-dedrm.txt deleted file mode 100644 index e69de29..0000000 diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/prefs.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/prefs.py deleted file mode 100644 index c1bfcb9..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/prefs.py +++ /dev/null @@ -1,295 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai - -from __future__ import with_statement -__license__ = 'GPL v3' - -# Standard Python modules. -import os, sys, re, hashlib -import json -import traceback - -from calibre.utils.config import dynamic, config_dir, JSONConfig -from calibre_plugins.dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION -from calibre.constants import iswindows, isosx - -class DeDRM_Prefs(): - def __init__(self): - JSON_PATH = os.path.join(u"plugins", PLUGIN_NAME.strip().lower().replace(' ', '_') + '.json') - self.dedrmprefs = JSONConfig(JSON_PATH) - - self.dedrmprefs.defaults['configured'] = False - self.dedrmprefs.defaults['bandnkeys'] = {} - self.dedrmprefs.defaults['adeptkeys'] = {} - self.dedrmprefs.defaults['ereaderkeys'] = {} - self.dedrmprefs.defaults['kindlekeys'] = {} - self.dedrmprefs.defaults['androidkeys'] = {} - self.dedrmprefs.defaults['pids'] = [] - self.dedrmprefs.defaults['serials'] = [] - self.dedrmprefs.defaults['adobewineprefix'] = "" - self.dedrmprefs.defaults['kindlewineprefix'] = "" - - # initialise - # we must actually set the prefs that are dictionaries and lists - # to empty dictionaries and lists, otherwise we are unable to add to them - # as then it just adds to the (memory only) dedrmprefs.defaults versions! - if self.dedrmprefs['bandnkeys'] == {}: - self.dedrmprefs['bandnkeys'] = {} - if self.dedrmprefs['adeptkeys'] == {}: - self.dedrmprefs['adeptkeys'] = {} - if self.dedrmprefs['ereaderkeys'] == {}: - self.dedrmprefs['ereaderkeys'] = {} - if self.dedrmprefs['kindlekeys'] == {}: - self.dedrmprefs['kindlekeys'] = {} - if self.dedrmprefs['androidkeys'] == {}: - self.dedrmprefs['androidkeys'] = {} - if self.dedrmprefs['pids'] == []: - self.dedrmprefs['pids'] = [] - if self.dedrmprefs['serials'] == []: - self.dedrmprefs['serials'] = [] - - def __getitem__(self,kind = None): - if kind is not None: - return self.dedrmprefs[kind] - return self.dedrmprefs - - def set(self, kind, value): - self.dedrmprefs[kind] = value - - def writeprefs(self,value = True): - self.dedrmprefs['configured'] = value - - def addnamedvaluetoprefs(self, prefkind, keyname, keyvalue): - try: - if keyvalue not in self.dedrmprefs[prefkind].values(): - # ensure that the keyname is unique - # by adding a number (starting with 2) to the name if it is not - namecount = 1 - newname = keyname - while newname in self.dedrmprefs[prefkind]: - namecount += 1 - newname = "{0:s}_{1:d}".format(keyname,namecount) - # add to the preferences - self.dedrmprefs[prefkind][newname] = keyvalue - return (True, newname) - except: - traceback.print_exc() - pass - return (False, keyname) - - def addvaluetoprefs(self, prefkind, prefsvalue): - # ensure the keyvalue isn't already in the preferences - try: - if prefsvalue not in self.dedrmprefs[prefkind]: - self.dedrmprefs[prefkind].append(prefsvalue) - return True - except: - traceback.print_exc() - return False - - -def convertprefs(always = False): - - def parseIgnobleString(keystuff): - from calibre_plugins.dedrm.ignoblekeygen import generate_key - userkeys = [] - ar = keystuff.split(':') - for keystring in ar: - try: - name, ccn = keystring.split(',') - # Generate Barnes & Noble EPUB user key from name and credit card number. - keyname = u"{0}_{1}".format(name.strip(),ccn.strip()[-4:]) - keyvalue = generate_key(name, ccn) - userkeys.append([keyname,keyvalue]) - except Exception, e: - traceback.print_exc() - print e.args[0] - pass - return userkeys - - def parseeReaderString(keystuff): - from calibre_plugins.dedrm.erdr2pml import getuser_key - userkeys = [] - ar = keystuff.split(':') - for keystring in ar: - try: - name, cc = keystring.split(',') - # Generate eReader user key from name and credit card number. - keyname = u"{0}_{1}".format(name.strip(),cc.strip()[-4:]) - keyvalue = getuser_key(name,cc).encode('hex') - userkeys.append([keyname,keyvalue]) - except Exception, e: - traceback.print_exc() - print e.args[0] - pass - return userkeys - - def parseKindleString(keystuff): - pids = [] - serials = [] - ar = keystuff.split(',') - for keystring in ar: - keystring = str(keystring).strip().replace(" ","") - if len(keystring) == 10 or len(keystring) == 8 and keystring not in pids: - pids.append(keystring) - elif len(keystring) == 16 and keystring[0] == 'B' and keystring not in serials: - serials.append(keystring) - return (pids,serials) - - def getConfigFiles(extension, encoding = None): - # get any files with extension 'extension' in the config dir - userkeys = [] - files = [f for f in os.listdir(config_dir) if f.endswith(extension)] - for filename in files: - try: - fpath = os.path.join(config_dir, filename) - key = os.path.splitext(filename)[0] - value = open(fpath, 'rb').read() - if encoding is not None: - value = value.encode(encoding) - userkeys.append([key,value]) - except: - traceback.print_exc() - pass - return userkeys - - dedrmprefs = DeDRM_Prefs() - - if (not always) and dedrmprefs['configured']: - # We've already converted old preferences, - # and we're not being forced to do it again, so just return - return - - - print u"{0} v{1}: Importing configuration data from old DeDRM plugins".format(PLUGIN_NAME, PLUGIN_VERSION) - - IGNOBLEPLUGINNAME = "Ignoble Epub DeDRM" - EREADERPLUGINNAME = "eReader PDB 2 PML" - OLDKINDLEPLUGINNAME = "K4PC, K4Mac, Kindle Mobi and Topaz DeDRM" - - # get prefs from older tools - kindleprefs = JSONConfig(os.path.join(u"plugins", u"K4MobiDeDRM")) - ignobleprefs = JSONConfig(os.path.join(u"plugins", u"ignoble_epub_dedrm")) - - # Handle the old ignoble plugin's customization string by converting the - # old string to stored keys... get that personal data out of plain sight. - from calibre.customize.ui import config - sc = config['plugin_customization'] - val = sc.pop(IGNOBLEPLUGINNAME, None) - if val is not None: - print u"{0} v{1}: Converting old Ignoble plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION) - priorkeycount = len(dedrmprefs['bandnkeys']) - userkeys = parseIgnobleString(str(val)) - for keypair in userkeys: - name = keypair[0] - value = keypair[1] - dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value) - addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount - print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from old Ignoble plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys") - # Make the json write all the prefs to disk - dedrmprefs.writeprefs(False) - - # Handle the old eReader plugin's customization string by converting the - # old string to stored keys... get that personal data out of plain sight. - val = sc.pop(EREADERPLUGINNAME, None) - if val is not None: - print u"{0} v{1}: Converting old eReader plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION) - priorkeycount = len(dedrmprefs['ereaderkeys']) - userkeys = parseeReaderString(str(val)) - for keypair in userkeys: - name = keypair[0] - value = keypair[1] - dedrmprefs.addnamedvaluetoprefs('ereaderkeys', name, value) - addedkeycount = len(dedrmprefs['ereaderkeys'])-priorkeycount - print u"{0} v{1}: {2:d} eReader {3} imported from old eReader plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys") - # Make the json write all the prefs to disk - dedrmprefs.writeprefs(False) - - # get old Kindle plugin configuration string - val = sc.pop(OLDKINDLEPLUGINNAME, None) - if val is not None: - print u"{0} v{1}: Converting old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION) - priorpidcount = len(dedrmprefs['pids']) - priorserialcount = len(dedrmprefs['serials']) - pids, serials = parseKindleString(val) - for pid in pids: - dedrmprefs.addvaluetoprefs('pids',pid) - for serial in serials: - dedrmprefs.addvaluetoprefs('serials',serial) - addedpidcount = len(dedrmprefs['pids']) - priorpidcount - addedserialcount = len(dedrmprefs['serials']) - priorserialcount - print u"{0} v{1}: {2:d} {3} and {4:d} {5} imported from old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, u"PID" if addedpidcount==1 else u"PIDs", addedserialcount, u"serial number" if addedserialcount==1 else u"serial numbers") - # Make the json write all the prefs to disk - dedrmprefs.writeprefs(False) - - # copy the customisations back into calibre preferences, as we've now removed the nasty plaintext - config['plugin_customization'] = sc - - # get any .b64 files in the config dir - priorkeycount = len(dedrmprefs['bandnkeys']) - bandnfilekeys = getConfigFiles('.b64') - for keypair in bandnfilekeys: - name = keypair[0] - value = keypair[1] - dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value) - addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount - if addedkeycount > 0: - print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key file" if addedkeycount==1 else u"key files") - # Make the json write all the prefs to disk - dedrmprefs.writeprefs(False) - - # get any .der files in the config dir - priorkeycount = len(dedrmprefs['adeptkeys']) - adeptfilekeys = getConfigFiles('.der','hex') - for keypair in adeptfilekeys: - name = keypair[0] - value = keypair[1] - dedrmprefs.addnamedvaluetoprefs('adeptkeys', name, value) - addedkeycount = len(dedrmprefs['adeptkeys'])-priorkeycount - if addedkeycount > 0: - print u"{0} v{1}: {2:d} Adobe Adept {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"keyfile" if addedkeycount==1 else u"keyfiles") - # Make the json write all the prefs to disk - dedrmprefs.writeprefs(False) - - # get ignoble json prefs - if 'keys' in ignobleprefs: - priorkeycount = len(dedrmprefs['bandnkeys']) - for name in ignobleprefs['keys']: - value = ignobleprefs['keys'][name] - dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value) - addedkeycount = len(dedrmprefs['bandnkeys']) - priorkeycount - # no need to delete old prefs, since they contain no recoverable private data - if addedkeycount > 0: - print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from Ignoble plugin preferences.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys") - # Make the json write all the prefs to disk - dedrmprefs.writeprefs(False) - - # get kindle json prefs - priorpidcount = len(dedrmprefs['pids']) - priorserialcount = len(dedrmprefs['serials']) - if 'pids' in kindleprefs: - pids, serials = parseKindleString(kindleprefs['pids']) - for pid in pids: - dedrmprefs.addvaluetoprefs('pids',pid) - if 'serials' in kindleprefs: - pids, serials = parseKindleString(kindleprefs['serials']) - for serial in serials: - dedrmprefs.addvaluetoprefs('serials',serial) - addedpidcount = len(dedrmprefs['pids']) - priorpidcount - if addedpidcount > 0: - print u"{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, u"PID" if addedpidcount==1 else u"PIDs") - addedserialcount = len(dedrmprefs['serials']) - priorserialcount - if addedserialcount > 0: - print u"{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedserialcount, u"serial number" if addedserialcount==1 else u"serial numbers") - try: - if 'wineprefix' in kindleprefs and kindleprefs['wineprefix'] != "": - dedrmprefs.set('adobewineprefix',kindleprefs['wineprefix']) - dedrmprefs.set('kindlewineprefix',kindleprefs['wineprefix']) - print u"{0} v{1}: WINEPREFIX ‘(2)’ imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, kindleprefs['wineprefix']) - except: - traceback.print_exc() - - - # Make the json write all the prefs to disk - dedrmprefs.writeprefs() - print u"{0} v{1}: Finished setting up configuration data.".format(PLUGIN_NAME, PLUGIN_VERSION) diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/pycrypto_des.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/pycrypto_des.py deleted file mode 100644 index 80d7d65..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/pycrypto_des.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab - - -def load_pycrypto(): - try : - from Crypto.Cipher import DES as _DES - except: - return None - - class DES(object): - def __init__(self, key): - if len(key) != 8 : - raise Error('DES improper key used') - self.key = key - self._des = _DES.new(key,_DES.MODE_ECB) - def desdecrypt(self, data): - return self._des.decrypt(data) - def decrypt(self, data): - if not data: - return '' - i = 0 - result = [] - while i < len(data): - block = data[i:i+8] - processed_block = self.desdecrypt(block) - result.append(processed_block) - i += 8 - return ''.join(result) - return DES diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/python_des.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/python_des.py deleted file mode 100644 index bd02904..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/python_des.py +++ /dev/null @@ -1,220 +0,0 @@ -#!/usr/bin/env python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab -import sys - -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, - 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, - 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] - __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, - 23, 24, 25, 26, 27, 28,27, 28, 29, 30, 31, 0] - __sbox = [[14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, - 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8, - 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0, - 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13], - [15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10, - 3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5, - 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15, - 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9], - [10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8, - 13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1, - 13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7, - 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12], - [7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15, - 13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9, - 10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4, - 3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14], - [2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9, - 14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6, - 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14, - 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3], - [12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11, - 10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8, - 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6, - 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13], - [4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1, - 13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6, - 1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2, - 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12], - [13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, - 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2, - 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8, - 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11],] - __p = [15, 6, 19, 20, 28, 11,27, 16, 0, 14, 22, 25, - 4, 17, 30, 9, 1, 7,23,13, 31, 26, 2, 8,18, 12, 29, 5, 21, 10,3, 24] - __fp = [39, 7, 47, 15, 55, 23, 63, 31,38, 6, 46, 14, 54, 22, 62, 30, - 37, 5, 45, 13, 53, 21, 61, 29,36, 4, 44, 12, 52, 20, 60, 28, - 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 - 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.") - self.block_size = 8 - self.key_size = 8 - self.__padding = '' - self.setMode(mode) - if IV: - self.setIV(IV) - self.L = [] - self.R = [] - self.Kn = [ [0] * 48 ] * 16 # 16 48-bit keys (K1 - K16) - self.final = [] - self.setKey(key) - def getKey(self): - return self.__key - def setKey(self, key): - self.__key = key - self.__create_sub_keys() - def getMode(self): - return self.__mode - def setMode(self, mode): - self.__mode = mode - def getIV(self): - return self.__iv - def setIV(self, IV): - if not IV or len(IV) != self.block_size: - raise ValueError("Invalid Initial Value (IV), must be a multiple of " + str(self.block_size) + " bytes") - self.__iv = IV - def getPadding(self): - return self.__padding - def __String_to_BitList(self, data): - l = len(data) * 8 - result = [0] * l - pos = 0 - for c in data: - i = 7 - ch = ord(c) - while i >= 0: - if ch & (1 << i) != 0: - result[pos] = 1 - else: - result[pos] = 0 - pos += 1 - i -= 1 - return result - def __BitList_to_String(self, data): - result = '' - pos = 0 - c = 0 - while pos < len(data): - c += data[pos] << (7 - (pos % 8)) - if (pos % 8) == 7: - result += chr(c) - c = 0 - pos += 1 - return result - def __permutate(self, table, block): - return [block[x] for x in table] - def __create_sub_keys(self): - key = self.__permutate(Des.__pc1, self.__String_to_BitList(self.getKey())) - i = 0 - self.L = key[:28] - self.R = key[28:] - while i < 16: - j = 0 - while j < Des.__left_rotations[i]: - self.L.append(self.L[0]) - del self.L[0] - self.R.append(self.R[0]) - del self.R[0] - j += 1 - self.Kn[i] = self.__permutate(Des.__pc2, self.L + self.R) - i += 1 - def __des_crypt(self, block, crypt_type): - block = self.__permutate(Des.__ip, block) - self.L = block[:32] - self.R = block[32:] - if crypt_type == Des.ENCRYPT: - iteration = 0 - iteration_adjustment = 1 - else: - iteration = 15 - iteration_adjustment = -1 - i = 0 - while i < 16: - tempR = self.R[:] - self.R = self.__permutate(Des.__expansion_table, self.R) - self.R = [x ^ y for x,y in zip(self.R, self.Kn[iteration])] - B = [self.R[:6], self.R[6:12], self.R[12:18], self.R[18:24], self.R[24:30], self.R[30:36], self.R[36:42], self.R[42:]] - j = 0 - Bn = [0] * 32 - pos = 0 - while j < 8: - m = (B[j][0] << 1) + B[j][5] - n = (B[j][1] << 3) + (B[j][2] << 2) + (B[j][3] << 1) + B[j][4] - v = Des.__sbox[j][(m << 4) + n] - Bn[pos] = (v & 8) >> 3 - Bn[pos + 1] = (v & 4) >> 2 - Bn[pos + 2] = (v & 2) >> 1 - Bn[pos + 3] = v & 1 - pos += 4 - j += 1 - self.R = self.__permutate(Des.__p, Bn) - self.R = [x ^ y for x, y in zip(self.R, self.L)] - self.L = tempR - i += 1 - iteration += iteration_adjustment - self.final = self.__permutate(Des.__fp, self.R + self.L) - return self.final - def crypt(self, data, crypt_type): - if not data: - return '' - if len(data) % self.block_size != 0: - if crypt_type == Des.DECRYPT: # Decryption must work on 8 byte blocks - raise ValueError("Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n.") - if not self.getPadding(): - raise ValueError("Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n. Try setting the optional padding character") - else: - data += (self.block_size - (len(data) % self.block_size)) * self.getPadding() - if self.getMode() == CBC: - if self.getIV(): - iv = self.__String_to_BitList(self.getIV()) - else: - raise ValueError("For CBC mode, you must supply the Initial Value (IV) for ciphering") - i = 0 - dict = {} - result = [] - while i < len(data): - block = self.__String_to_BitList(data[i:i+8]) - if self.getMode() == CBC: - if crypt_type == Des.ENCRYPT: - block = [x ^ y for x, y in zip(block, iv)] - processed_block = self.__des_crypt(block, crypt_type) - if crypt_type == Des.DECRYPT: - processed_block = [x ^ y for x, y in zip(processed_block, iv)] - iv = block - else: - iv = processed_block - else: - processed_block = self.__des_crypt(block, crypt_type) - result.append(self.__BitList_to_String(processed_block)) - i += 8 - if crypt_type == Des.DECRYPT and self.getPadding(): - s = result[-1] - while s[-1] == self.getPadding(): - s = s[:-1] - result[-1] = s - return ''.join(result) - def encrypt(self, data, pad=''): - self.__padding = pad - return self.crypt(data, Des.ENCRYPT) - def decrypt(self, data, pad=''): - self.__padding = pad - return self.crypt(data, Des.DECRYPT) diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/scriptinterface.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/scriptinterface.py deleted file mode 100644 index ec86b13..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/scriptinterface.py +++ /dev/null @@ -1,198 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab - -import sys -import os -import re -import ineptepub -import ignobleepub -import epubtest -import zipfix -import ineptpdf -import erdr2pml -import k4mobidedrm -import traceback - -def decryptepub(infile, outdir, rscpath): - errlog = '' - - # first fix the epub to make sure we do not get errors - name, ext = os.path.splitext(os.path.basename(infile)) - bpath = os.path.dirname(infile) - zippath = os.path.join(bpath,name + '_temp.zip') - rv = zipfix.repairBook(infile, zippath) - if rv != 0: - print "Error while trying to fix epub" - return rv - - # determine a good name for the output file - outfile = os.path.join(outdir, name + '_nodrm.epub') - - rv = 1 - # first try with the Adobe adept epub - if ineptepub.adeptBook(zippath): - # try with any keyfiles (*.der) in the rscpath - files = os.listdir(rscpath) - filefilter = re.compile("\.der$", re.IGNORECASE) - files = filter(filefilter.search, files) - if files: - for filename in files: - keypath = os.path.join(rscpath, filename) - userkey = open(keypath,'rb').read() - try: - rv = ineptepub.decryptBook(userkey, zippath, outfile) - if rv == 0: - print "Decrypted Adobe ePub with key file {0}".format(filename) - break - except Exception, e: - errlog += traceback.format_exc() - errlog += str(e) - rv = 1 - # now try with ignoble epub - elif ignobleepub.ignobleBook(zippath): - # try with any keyfiles (*.b64) in the rscpath - files = os.listdir(rscpath) - filefilter = re.compile("\.b64$", re.IGNORECASE) - files = filter(filefilter.search, files) - if files: - for filename in files: - keypath = os.path.join(rscpath, filename) - userkey = open(keypath,'r').read() - #print userkey - try: - rv = ignobleepub.decryptBook(userkey, zippath, outfile) - if rv == 0: - print "Decrypted B&N ePub with key file {0}".format(filename) - break - except Exception, e: - errlog += traceback.format_exc() - errlog += str(e) - rv = 1 - else: - encryption = epubtest.encryption(zippath) - if encryption == "Unencrypted": - print "{0} is not DRMed.".format(name) - rv = 0 - else: - print "{0} has an unknown encryption.".format(name) - - os.remove(zippath) - if rv != 0: - print errlog - return rv - - -def decryptpdf(infile, outdir, rscpath): - errlog = '' - rv = 1 - - # determine a good name for the output file - name, ext = os.path.splitext(os.path.basename(infile)) - outfile = os.path.join(outdir, name + '_nodrm.pdf') - - # try with any keyfiles (*.der) in the rscpath - files = os.listdir(rscpath) - filefilter = re.compile("\.der$", re.IGNORECASE) - files = filter(filefilter.search, files) - if files: - for filename in files: - keypath = os.path.join(rscpath, filename) - userkey = open(keypath,'rb').read() - try: - rv = ineptpdf.decryptBook(userkey, infile, outfile) - if rv == 0: - break - except Exception, e: - errlog += traceback.format_exc() - errlog += str(e) - rv = 1 - - if rv != 0: - print errlog - return rv - - -def decryptpdb(infile, outdir, rscpath): - outname = os.path.splitext(os.path.basename(infile))[0] + ".pmlz" - outpath = os.path.join(outdir, outname) - rv = 1 - socialpath = os.path.join(rscpath,'sdrmlist.txt') - if os.path.exists(socialpath): - keydata = file(socialpath,'r').read() - keydata = keydata.rstrip(os.linesep) - ar = keydata.split(',') - for i in ar: - try: - name, cc8 = i.split(':') - except ValueError: - print ' Error parsing user supplied social drm data.' - return 1 - try: - rv = erdr2pml.decryptBook(infile, outpath, True, erdr2pml.getuser_key(name, cc8)) - except Exception, e: - errlog += traceback.format_exc() - errlog += str(e) - rv = 1 - - if rv == 0: - break - return rv - - -def decryptk4mobi(infile, outdir, rscpath): - rv = 1 - pidnums = [] - pidspath = os.path.join(rscpath,'pidlist.txt') - if os.path.exists(pidspath): - pidstr = file(pidspath,'r').read() - pidstr = pidstr.rstrip(os.linesep) - pidstr = pidstr.strip() - if pidstr != '': - pidnums = pidstr.split(',') - serialnums = [] - serialnumspath = os.path.join(rscpath,'seriallist.txt') - if os.path.exists(serialnumspath): - serialstr = file(serialnumspath,'r').read() - serialstr = serialstr.rstrip(os.linesep) - serialstr = serialstr.strip() - if serialstr != '': - serialnums = serialstr.split(',') - kDatabaseFiles = [] - files = os.listdir(rscpath) - filefilter = re.compile("\.k4i$", re.IGNORECASE) - files = filter(filefilter.search, files) - if files: - for filename in files: - dpath = os.path.join(rscpath,filename) - kDatabaseFiles.append(dpath) - androidFiles = [] - files = os.listdir(rscpath) - filefilter = re.compile("\.ab$", re.IGNORECASE) - files = filter(filefilter.search, files) - if files: - for filename in files: - dpath = os.path.join(rscpath,filename) - androidFiles.append(dpath) - files = os.listdir(rscpath) - filefilter = re.compile("\.db$", re.IGNORECASE) - files = filter(filefilter.search, files) - if files: - for filename in files: - dpath = os.path.join(rscpath,filename) - androidFiles.append(dpath) - files = os.listdir(rscpath) - filefilter = re.compile("\.xml$", re.IGNORECASE) - files = filter(filefilter.search, files) - if files: - for filename in files: - dpath = os.path.join(rscpath,filename) - androidFiles.append(dpath) - try: - rv = k4mobidedrm.decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serialnums, pidnums) - except Exception, e: - errlog += traceback.format_exc() - errlog += str(e) - rv = 1 - - return rv diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/scrolltextwidget.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/scrolltextwidget.py deleted file mode 100644 index 98b4147..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/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/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/simpleprefs.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/simpleprefs.py deleted file mode 100644 index 0809944..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/simpleprefs.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab - -import sys -import os, os.path -import shutil - -class SimplePrefsError(Exception): - pass - -class SimplePrefs(object): - def __init__(self, target, description): - self.prefs = {} - self.key2file={} - self.file2key={} - for keyfilemap in description: - [key, filename] = keyfilemap - self.key2file[key] = filename - self.file2key[filename] = key - self.target = target + 'Prefs' - if sys.platform.startswith('win'): - import _winreg as winreg - regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\") - path = winreg.QueryValueEx(regkey, 'Local AppData')[0] - prefdir = path + os.sep + self.target - elif sys.platform.startswith('darwin'): - home = os.getenv('HOME') - prefdir = os.path.join(home,'Library','Preferences','org.' + self.target) - else: - # linux and various flavors of unix - home = os.getenv('HOME') - prefdir = os.path.join(home,'.' + self.target) - if not os.path.exists(prefdir): - os.makedirs(prefdir) - self.prefdir = prefdir - self.prefs['dir'] = self.prefdir - self._loadPreferences() - - def _loadPreferences(self): - filenames = os.listdir(self.prefdir) - for filename in filenames: - if filename in self.file2key: - key = self.file2key[filename] - filepath = os.path.join(self.prefdir,filename) - if os.path.isfile(filepath): - try : - data = file(filepath,'rb').read() - self.prefs[key] = data - except Exception, e: - pass - - def getPreferences(self): - return self.prefs - - def setPreferences(self, newprefs={}): - if 'dir' not in newprefs: - raise SimplePrefsError('Error: Attempt to Set Preferences in unspecified directory') - if newprefs['dir'] != self.prefs['dir']: - raise SimplePrefsError('Error: Attempt to Set Preferences in unspecified directory') - for key in newprefs: - if key != 'dir': - if key in self.key2file: - filename = self.key2file[key] - filepath = os.path.join(self.prefdir,filename) - data = newprefs[key] - if data != None: - data = str(data) - if data == None or data == '': - if os.path.exists(filepath): - os.remove(filepath) - else: - try: - file(filepath,'wb').write(data) - except Exception, e: - pass - self.prefs = newprefs - return diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/stylexml2css.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/stylexml2css.py deleted file mode 100644 index daa108a..0000000 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/stylexml2css.py +++ /dev/null @@ -1,284 +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 - -debug = False - -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