mirror of
https://github.com/noDRM/DeDRM_tools
synced 2025-01-17 18:12:46 +01:00
tools v5.5
Plugins now include unaltered stand-alone scripts, so no longer need to keep separate copies.
This commit is contained in:
parent
b661a6cdc5
commit
9fda194391
162 changed files with 7115 additions and 26596 deletions
|
@ -1,17 +1,19 @@
|
|||
Ignoble Epub DeDRM - ignobleepub_v02.4_plugin.zip
|
||||
Ignoble Epub DeDRM - ignobleepub_v02.5_plugin.zip
|
||||
=================================================
|
||||
|
||||
All credit given to I♥Cabbages for the original standalone scripts.
|
||||
I had the much easier job of converting them to a calibre plugin.
|
||||
All credit given to i♥cabbages for the original standalone scripts. I had the much easier job of converting them to a calibre plugin.
|
||||
|
||||
This plugin is meant to decrypt Barnes & Noble Epubs that are protected with Adobe's Adept encryption. It is meant to function without having to install any dependencies... other than having calibre installed, of course. It will still work if you have Python and PyCrypto already installed, but they aren't necessary.
|
||||
|
||||
|
||||
Installation:
|
||||
Installation
|
||||
------------
|
||||
|
||||
Go to calibre's Preferences page. Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior". Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (ignobleepub_v02.4_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog.
|
||||
Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior" to go to Calibre's Preferences page. Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (ignobleepub_v02.5_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog.
|
||||
|
||||
|
||||
Configuration:
|
||||
Customization
|
||||
-------------
|
||||
|
||||
Upon first installing the plugin (or upgrading from a version earlier than 0.2.0), the plugin will be unconfigured. Until you create at least one B&N key—or migrate your existing key(s)/data from an earlier version of the plugin—the plugin will not function. When unconfigured (no saved keys)... an error message will occur whenever ePubs are imported to calibre. To eliminate the error message, open the plugin's customization dialog and create/import/migrate a key (or disable/uninstall the plugin). You can get to the plugin's customization dialog by opening calibre's Preferences dialog, and clicking Plugins (under the Advanced section). Once in the Plugin Preferences, expand the "File type plugins" section and look for the "Ignoble Epub DeDRM" plugin. Highlight that plugin and click the "Customize plugin" button.
|
||||
|
||||
|
@ -46,7 +48,8 @@ At the bottom-left of the plugin's customization dialog, you will see a button l
|
|||
|
||||
Once done creating/importing/exporting/deleting decryption keys; click "OK" to exit the customization dialogue (the cancel button will actually work the same way here ... at this point all data/changes are committed already, so take your pick).
|
||||
|
||||
Troubleshooting:
|
||||
Troubleshooting
|
||||
---------------
|
||||
|
||||
If you find that it's not working for you (imported ebooks still have DRM), you can save a lot of time and trouble by first deleting the DRMed ebook from calibre and then trying to add the ebook to calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might as well get used to it. ;)
|
||||
|
||||
|
@ -64,4 +67,4 @@ Now copy the output from the terminal window.
|
|||
On Windows, you must use the window menu (little icon at left of window bar) to select all the text and then to copy it.
|
||||
On Macintosh and Linux, just use the normal text select and copy commands.
|
||||
|
||||
Paste the information into a comment at my blog, describing your problem.
|
||||
Paste the information into a comment at my blog, http://apprenticealf.wordpress.com/ describing your problem.
|
||||
|
|
|
@ -1,25 +1,26 @@
|
|||
Inept Epub DeDRM - ineptepub_v01.9_plugin.zip
|
||||
Inept Epub DeDRM - ineptepub_v02.0_plugin.zip
|
||||
=============================================
|
||||
|
||||
All credit given to I♥Cabbages for the original standalone scripts.
|
||||
I had the much easier job of converting them to a Calibre plugin.
|
||||
All credit given to i♥cabbages for the original standalone scripts. I had the much easier job of converting them to a Calibre plugin.
|
||||
|
||||
This plugin is meant to decrypt Adobe Digital Edition Epubs that are protected with Adobe's Adept encryption. It is meant to function without having to install any dependencies... other than having Calibre installed, of course. It will still work if you have Python and PyCrypto already installed, but they aren't necessary.
|
||||
|
||||
|
||||
Installation:
|
||||
Installation
|
||||
------------
|
||||
|
||||
Go to Calibre's Preferences page. Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Cahnge calibre behavior". Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (ineptepub_v01.9_plugin.zip) and click the 'Add' button. you're done.
|
||||
|
||||
Please note: Calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added.
|
||||
Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior" to go to Calibre's Preferences page. Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (ineptepub_v02.0_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog.
|
||||
|
||||
|
||||
Configuration:
|
||||
|
||||
Customization
|
||||
-------------
|
||||
|
||||
When first run, the plugin will attempt to find your Adobe Digital Editions installation (on Windows and Mac OS). If successful, it will create 'calibre-adeptkey[number].der' file(s) and save them in Calibre's configuration directory. It will use those files and any other '*.der' files in any decryption attempts. If there is already at least one 'calibre-adept*.der' file in the directory, the plugin won't attempt to find the Adobe Digital Editions installation keys again.
|
||||
|
||||
So if you have Adobe Digital Editions installation installed on the same machine as Calibre... you are ready to go. If not... keep reading.
|
||||
|
||||
If you already have keyfiles generated with I♥Cabbages' ineptkey.pyw script, you can put those keyfiles in Calibre's configuration directory. The easiest way to find the correct directory is to go to Calibre's Preferences page... click on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre configuration directory' button. Paste your keyfiles in there. Just make sure that they have different names and are saved with the '.der' extension (like the ineptkey script produces). This directory isn't touched when upgrading Calibre, so it's quite safe to leave them there.
|
||||
If you already have keyfiles generated with i♥cabbages' ineptkey.pyw script, you can put those keyfiles in Calibre's configuration directory. The easiest way to find the correct directory is to go to Calibre's Preferences page... click on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre configuration directory' button. Paste your keyfiles in there. Just make sure that they have different names and are saved with the '.der' extension (like the ineptkey script produces). This directory isn't touched when upgrading Calibre, so it's quite safe to leave them there.
|
||||
|
||||
Since there is no Linux version of Adobe Digital Editions, Linux users will have to obtain a keyfile through other methods and put the file in Calibre's configuration directory.
|
||||
|
||||
|
@ -29,7 +30,8 @@ All keyfiles with a '.der' extension found in Calibre's configuration directory
|
|||
** NOTE ** There is no plugin customization data for the Inept Epub DeDRM plugin.
|
||||
|
||||
|
||||
Troubleshooting:
|
||||
Troubleshooting
|
||||
---------------
|
||||
|
||||
If you find that it's not working for you (imported ebooks still have DRM), you can save a lot of time and trouble by first deleting the DRMed ebook from calibre and then trying to add the ebook to calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might as well get used to it. ;)
|
||||
|
||||
|
@ -47,4 +49,67 @@ Now copy the output from the terminal window.
|
|||
On Windows, you must use the window menu (little icon at left of window bar) to select all the text and then to copy it.
|
||||
On Macintosh and Linux, just use the normal text select and copy commands.
|
||||
|
||||
Paste the information into a comment at my blog, describing your problem.
|
||||
Paste the information into a comment at my blog, http://apprenticealf.wordpress.com/ describing your problem.
|
||||
|
||||
|
||||
Linux and Adobe Digital Editions ePubs
|
||||
--------------------------------------
|
||||
|
||||
Here are the instructions for using the tools with ePub books and Adobe Digital Editions on Linux under Wine. (Thank you mclien!)
|
||||
|
||||
|
||||
1. download the most recent version of wine from winehq.org (1.3.29 in my case)
|
||||
|
||||
For debian users:
|
||||
|
||||
to get a recent version of wine I decited to use aptosid (2011-02, xfce)
|
||||
(because I’m used to debian)
|
||||
install aptosid and upgrade it (see aptosid site for detaild instructions)
|
||||
|
||||
|
||||
2. properly install Wine (see the Wine site for details)
|
||||
|
||||
For debian users:
|
||||
|
||||
cd to this dir and install the packages as root:
|
||||
‘dpkg -i *.deb’
|
||||
you will get some error messages, which can be ignored.
|
||||
again as root use
|
||||
‘apt-get -f install’ to correct this errors
|
||||
|
||||
3. python 2.7 should already be installed on your system but you may need the following additional python package
|
||||
|
||||
'apt-get install python-tk’
|
||||
|
||||
4. all programms need to be installed as normal user. All these programm are installed the same way:
|
||||
‘wine ‘
|
||||
we need:
|
||||
a) Adobe Digital Edition 1.7.2(from: http://kb2.adobe.com/cps/403/kb403051.html)
|
||||
(there is a “can’t install ADE” site, where the setup.exe hides)
|
||||
|
||||
b) ActivePython-2.7.2.5-win32-x86.msi (from: http://www.activestate.com/activepython/downloads)
|
||||
|
||||
c) Win32OpenSSL_Light-0_9_8r.exe (from: http://www.slproweb.com/)
|
||||
|
||||
d) pycrypto-2.3.win32-py2.7.msi (from: http://www.voidspace.org.uk/python/modules.shtml)
|
||||
|
||||
5. now get and unpack the very latest tools_vX.X (from Apprentice Alf) in the users drive_c of wine
|
||||
(~/.wine/drive_c/)
|
||||
|
||||
6. start ADE with:
|
||||
‘wine digitaleditions.exe’ or from the start menue wine-adobe-digital..
|
||||
|
||||
7. register this instance of ADE with your adobeID and close it
|
||||
change to the tools_vX.X dir:
|
||||
cd ~/.wine/drive_c/tools_vX.X/Other_Tools/
|
||||
|
||||
8. create the adeptkey.der with:
|
||||
‘wine python ineptkey.py’ (only need once!)
|
||||
(key will be here: ~/.wine/drive_c/tools_vX.X/Other_Tools/adeptkey.der)
|
||||
|
||||
9. Use ADE running under Wine to dowload all of your purchased ePub ebooks
|
||||
|
||||
10. install the ineptepub and ineptpdf plugins from the tools as discribed in the readmes.
|
||||
|
||||
11. copy the adeptkey.der into the config dir of calibre (~/.config/calibre in debian). Your ADE books imported to calibre will automatically be freed from DRM.
|
||||
|
||||
|
|
|
@ -1,23 +1,25 @@
|
|||
Inept PDF Plugin - ineptpdf_v01.8_plugin.zip
|
||||
Inept PDF Plugin - ineptpdf_v01.9_plugin.zip
|
||||
============================================
|
||||
|
||||
All credit given to I♥Cabbages for the original standalone scripts.
|
||||
I had the much easier job of converting them to a Calibre plugin.
|
||||
All credit given to i♥cabbages for the original standalone scripts. I had the much easier job of converting them to a Calibre plugin.
|
||||
|
||||
This plugin is meant to decrypt Adobe Digital Edition PDFs that are protected with Adobe's Adept encryption. It is meant to function without having to install any dependencies... other than having Calibre installed, of course. It will still work if you have Python, PyCrypto and/or OpenSSL already installed, but they aren't necessary.
|
||||
|
||||
|
||||
Installation:
|
||||
Installation
|
||||
------------
|
||||
|
||||
Go to calibre's Preferences page. Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior". Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (ineptpdf_v01.8_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog.
|
||||
Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior" to go to Calibre's Preferences page. Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (ineptpdf_v01.9_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog.
|
||||
|
||||
|
||||
Configuration:
|
||||
Customization
|
||||
-------------
|
||||
|
||||
When first run, the plugin will attempt to find your Adobe Digital Editions installation (on Windows and Mac OS). If successful, it will create 'calibre-adeptkey[number].der' file(s) and save them in Calibre's configuration directory. It will use those files and any other '*.der' files in any decryption attempts. If there is already at least one 'calibre-adept*.der' file in the directory, the plugin won't attempt to find the Adobe Digital Editions installation keys again.
|
||||
|
||||
So if you have Adobe Digital Editions installation installed on the same machine as Calibre... you are ready to go. If not... keep reading.
|
||||
|
||||
If you already have keyfiles generated with I♥Cabbages' ineptkey.pyw script, you can put those keyfiles in Calibre's configuration directory. The easiest way to find the correct directory is to go to Calibre's Preferences page... click on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre configuration directory' button. Paste your keyfiles in there. Just make sure that
|
||||
If you already have keyfiles generated with i♥cabbages' ineptkey.pyw script, you can put those keyfiles in Calibre's configuration directory. The easiest way to find the correct directory is to go to Calibre's Preferences page... click on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre configuration directory' button. Paste your keyfiles in there. Just make sure that
|
||||
they have different names and are saved with the '.der' extension (like the ineptkey script produces). This directory isn't touched when upgrading Calibre, so it's quite safe to leave them there.
|
||||
|
||||
Since there is no Linux version of Adobe Digital Editions, Linux users will have to obtain a keyfile through other methods and put the file in Calibre's configuration directory.
|
||||
|
@ -27,7 +29,8 @@ All keyfiles with a '.der' extension found in Calibre's configuration directory
|
|||
** NOTE ** There is no plugin customization data for the Inept PDF plugin.
|
||||
|
||||
|
||||
Troubleshooting:
|
||||
Troubleshooting
|
||||
---------------
|
||||
|
||||
If you find that it's not working for you (imported ebooks still have DRM), you can save a lot of time and trouble by first deleting the DRMed ebook from calibre and then trying to add the ebook to calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might as well get used to it. ;)
|
||||
|
||||
|
@ -45,4 +48,67 @@ Now copy the output from the terminal window.
|
|||
On Windows, you must use the window menu (little icon at left of window bar) to select all the text and then to copy it.
|
||||
On Macintosh and Linux, just use the normal text select and copy commands.
|
||||
|
||||
Paste the information into a comment at my blog, describing your problem.
|
||||
Paste the information into a comment at my blog, http://apprenticealf.wordpress.com/ describing your problem.
|
||||
|
||||
|
||||
Linux and Adobe Digital Editions PDFs
|
||||
--------------------------------------
|
||||
|
||||
Here are the instructions for using the tools with ePub books and Adobe Digital Editions on Linux under Wine. (Thank you mclien!)
|
||||
|
||||
|
||||
1. download the most recent version of wine from winehq.org (1.3.29 in my case)
|
||||
|
||||
For debian users:
|
||||
|
||||
to get a recent version of wine I decited to use aptosid (2011-02, xfce)
|
||||
(because I’m used to debian)
|
||||
install aptosid and upgrade it (see aptosid site for detaild instructions)
|
||||
|
||||
|
||||
2. properly install Wine (see the Wine site for details)
|
||||
|
||||
For debian users:
|
||||
|
||||
cd to this dir and install the packages as root:
|
||||
‘dpkg -i *.deb’
|
||||
you will get some error messages, which can be ignored.
|
||||
again as root use
|
||||
‘apt-get -f install’ to correct this errors
|
||||
|
||||
3. python 2.7 should already be installed on your system but you may need the following additional python package
|
||||
|
||||
'apt-get install python-tk’
|
||||
|
||||
4. all programms need to be installed as normal user. All these programm are installed the same way:
|
||||
‘wine ‘
|
||||
we need:
|
||||
a) Adobe Digital Edition 1.7.2(from: http://kb2.adobe.com/cps/403/kb403051.html)
|
||||
(there is a “can’t install ADE” site, where the setup.exe hides)
|
||||
|
||||
b) ActivePython-2.7.2.5-win32-x86.msi (from: http://www.activestate.com/activepython/downloads)
|
||||
|
||||
c) Win32OpenSSL_Light-0_9_8r.exe (from: http://www.slproweb.com/)
|
||||
|
||||
d) pycrypto-2.3.win32-py2.7.msi (from: http://www.voidspace.org.uk/python/modules.shtml)
|
||||
|
||||
5. now get and unpack the very latest tools_vX.X (from Apprentice Alf) in the users drive_c of wine
|
||||
(~/.wine/drive_c/)
|
||||
|
||||
6. start ADE with:
|
||||
‘wine digitaleditions.exe’ or from the start menue wine-adobe-digital..
|
||||
|
||||
7. register this instance of ADE with your adobeID and close it
|
||||
change to the tools_vX.X dir:
|
||||
cd ~/.wine/drive_c/tools_vX.X/Other_Tools/
|
||||
|
||||
8. create the adeptkey.der with:
|
||||
‘wine python ineptkey.py’ (only need once!)
|
||||
(key will be here: ~/.wine/drive_c/tools_vX.X/Other_Tools/adeptkey.der)
|
||||
|
||||
9. Use ADE running under Wine to dowload all of your purchased ePub ebooks
|
||||
|
||||
10. install the ineptepub and ineptpdf plugins from the tools as discribed in the readmes.
|
||||
|
||||
11. copy the adeptkey.der into the config dir of calibre (~/.config/calibre in debian). Your ADE books imported to calibre will automatically be freed from DRM.
|
||||
|
||||
|
|
|
@ -1,38 +1,37 @@
|
|||
K4MobiDeDRM_v04.7_plugin.zip
|
||||
Kindle and Mobipocket Plugin - K4MobiDeDRM_v04.10_plugin.zip
|
||||
============================================================
|
||||
|
||||
Credit given to The Dark Reverser for the original standalone script. Credit also to the many people who have updated and expanded that script since then.
|
||||
|
||||
Plugin for K4PC, K4Mac, eInk Kindles and Mobipocket.
|
||||
|
||||
This plugin supersedes MobiDeDRM, K4DeDRM, and K4PCDeDRM and K4X plugins. If you install this plugin, those plugins should be removed.
|
||||
This plugin supersedes MobiDeDRM, K4DeDRM, and K4PCDeDRM and K4X plugins. If you install this plugin, those plugins should be removed, as should any earlier versions of this plugin.
|
||||
|
||||
This plugin is meant to remove the DRM from .prc, .mobi, .azw, .azw1, .azw3, .azw4 and .tpz ebooks. Calibre can then convert them to whatever format you desire. It is meant to function without having to install any dependencies except for Calibre being on your same machine and in the same account as your "Kindle for PC" or "Kindle for Mac" application if you are going to remove the DRM from those types of books.
|
||||
This plugin is meant to remove the DRM from .prc, .mobi, .azw, .azw1, .azw3, .azw4 and .tpz ebooks. Calibre can then convert them to whatever format you desire. It is meant to function without having to install any dependencies except for Calibre being on your same machine and in the same account as your "Kindle for PC" or "Kindle for Mac" application if you are going to remove the DRM from books from those programs.
|
||||
|
||||
|
||||
Installation:
|
||||
Installation
|
||||
------------
|
||||
|
||||
Go to calibre's Preferences page. Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior". Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (K4MobiDeDRM_v04.7_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog.
|
||||
Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior" to go to Calibre's Preferences page. Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (K4MobiDeDRM_v04.10_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog.
|
||||
|
||||
Make sure that you delete any old versions of the plugin. They might interfere with the operation of the new one.
|
||||
|
||||
|
||||
Configuration:
|
||||
Customization
|
||||
-------------
|
||||
|
||||
Highlight the plugin (K4MobiDeDRM under the "File type plugins" category) and click the "Customize Plugin" button on Calibre's Preferences->Plugins page.
|
||||
|
||||
If you have an eInk Kindle enter the 16 character serial number (these all begin a "B") in the serial numbers field. The easiest way to make sure that you have the serial number right is to copy it from your Amazon account pages (the "Manage Your Devices" page). If you have more than one eInk Kindle, you can enter multiple serial numbers separated by commas.
|
||||
If you have an eInk Kindle enter the 16 character serial number (these all begin a "B" or a "9") in the serial numbers field. The easiest way to make sure that you have the serial number right is to copy it from your Amazon account pages (the "Manage Your Devices" page). If you have more than one eInk Kindle, you can enter multiple serial numbers separated by commas.
|
||||
|
||||
If you have Mobipocket books, enter your 8 or 10 digit PID in the Mobipocket PIDs field. If you have more than one PID, separate them with commas.
|
||||
|
||||
These configuration steps are not needed if you only want to decode "Kindle for PC" or "Kindle for Mac" books.
|
||||
|
||||
|
||||
Linux Systems Only:
|
||||
|
||||
If you install Kindle for PC in Wine, the plugin should be able to decode files from that Kindle for PC installation under Wine. You might need to enter a Wine Prefix if it's not already set in your Environment variables.
|
||||
|
||||
|
||||
Troubleshooting:
|
||||
Troubleshooting
|
||||
---------------
|
||||
|
||||
If you find that it's not working for you (imported ebooks still have DRM), you can save a lot of time and trouble by first deleting the DRMed ebook from calibre and then trying to add the ebook to calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might as well get used to it. ;)
|
||||
|
||||
|
@ -50,6 +49,74 @@ Now copy the output from the terminal window.
|
|||
On Windows, you must use the window menu (little icon at left of window bar) to select all the text and then to copy it.
|
||||
On Macintosh and Linux, just use the normal text select and copy commands.
|
||||
|
||||
Paste the information into a comment at my blog, describing your problem.
|
||||
Paste the information into a comment at my blog, http://apprenticealf.wordpress.com/ describing your problem.
|
||||
|
||||
|
||||
|
||||
Linux Systems Only
|
||||
-----------------
|
||||
|
||||
If you install Kindle for PC in Wine, the plugin should be able to decode files from that Kindle for PC installation under Wine. You might need to enter a Wine Prefix if it's not already set in your Environment variables. You will need to install Python and PyCrypto under Wine as detailed below. In addition, some people who have successfully used the plugin in this way have commented as follows:
|
||||
|
||||
Here are the instructions for using Kindle for PC on Linux under Wine. (Thank you Eyeless and Pete)
|
||||
|
||||
1. upgrade to very recent versions of Wine; This has been tested with Wine 1.3.15 – 1.3.2X. It may work with earlier versions but no promises. It does not work with wine 1.2.X versions.
|
||||
|
||||
If you have not already installed Kindle for PC under wine, follow steps 2 and 3 otherwise jump to step 4
|
||||
|
||||
2. Some versions of winecfg have a bug in setting the volume serial number, so create a .windows-serial file at root of drive_c to set a proper windows volume serial number (8 digit hex value for unsigned integer).
|
||||
cd ~
|
||||
cd .wine
|
||||
cd drive_c
|
||||
echo deadbeef > .windows-serial
|
||||
|
||||
Replace "deadbeef" with whatever hex value you want but I would stay away from the default setting of "ffffffff" which does not seem to work. BTW: deadbeef is itself a valid possible hex value if you want to use it
|
||||
|
||||
3. Only ***after*** setting the volume serial number properly – download and install under wine K4PC version for Windows. Register it and download from your Archive one of your Kindle ebooks. Versions known to work are K4PC 1.7.1 and earlier. Later version may work but no promises.
|
||||
|
||||
|
||||
FIRST user
|
||||
----------
|
||||
Hi everyone, I struggled to get this working on Ubuntu 12.04. Here are the secrets for everyone:
|
||||
|
||||
1. Make sure your Wine installation is set up to be 32 bit. 64 bit is not going to work! To do this, remove your .wine directory (or use a different wineprefix). Then use WINEARCH=win32 winecfg
|
||||
|
||||
2. But wait, you can’t install Kindle yet. It won’t work. You need to do: winetricks -q vcrun2008 or else you’ll get an error: unimplemented function msvcp90.dll .
|
||||
|
||||
3. Now download and install Kindle for PC and download your content as normal.
|
||||
|
||||
4. Now download and install Python 2.7 32 bit for Windows from python.org, 32 bit, install it the usual way, and you can now run the Kindle DRM tools.
|
||||
|
||||
SECOND USER
|
||||
-----------
|
||||
It took a while to figure out that I needed wine 32 bit, plus Python 27 32 bit, plus the winetricks, to get all this working together but once it’s done, it’s great and I can read my Kindle content on my Nook Color running Cyanogenmod!!!
|
||||
Linux Systems Only:
|
||||
For all of the following wine installs, use WINEARCH=win32 if you are on x86_64. Also remember that in order to execute a *.msi file, you have to run ‘WINEARCH=win32 wine msiexec /i xxxxx.msi’.
|
||||
1. Install Kindle for PC with wine.
|
||||
2. Install ActivePython 2.7.x (Windows x86) with wine from here: http://www.activestate.com/activepython/downloads
|
||||
3. Install the pycrypto (Windows 32 bit for Python 2.7) module with wine from here: http://www.voidspace.org.uk/python/modules.shtml#pycrypto
|
||||
4. Install the K4MobiDeDRM plugin into your _Linux_ Calibre installation
|
||||
Now all Kindle books downloaded from Kindle for PC in Wine will be automatically de-DRM’d when they are added to your _Linux_ Calibre. As always, you can troubleshoot problems by adding a book from the terminal using ‘calibredb add xxxx’.
|
||||
|
||||
Or something like that! Hope that helps someone out.
|
||||
|
||||
|
||||
Installing Python on Windows
|
||||
----------------------------
|
||||
I strongly recommend fully installing ActiveState’s Active Python, free Community Edition for Windows (x86) 32 bits. This is a free, full version of the Python. It comes with some important additional modules that are not included in the bare-bones version from www.python.org unless you choose to install everything.
|
||||
|
||||
1. Download ActivePython 2.7.X for Windows (x86) (or later 2.7 version for Windows (x86) ) from http://www.activestate.com/activepython/downloads. Do not download the ActivePython 2.7.X for Windows (64-bit, x64) verson, even if you are running 64-bit Windows.
|
||||
|
||||
2. When it has finished downloading, run the installer. Accept the default options.
|
||||
|
||||
|
||||
Installing PyCrypto on Windows
|
||||
------------------------------
|
||||
PyCrypto is a set of encryption/decryption routines that work with Python. The sources are freely available, and compiled versions are available from several sources. You must install a version that is for 32-bit Windows and Python 2.7. I recommend the installer linked from Michael Foord’s blog.
|
||||
|
||||
1. Download PyCrypto 2.1 for 32bit Windows and Python 2.7 from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
|
||||
|
||||
2. When it has finished downloading, unzip it. This will produce a file “pycrypto-2.1.0.win32-py2.7.exe”.
|
||||
|
||||
3. Double-click “pycrypto-2.1.0.win32-py2.7.exe” to run it. Accept the default options.
|
||||
|
||||
|
|
|
@ -1,30 +1,69 @@
|
|||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
from calibre.customize import FileTypePlugin
|
||||
from calibre.gui2 import is_ok_to_use_qt
|
||||
from calibre.utils.config import config_dir
|
||||
from calibre.constants import iswindows, isosx
|
||||
# from calibre.ptempfile import PersistentTemporaryDirectory
|
||||
__license__ = 'GPL v3'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
#
|
||||
# Requires Calibre version 0.7.55 or higher.
|
||||
#
|
||||
# All credit given to The Dark Reverser for the original mobidedrm script.
|
||||
# Thanks to all those who've worked on the scripts since 2008 to improve
|
||||
# the support for formats and sources.
|
||||
#
|
||||
# Revision history:
|
||||
# 0.4.8 - Major code change to use unaltered k4mobidedrm.py 4.8 and later
|
||||
# 0.4.9 - typo fix
|
||||
# 0.4.10 - Another Topaz Fix (class added to page and group and region)
|
||||
|
||||
"""
|
||||
Decrypt Amazon Kindle and Mobipocket encrypted ebooks.
|
||||
"""
|
||||
|
||||
PLUGIN_NAME = u"Kindle and Mobipocket DeDRM"
|
||||
PLUGIN_VERSION_TUPLE = (0, 4, 10)
|
||||
PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE])
|
||||
|
||||
import sys, os, re
|
||||
import time
|
||||
from zipfile import ZipFile
|
||||
|
||||
from calibre.customize import FileTypePlugin
|
||||
from calibre.constants import iswindows, isosx
|
||||
from calibre.gui2 import is_ok_to_use_qt
|
||||
from calibre.utils.config import config_dir
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
|
||||
class K4DeDRM(FileTypePlugin):
|
||||
name = 'Kindle and Mobipocket DeDRM' # Name of the plugin
|
||||
description = 'Removes DRM from eInk Kindle, Kindle 4 Mac and Kindle 4 PC ebooks, and from Mobipocket ebooks. Provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, mdlnx, ApprenticeAlf, etc.'
|
||||
name = PLUGIN_NAME
|
||||
description = u"Removes DRM from eInk Kindle, Kindle 4 Mac and Kindle 4 PC ebooks, and from Mobipocket ebooks. Provided by the work of many including The Dark Reverser, DiapDealer, SomeUpdates, i♥cabbages, CMBDTC, Skindle, mdlnx, ApprenticeAlf, and probably others."
|
||||
supported_platforms = ['osx', 'windows', 'linux'] # Platforms this plugin will run on
|
||||
author = 'DiapDealer, SomeUpdates, mdlnx, Apprentice Alf' # The author of this plugin
|
||||
version = (0, 4, 7) # The version number of this plugin
|
||||
author = u"DiapDealer, SomeUpdates, mdlnx, Apprentice Alf and The Dark Reverser"
|
||||
version = PLUGIN_VERSION_TUPLE
|
||||
file_types = set(['prc','mobi','azw','azw1','azw3','azw4','tpz']) # The file types that this plugin will be applied to
|
||||
on_import = True # Run this plugin during the import
|
||||
priority = 520 # run this plugin before earlier versions
|
||||
priority = 521 # run this plugin before earlier versions
|
||||
minimum_calibre_version = (0, 7, 55)
|
||||
|
||||
def initialize(self):
|
||||
|
@ -37,45 +76,39 @@ class K4DeDRM(FileTypePlugin):
|
|||
so the CDLL stuff will work in the alfcrypto.py script.
|
||||
"""
|
||||
if iswindows:
|
||||
names = ['alfcrypto.dll','alfcrypto64.dll']
|
||||
names = [u"alfcrypto.dll",u"alfcrypto64.dll"]
|
||||
elif isosx:
|
||||
names = ['libalfcrypto.dylib']
|
||||
names = [u"libalfcrypto.dylib"]
|
||||
else:
|
||||
names = ['libalfcrypto32.so','libalfcrypto64.so','alfcrypto.py','alfcrypto.dll','alfcrypto64.dll','getk4pcpids.py','mobidedrm.py','kgenpids.py','k4pcutils.py','topazextract.py','outputfix.py']
|
||||
names = [u"libalfcrypto32.so",u"libalfcrypto64.so",u"alfcrypto.py",u"alfcrypto.dll",u"alfcrypto64.dll",u"getk4pcpids.py",u"k4mobidedrm.py",u"mobidedrm.py",u"kgenpids.py",u"k4pcutils.py",u"topazextract.py"]
|
||||
lib_dict = self.load_resources(names)
|
||||
self.alfdir = os.path.join(config_dir, 'alfcrypto')
|
||||
self.alfdir = os.path.join(config_dir,u"alfcrypto")
|
||||
if not os.path.exists(self.alfdir):
|
||||
os.mkdir(self.alfdir)
|
||||
for entry, data in lib_dict.items():
|
||||
file_path = os.path.join(self.alfdir, entry)
|
||||
with open(file_path,'wb') as f:
|
||||
f.write(data)
|
||||
open(file_path,'wb').write(data)
|
||||
|
||||
def run(self, path_to_ebook):
|
||||
# make sure any unicode output gets converted safely with 'replace'
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
|
||||
starttime = time.time()
|
||||
print u"{0} v{1}: Trying to decrypt {2}.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))
|
||||
|
||||
# add the alfcrypto directory to sys.path so alfcrypto.py
|
||||
# will be able to locate the custom lib(s) for CDLL import.
|
||||
sys.path.insert(0, self.alfdir)
|
||||
# Had to move these imports here so the custom libs can be
|
||||
# extracted to the appropriate places beforehand these routines
|
||||
# look for them.
|
||||
from calibre_plugins.k4mobidedrm import kgenpids, topazextract, mobidedrm, outputfix
|
||||
from calibre_plugins.k4mobidedrm import k4mobidedrm
|
||||
|
||||
if sys.stdout.encoding == None:
|
||||
sys.stdout = outputfix.getwriter('utf-8')(sys.stdout)
|
||||
else:
|
||||
sys.stdout = outputfix.getwriter(sys.stdout.encoding)(sys.stdout)
|
||||
if sys.stderr.encoding == None:
|
||||
sys.stderr = outputfix.getwriter('utf-8')(sys.stderr)
|
||||
else:
|
||||
sys.stderr = outputfix.getwriter(sys.stderr.encoding)(sys.stderr)
|
||||
|
||||
plug_ver = '.'.join(str(self.version).strip('()').replace(' ', '').split(','))
|
||||
k4 = True
|
||||
pids = []
|
||||
serials = []
|
||||
kInfoFiles = []
|
||||
starttime = time.time()
|
||||
print "K4MobiDeDRM plugin v{0:s}: Starting".format(plug_ver)
|
||||
|
||||
self.config()
|
||||
|
||||
|
@ -87,7 +120,7 @@ class K4DeDRM(FileTypePlugin):
|
|||
pids.append(pid)
|
||||
else:
|
||||
if len(pid) > 0:
|
||||
print "'%s' is not a valid Mobipocket PID." % pid
|
||||
print u"{0} v{1}: \'{2}\' is not a valid Mobipocket PID.".format(PLUGIN_NAME, PLUGIN_VERSION, pid)
|
||||
|
||||
# For linux, get PIDs by calling the right routines under WINE
|
||||
if sys.platform.startswith('linux'):
|
||||
|
@ -98,15 +131,15 @@ class K4DeDRM(FileTypePlugin):
|
|||
serialstringlistt = self.serials_string.split(',')
|
||||
for serial in serialstringlistt:
|
||||
serial = str(serial).replace(" ","")
|
||||
if len(serial) == 16 and serial[0] == 'B':
|
||||
if len(serial) == 16 and serial[0] in ['B','9']:
|
||||
serials.append(serial)
|
||||
else:
|
||||
if len(serial) > 0:
|
||||
print "'%s' is not a valid Kindle serial number." % serial
|
||||
print u"{0} v{1}: \'{2}\' is not a valid eInk Kindle serial number.".format(PLUGIN_NAME, PLUGIN_VERSION, serial)
|
||||
|
||||
# Load any kindle info files (*.info) included Calibre's config directory.
|
||||
try:
|
||||
print 'K4MobiDeDRM v%s: Calibre configuration directory = %s' % (plug_ver, config_dir)
|
||||
print u"{0} v{1}: Calibre configuration directory is {2}".format(PLUGIN_NAME, PLUGIN_VERSION, config_dir)
|
||||
files = os.listdir(config_dir)
|
||||
filefilter = re.compile("\.info$|\.kinf$", re.IGNORECASE)
|
||||
files = filter(filefilter.search, files)
|
||||
|
@ -114,67 +147,29 @@ class K4DeDRM(FileTypePlugin):
|
|||
for filename in files:
|
||||
fpath = os.path.join(config_dir, filename)
|
||||
kInfoFiles.append(fpath)
|
||||
print 'K4MobiDeDRM v%s: Kindle info/kinf file %s found in config folder.' % (plug_ver, filename)
|
||||
except IOError:
|
||||
print 'K4MobiDeDRM v%s: Error reading kindle info/kinf files from config directory.' % plug_ver
|
||||
print u"{0} v{1}: Kindle info/kinf file {2} found in config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, filename)
|
||||
except IOError, e:
|
||||
print u"{0} v{1}: Error \'{2}\' reading kindle info/kinf files from config directory.".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0])
|
||||
pass
|
||||
|
||||
mobi = True
|
||||
magic3 = file(path_to_ebook,'rb').read(3)
|
||||
if magic3 == 'TPZ':
|
||||
mobi = False
|
||||
|
||||
bookname = os.path.splitext(os.path.basename(path_to_ebook))[0]
|
||||
|
||||
if mobi:
|
||||
mb = mobidedrm.MobiBook(path_to_ebook)
|
||||
else:
|
||||
mb = topazextract.TopazBook(path_to_ebook)
|
||||
|
||||
title = mb.getBookTitle()
|
||||
md1, md2 = mb.getPIDMetaInfo()
|
||||
pids.extend(kgenpids.getPidList(md1, md2, k4, serials, kInfoFiles))
|
||||
print "K4MobiDeDRM plugin v{2:s}: Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(pids),plug_ver)
|
||||
|
||||
try:
|
||||
mb.processBook(pids)
|
||||
|
||||
except mobidedrm.DrmException, e:
|
||||
book = k4mobidedrm.GetDecryptedBook(path_to_ebook,kInfoFiles,serials,pids,starttime)
|
||||
except Exception, e:
|
||||
#if you reached here then no luck raise and exception
|
||||
if is_ok_to_use_qt():
|
||||
from PyQt4.Qt import QMessageBox
|
||||
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM v%s Plugin" % plug_ver, "Error: " + str(e) + "... %s\n" % path_to_ebook)
|
||||
d = QMessageBox(QMessageBox.Warning, u"{0} v{1}".format(PLUGIN_NAME, PLUGIN_VERSION), u"Error after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime))
|
||||
d.show()
|
||||
d.raise_()
|
||||
d.exec_()
|
||||
raise Exception("K4MobiDeDRM plugin v{1:s} Error: {2:s} after {0:.1f} seconds".format(time.time()-starttime,plug_ver,str(e)))
|
||||
except topazextract.TpzDRMError, e:
|
||||
#if you reached here then no luck raise and exception
|
||||
if is_ok_to_use_qt():
|
||||
from PyQt4.Qt import QMessageBox
|
||||
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM v%s Plugin" % plug_ver, "Error: " + str(e) + "... %s\n" % path_to_ebook)
|
||||
d.show()
|
||||
d.raise_()
|
||||
d.exec_()
|
||||
raise Exception("K4MobiDeDRM plugin v{1:s} Error: {2:s} after {0:.1f} seconds".format(time.time()-starttime,plug_ver,str(e)))
|
||||
raise Exception(u"{0} v{1}: Error after {3:.1f} seconds: {2}".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0],time.time()-starttime))
|
||||
|
||||
print "K4MobiDeDRM plugin v{1:s}: Successfully decrypted book after {0:.1f} seconds".format(time.time()-starttime,plug_ver)
|
||||
if mobi:
|
||||
if mb.getPrintReplica():
|
||||
of = self.temporary_file(bookname+'.azw4')
|
||||
print 'K4MobiDeDRM plugin v%s: Print Replica format detected.' % plug_ver
|
||||
elif mb.getMobiVersion() >= 8:
|
||||
print 'K4MobiDeDRM plugin v%s: Stand-alone KF8 format detected.' % plug_ver
|
||||
of = self.temporary_file(bookname+'.azw3')
|
||||
else:
|
||||
of = self.temporary_file(bookname+'.mobi')
|
||||
mb.getMobiFile(of.name)
|
||||
print "K4MobiDeDRM plugin v{1:s}: Saved decrypted book after {0:.1f} seconds".format(time.time()-starttime,plug_ver)
|
||||
else:
|
||||
of = self.temporary_file(bookname+'.htmlz')
|
||||
mb.getHTMLZip(of.name)
|
||||
mb.cleanup()
|
||||
print "K4MobiDeDRM plugin v{1:s}: Saved decrypted Topaz HTMLZ after {0:.1f} seconds".format(time.time()-starttime,plug_ver)
|
||||
|
||||
print u"{0} v{1}: Successfully decrypted book after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-starttime)
|
||||
|
||||
of = self.temporary_file(k4mobidedrm.cleanup_name(k4mobidedrm.unescape(book.getBookTitle()))+book.getBookExtension())
|
||||
book.getFile(of.name)
|
||||
book.cleanup()
|
||||
return of.name
|
||||
|
||||
def WINEgetPIDs(self, infile):
|
||||
|
@ -185,40 +180,36 @@ class K4DeDRM(FileTypePlugin):
|
|||
import subasyncio
|
||||
from subasyncio import Process
|
||||
|
||||
print " Getting PIDs from WINE"
|
||||
print u" Getting PIDs from Wine"
|
||||
|
||||
outfile = os.path.join(self.alfdir + 'winepids.txt')
|
||||
outfile = os.path.join(self.alfdir + u"winepids.txt")
|
||||
# Remove any previous winepids.txt file.
|
||||
if os.path.exists(outfile):
|
||||
os.remove(outfile)
|
||||
|
||||
cmdline = 'wine python.exe ' \
|
||||
+ '"'+self.alfdir + '/getk4pcpids.py"' \
|
||||
+ ' "' + infile + '"' \
|
||||
+ ' "' + outfile + '"'
|
||||
|
||||
cmdline = u"wine python.exe \"{0}/getk4pcpids.py\" \"{1}\" \"{2}\"".format(self.alfdir,infile,outfile)
|
||||
env = os.environ
|
||||
|
||||
print "My wine_prefix from tweaks is ", self.wine_prefix
|
||||
print u"wine_prefix from tweaks is \'{0}\'".format(self.wine_prefix)
|
||||
|
||||
if ("WINEPREFIX" in env):
|
||||
print "Using WINEPREFIX from the environment: ", env["WINEPREFIX"]
|
||||
print u"Using WINEPREFIX from the environment instead: \'{0}\'".format(env["WINEPREFIX"])
|
||||
elif (self.wine_prefix is not None):
|
||||
env['WINEPREFIX'] = self.wine_prefix
|
||||
print "Using WINEPREFIX from tweaks: ", self.wine_prefix
|
||||
env["WINEPREFIX"] = self.wine_prefix
|
||||
print u"Using WINEPREFIX from tweaks \'{0}\'".format(self.wine_prefix)
|
||||
else:
|
||||
print "No wine prefix used"
|
||||
print u"No wine prefix used."
|
||||
|
||||
print cmdline
|
||||
print u"Trying command: {0}".format(cmdline)
|
||||
|
||||
try:
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p2 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=sys.stdout, stderr=STDOUT, close_fds=False)
|
||||
result = p2.wait("wait")
|
||||
except Exception, e:
|
||||
print "WINE subprocess error ", str(e)
|
||||
print u"WINE subprocess error: {0}".format(e.args[0])
|
||||
return []
|
||||
print "WINE subprocess returned ", result
|
||||
print u"WINE subprocess returned {0}".format(result)
|
||||
|
||||
WINEpids = []
|
||||
if os.path.exists(outfile):
|
||||
|
@ -229,13 +220,14 @@ class K4DeDRM(FileTypePlugin):
|
|||
customvalue = customvalue.strip()
|
||||
if len(customvalue) == 10 or len(customvalue) == 8:
|
||||
WINEpids.append(customvalue)
|
||||
print u"Found PID '{0}'".format(customvalue)
|
||||
else:
|
||||
print "'%s' is not a valid PID." % customvalue
|
||||
print u"'{0}' is not a valid PID.".format(customvalue)
|
||||
except Exception, e:
|
||||
print "Error parsing winepids.txt: ", str(e)
|
||||
print u"Error parsing winepids.txt: {0}".format(e.args[0])
|
||||
return []
|
||||
else:
|
||||
print "No PIDs generated by Wine Python subprocess."
|
||||
if len(WINEpids) == 0:
|
||||
print u"No PIDs generated by Wine Python subprocess."
|
||||
return WINEpids
|
||||
|
||||
def is_customizable(self):
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
#! /usr/bin/env python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# crypto library mainly by some_updates
|
||||
|
||||
# pbkdf2.py pbkdf2 code taken from pbkdf2.py
|
||||
# pbkdf2.py Copyright © 2004 Matt Johnston <matt @ ucc asn au>
|
||||
# pbkdf2.py Copyright © 2009 Daniel Holth <dholth@fastmail.fm>
|
||||
# pbkdf2.py This code may be freely used and modified for any purpose.
|
||||
|
||||
import sys, os
|
||||
import hmac
|
||||
from struct import pack
|
||||
import hashlib
|
||||
|
||||
|
||||
# interface to needed routines libalfcrypto
|
||||
def _load_libalfcrypto():
|
||||
import ctypes
|
||||
|
@ -26,8 +33,8 @@ def _load_libalfcrypto():
|
|||
name_of_lib = 'libalfcrypto32.so'
|
||||
else:
|
||||
name_of_lib = 'libalfcrypto64.so'
|
||||
|
||||
libalfcrypto = sys.path[0] + os.sep + name_of_lib
|
||||
|
||||
libalfcrypto = os.path.join(sys.path[0],name_of_lib)
|
||||
|
||||
if not os.path.isfile(libalfcrypto):
|
||||
raise Exception('libalfcrypto not found')
|
||||
|
@ -55,7 +62,7 @@ def _load_libalfcrypto():
|
|||
#
|
||||
# int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key);
|
||||
#
|
||||
#
|
||||
#
|
||||
# void AES_cbc_encrypt(const unsigned char *in, unsigned char *out,
|
||||
# const unsigned long length, const AES_KEY *key,
|
||||
# unsigned char *ivec, const int enc);
|
||||
|
@ -147,7 +154,7 @@ def _load_libalfcrypto():
|
|||
topazCryptoDecrypt(ctx, data, out, len(data))
|
||||
return out.raw
|
||||
|
||||
print "Using Library AlfCrypto DLL/DYLIB/SO"
|
||||
print u"Using Library AlfCrypto DLL/DYLIB/SO"
|
||||
return (AES_CBC, Pukall_Cipher, Topaz_Cipher)
|
||||
|
||||
|
||||
|
@ -164,8 +171,7 @@ def _load_python_alfcrypto():
|
|||
sum2 = 0;
|
||||
keyXorVal = 0;
|
||||
if len(key)!=16:
|
||||
print "Bad key length!"
|
||||
return None
|
||||
raise Exception('Pukall_Cipher: Bad key length.')
|
||||
wkey = []
|
||||
for i in xrange(8):
|
||||
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
|
||||
|
@ -234,6 +240,7 @@ def _load_python_alfcrypto():
|
|||
cleartext = self.aes.decrypt(iv + data)
|
||||
return cleartext
|
||||
|
||||
print u"Using Library AlfCrypto Python"
|
||||
return (AES_CBC, Pukall_Cipher, Topaz_Cipher)
|
||||
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from PyQt4.Qt import QWidget, QVBoxLayout, QLabel, QLineEdit
|
||||
|
||||
from calibre.utils.config import JSONConfig
|
||||
|
|
|
@ -230,6 +230,7 @@ class PageParser(object):
|
|||
'empty' : (1, 'snippets', 1, 0),
|
||||
|
||||
'page' : (1, 'snippets', 1, 0),
|
||||
'page.class' : (1, 'scalar_text', 0, 0),
|
||||
'page.pageid' : (1, 'scalar_text', 0, 0),
|
||||
'page.pagelabel' : (1, 'scalar_text', 0, 0),
|
||||
'page.type' : (1, 'scalar_text', 0, 0),
|
||||
|
@ -238,11 +239,13 @@ class PageParser(object):
|
|||
'page.startID' : (1, 'scalar_number', 0, 0),
|
||||
|
||||
'group' : (1, 'snippets', 1, 0),
|
||||
'group.class' : (1, 'scalar_text', 0, 0),
|
||||
'group.type' : (1, 'scalar_text', 0, 0),
|
||||
'group._tag' : (1, 'scalar_text', 0, 0),
|
||||
'group.orientation': (1, 'scalar_text', 0, 0),
|
||||
|
||||
'region' : (1, 'snippets', 1, 0),
|
||||
'region.class' : (1, 'scalar_text', 0, 0),
|
||||
'region.type' : (1, 'scalar_text', 0, 0),
|
||||
'region.x' : (1, 'scalar_number', 0, 0),
|
||||
'region.y' : (1, 'scalar_number', 0, 0),
|
||||
|
|
302
Calibre_Plugins/K4MobiDeDRM_plugin/k4mobidedrm.py
Normal file
302
Calibre_Plugins/K4MobiDeDRM_plugin/k4mobidedrm.py
Normal file
|
@ -0,0 +1,302 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ignobleepub.pyw, version 3.6
|
||||
# Copyright © 2009-2012 by DiapDealer et al.
|
||||
|
||||
# engine to remove drm from Kindle for Mac and Kindle for PC books
|
||||
# for personal use for archiving and converting your ebooks
|
||||
|
||||
# PLEASE DO NOT PIRATE EBOOKS!
|
||||
|
||||
# We want all authors and publishers, and eBook stores to live
|
||||
# long and prosperous lives but at the same time we just want to
|
||||
# be able to read OUR books on whatever device we want and to keep
|
||||
# readable for a long, long time
|
||||
|
||||
# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
|
||||
# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates
|
||||
# and many many others
|
||||
# Special thanks to The Dark Reverser for MobiDeDrm and CMBDTC for cmbdtc_dump
|
||||
# from which this script borrows most unashamedly.
|
||||
|
||||
|
||||
# Changelog
|
||||
# 1.0 - Name change to k4mobidedrm. Adds Mac support, Adds plugin code
|
||||
# 1.1 - Adds support for additional kindle.info files
|
||||
# 1.2 - Better error handling for older Mobipocket
|
||||
# 1.3 - Don't try to decrypt Topaz books
|
||||
# 1.7 - Add support for Topaz books and Kindle serial numbers. Split code.
|
||||
# 1.9 - Tidy up after Topaz, minor exception changes
|
||||
# 2.1 - Topaz fix and filename sanitizing
|
||||
# 2.2 - Topaz Fix and minor Mac code fix
|
||||
# 2.3 - More Topaz fixes
|
||||
# 2.4 - K4PC/Mac key generation fix
|
||||
# 2.6 - Better handling of non-K4PC/Mac ebooks
|
||||
# 2.7 - Better trailing bytes handling in mobidedrm
|
||||
# 2.8 - Moved parsing of kindle.info files to mac & pc util files.
|
||||
# 3.1 - Updated for new calibre interface. Now __init__ in plugin.
|
||||
# 3.5 - Now support Kindle for PC/Mac 1.6
|
||||
# 3.6 - Even better trailing bytes handling in mobidedrm
|
||||
# 3.7 - Add support for Amazon Print Replica ebooks.
|
||||
# 3.8 - Improved Topaz support
|
||||
# 4.1 - Improved Topaz support and faster decryption with alfcrypto
|
||||
# 4.2 - Added support for Amazon's KF8 format ebooks
|
||||
# 4.4 - Linux calls to Wine added, and improved configuration dialog
|
||||
# 4.5 - Linux works again without Wine. Some Mac key file search changes
|
||||
# 4.6 - First attempt to handle unicode properly
|
||||
# 4.7 - Added timing reports, and changed search for Mac key files
|
||||
# 4.8 - Much better unicode handling, matching the updated inept and ignoble scripts
|
||||
# - Moved back into plugin, __init__ in plugin now only contains plugin code.
|
||||
|
||||
__version__ = '4.8'
|
||||
|
||||
|
||||
import sys, os, re
|
||||
import csv
|
||||
import getopt
|
||||
import re
|
||||
import traceback
|
||||
import time
|
||||
import htmlentitydefs
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
if inCalibre:
|
||||
from calibre_plugins.k4mobidedrm import mobidedrm
|
||||
from calibre_plugins.k4mobidedrm import topazextract
|
||||
from calibre_plugins.k4mobidedrm import kgenpids
|
||||
else:
|
||||
import mobidedrm
|
||||
import topazextract
|
||||
import kgenpids
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return [u"mobidedrm.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
# cleanup unicode filenames
|
||||
# borrowed from calibre from calibre/src/calibre/__init__.py
|
||||
# added in removal of control (<32) chars
|
||||
# and removal of . at start and end
|
||||
# and with some (heavily edited) code from Paul Durrant's kindlenamer.py
|
||||
def cleanup_name(name):
|
||||
# substitute filename unfriendly characters
|
||||
name = name.replace(u"<",u"[").replace(u">",u"]").replace(u" : ",u" – ").replace(u": ",u" – ").replace(u":",u"—").replace(u"/",u"_").replace(u"\\",u"_").replace(u"|",u"_").replace(u"\"",u"\'")
|
||||
# delete control characters
|
||||
name = u"".join(char for char in name if ord(char)>=32)
|
||||
# white space to single space, delete leading and trailing while space
|
||||
name = re.sub(ur"\s", u" ", name).strip()
|
||||
# remove leading dots
|
||||
while len(name)>0 and name[0] == u".":
|
||||
name = name[1:]
|
||||
# remove trailing dots (Windows doesn't like them)
|
||||
if name.endswith(u'.'):
|
||||
name = name[:-1]
|
||||
return name
|
||||
|
||||
# must be passed unicode
|
||||
def unescape(text):
|
||||
def fixup(m):
|
||||
text = m.group(0)
|
||||
if text[:2] == u"&#":
|
||||
# character reference
|
||||
try:
|
||||
if text[:3] == u"&#x":
|
||||
return unichr(int(text[3:-1], 16))
|
||||
else:
|
||||
return unichr(int(text[2:-1]))
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
# named entity
|
||||
try:
|
||||
text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
|
||||
except KeyError:
|
||||
pass
|
||||
return text # leave as is
|
||||
return re.sub(u"&#?\w+;", fixup, text)
|
||||
|
||||
def GetDecryptedBook(infile, kInfoFiles, serials, pids, starttime = time.time()):
|
||||
# handle the obvious cases at the beginning
|
||||
if not os.path.isfile(infile):
|
||||
raise DRMException (u"Input file does not exist.")
|
||||
|
||||
mobi = True
|
||||
magic3 = file(infile,'rb').read(3)
|
||||
if magic3 == 'TPZ':
|
||||
mobi = False
|
||||
|
||||
if mobi:
|
||||
mb = mobidedrm.MobiBook(infile)
|
||||
else:
|
||||
mb = topazextract.TopazBook(infile)
|
||||
|
||||
bookname = unescape(mb.getBookTitle())
|
||||
print u"Decrypting {1} ebook: {0}".format(bookname, mb.getBookType())
|
||||
|
||||
# extend PID list with book-specific PIDs
|
||||
md1, md2 = mb.getPIDMetaInfo()
|
||||
pids.extend(kgenpids.getPidList(md1, md2, serials, kInfoFiles))
|
||||
print u"Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(pids))
|
||||
|
||||
try:
|
||||
mb.processBook(pids)
|
||||
except:
|
||||
mb.cleanup
|
||||
raise
|
||||
|
||||
print u"Decryption succeeded after {0:.1f} seconds".format(time.time()-starttime)
|
||||
return mb
|
||||
|
||||
|
||||
# infile, outdir and kInfoFiles should be unicode strings
|
||||
def decryptBook(infile, outdir, kInfoFiles, serials, pids):
|
||||
starttime = time.time()
|
||||
print "Starting decryptBook routine."
|
||||
try:
|
||||
book = GetDecryptedBook(infile, kInfoFiles, serials, pids, starttime)
|
||||
except Exception, e:
|
||||
print u"Error decrypting book after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime)
|
||||
return 1
|
||||
|
||||
# if we're saving to the same folder as the original, use file name_
|
||||
# if to a different folder, use book name
|
||||
if os.path.normcase(os.path.normpath(outdir)) == os.path.normcase(os.path.normpath(os.path.dirname(infile))):
|
||||
outfilename = os.path.splitext(os.path.basename(infile))[0]
|
||||
else:
|
||||
outfilename = cleanup_name(book.getBookTitle())
|
||||
|
||||
# avoid excessively long file names
|
||||
if len(outfilename)>150:
|
||||
outfilename = outfilename[:150]
|
||||
|
||||
outfilename = outfilename+u"_nodrm"
|
||||
outfile = os.path.join(outdir, outfilename + book.getBookExtension())
|
||||
|
||||
book.getFile(outfile)
|
||||
print u"Saved decrypted book {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename)
|
||||
|
||||
if book.getBookType()==u"Topaz":
|
||||
zipname = os.path.join(outdir, outfilename + u"_SVG.zip")
|
||||
book.getSVGZip(zipname)
|
||||
print u"Saved SVG ZIP Archive for {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename)
|
||||
|
||||
# remove internal temporary directory of Topaz pieces
|
||||
book.cleanup()
|
||||
|
||||
|
||||
def usage(progname):
|
||||
print u"Removes DRM protection from Mobipocket, Amazon KF8, Amazon Print Replica and Amazon Topaz ebooks"
|
||||
print u"Usage:"
|
||||
print u" {0} [-k <kindle.info>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] <infile> <outdir>".format(progname)
|
||||
|
||||
#
|
||||
# Main
|
||||
#
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__)
|
||||
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:")
|
||||
except getopt.GetoptError, err:
|
||||
print u"Error in options or arguments: {0}".format(err.args[0])
|
||||
usage(progname)
|
||||
sys.exit(2)
|
||||
if len(args)<2:
|
||||
usage(progname)
|
||||
sys.exit(2)
|
||||
|
||||
infile = args[0]
|
||||
outdir = args[1]
|
||||
kInfoFiles = []
|
||||
serials = []
|
||||
pids = []
|
||||
|
||||
for o, a in opts:
|
||||
if o == "-k":
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -k")
|
||||
kInfoFiles.append(a)
|
||||
if o == "-p":
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -p")
|
||||
pids = a.split(',')
|
||||
if o == "-s":
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -s")
|
||||
serials = a.split(',')
|
||||
|
||||
# try with built in Kindle Info files if not on Linux
|
||||
k4 = not sys.platform.startswith('linux')
|
||||
|
||||
return decryptBook(infile, outdir, kInfoFiles, serials, pids)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
|
@ -1,4 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
import sys
|
||||
|
@ -17,26 +18,24 @@ global charMap4
|
|||
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
if inCalibre:
|
||||
if sys.platform.startswith('win'):
|
||||
from calibre.constants import iswindows, isosx
|
||||
if iswindows:
|
||||
from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||
|
||||
if sys.platform.startswith('darwin'):
|
||||
if isosx:
|
||||
from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||
else:
|
||||
if sys.platform.startswith('win'):
|
||||
inCalibre = False
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
if iswindows:
|
||||
from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||
|
||||
if sys.platform.startswith('darwin'):
|
||||
if isosx:
|
||||
from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||
|
||||
|
||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M'
|
||||
charMap3 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||
charMap4 = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
|
||||
|
||||
# crypto digestroutines
|
||||
import hashlib
|
||||
|
@ -54,7 +53,7 @@ def SHA1(message):
|
|||
|
||||
# Encode the bytes in data with the characters in map
|
||||
def encode(data, map):
|
||||
result = ""
|
||||
result = ''
|
||||
for char in data:
|
||||
value = ord(char)
|
||||
Q = (value ^ 0x80) // len(map)
|
||||
|
@ -69,14 +68,14 @@ def encodeHash(data,map):
|
|||
|
||||
# Decode the string in data with the characters in map. Returns the decoded bytes
|
||||
def decode(data,map):
|
||||
result = ""
|
||||
result = ''
|
||||
for i in range (0,len(data)-1,2):
|
||||
high = map.find(data[i])
|
||||
low = map.find(data[i+1])
|
||||
if (high == -1) or (low == -1) :
|
||||
break
|
||||
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||
result += pack("B",value)
|
||||
result += pack('B',value)
|
||||
return result
|
||||
|
||||
#
|
||||
|
@ -98,7 +97,7 @@ def getSixBitsFromBitField(bitField,offset):
|
|||
# 8 bits to six bits encoding from hash to generate PID string
|
||||
def encodePID(hash):
|
||||
global charMap3
|
||||
PID = ""
|
||||
PID = ''
|
||||
for position in range (0,8):
|
||||
PID += charMap3[getSixBitsFromBitField(hash,position)]
|
||||
return PID
|
||||
|
@ -129,7 +128,7 @@ def generatePidSeed(table,dsn) :
|
|||
def generateDevicePID(table,dsn,nbRoll):
|
||||
global charMap4
|
||||
seed = generatePidSeed(table,dsn)
|
||||
pidAscii = ""
|
||||
pidAscii = ''
|
||||
pid = [(seed >>24) &0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF,(seed>>24) & 0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF]
|
||||
index = 0
|
||||
for counter in range (0,nbRoll):
|
||||
|
@ -176,28 +175,31 @@ def pidFromSerial(s, l):
|
|||
|
||||
|
||||
# Parse the EXTH header records and use the Kindle serial number to calculate the book pid.
|
||||
def getKindlePid(pidlst, rec209, token, serialnum):
|
||||
def getKindlePids(rec209, token, serialnum):
|
||||
pids=[]
|
||||
|
||||
# Compute book PID
|
||||
pidHash = SHA1(serialnum+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
pids.append(bookPID)
|
||||
|
||||
# compute fixed pid for old pre 2.5 firmware update pid as well
|
||||
bookPID = pidFromSerial(serialnum, 7) + "*"
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
kindlePID = pidFromSerial(serialnum, 7) + "*"
|
||||
kindlePID = checksumPid(kindlePID)
|
||||
pids.append(kindlePID)
|
||||
|
||||
return pidlst
|
||||
return pids
|
||||
|
||||
|
||||
# parse the Kindleinfo file to calculate the book pid.
|
||||
|
||||
keynames = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
|
||||
keynames = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber']
|
||||
|
||||
def getK4Pids(pidlst, rec209, token, kInfoFile):
|
||||
def getK4Pids(rec209, token, kInfoFile):
|
||||
global charMap1
|
||||
kindleDatabase = None
|
||||
pids = []
|
||||
try:
|
||||
kindleDatabase = getDBfromFile(kInfoFile)
|
||||
except Exception, message:
|
||||
|
@ -206,17 +208,17 @@ def getK4Pids(pidlst, rec209, token, kInfoFile):
|
|||
pass
|
||||
|
||||
if kindleDatabase == None :
|
||||
return pidlst
|
||||
return pids
|
||||
|
||||
try:
|
||||
# Get the Mazama Random number
|
||||
MazamaRandomNumber = kindleDatabase["MazamaRandomNumber"]
|
||||
MazamaRandomNumber = kindleDatabase['MazamaRandomNumber']
|
||||
|
||||
# Get the kindle account token
|
||||
kindleAccountToken = kindleDatabase["kindle.account.tokens"]
|
||||
kindleAccountToken = kindleDatabase['kindle.account.tokens']
|
||||
except KeyError:
|
||||
print "Keys not found in " + kInfoFile
|
||||
return pidlst
|
||||
print u"Keys not found in {0}".format(os.path.basename(kInfoFile))
|
||||
return pids
|
||||
|
||||
# Get the ID string used
|
||||
encodedIDString = encodeHash(GetIDString(),charMap1)
|
||||
|
@ -231,7 +233,7 @@ def getK4Pids(pidlst, rec209, token, kInfoFile):
|
|||
table = generatePidEncryptionTable()
|
||||
devicePID = generateDevicePID(table,DSN,4)
|
||||
devicePID = checksumPid(devicePID)
|
||||
pidlst.append(devicePID)
|
||||
pids.append(devicePID)
|
||||
|
||||
# Compute book PIDs
|
||||
|
||||
|
@ -239,36 +241,38 @@ def getK4Pids(pidlst, rec209, token, kInfoFile):
|
|||
pidHash = SHA1(DSN+kindleAccountToken+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
pids.append(bookPID)
|
||||
|
||||
# variant 1
|
||||
pidHash = SHA1(kindleAccountToken+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
pids.append(bookPID)
|
||||
|
||||
# variant 2
|
||||
pidHash = SHA1(DSN+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
pids.append(bookPID)
|
||||
|
||||
return pidlst
|
||||
return pids
|
||||
|
||||
def getPidList(md1, md2, k4 = True, serials=[], kInfoFiles=[]):
|
||||
def getPidList(md1, md2, serials=[], kInfoFiles=[]):
|
||||
pidlst = []
|
||||
if kInfoFiles is None:
|
||||
kInfoFiles = []
|
||||
if k4:
|
||||
if serials is None:
|
||||
serials = []
|
||||
if iswindows or isosx:
|
||||
kInfoFiles.extend(getKindleInfoFiles())
|
||||
for infoFile in kInfoFiles:
|
||||
try:
|
||||
pidlst = getK4Pids(pidlst, md1, md2, infoFile)
|
||||
except Exception, message:
|
||||
print("Error getting PIDs from " + infoFile + ": " + message)
|
||||
pidlst.extend(getK4Pids(md1, md2, infoFile))
|
||||
except Exception, e:
|
||||
print u"Error getting PIDs from {0}: {1}".format(os.path.basename(infoFile),e.args[0])
|
||||
for serialnum in serials:
|
||||
try:
|
||||
pidlst = getKindlePid(pidlst, md1, md2, serialnum)
|
||||
pidlst.extend(getKindlePids(md1, md2, serialnum))
|
||||
except Exception, message:
|
||||
print("Error getting PIDs from " + serialnum + ": " + message)
|
||||
print u"Error getting PIDs from serial number {0}: {1}".format(serialnum ,e.args[0])
|
||||
return pidlst
|
||||
|
|
142
Calibre_Plugins/K4MobiDeDRM_plugin/kindlepid.py
Normal file
142
Calibre_Plugins/K4MobiDeDRM_plugin/kindlepid.py
Normal file
|
@ -0,0 +1,142 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Mobipocket PID calculator v0.4 for Amazon Kindle.
|
||||
# Copyright (c) 2007, 2009 Igor Skochinsky <skochinsky@mail.ru>
|
||||
# History:
|
||||
# 0.1 Initial release
|
||||
# 0.2 Added support for generating PID for iPhone (thanks to mbp)
|
||||
# 0.3 changed to autoflush stdout, fixed return code usage
|
||||
# 0.3 updated for unicode
|
||||
|
||||
import sys
|
||||
import binascii
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return [u"mobidedrm.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
if sys.hexversion >= 0x3000000:
|
||||
print 'This script is incompatible with Python 3.x. Please install Python 2.7.x.'
|
||||
sys.exit(2)
|
||||
|
||||
letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
|
||||
|
||||
def crc32(s):
|
||||
return (~binascii.crc32(s,-1))&0xFFFFFFFF
|
||||
|
||||
def checksumPid(s):
|
||||
crc = crc32(s)
|
||||
crc = crc ^ (crc >> 16)
|
||||
res = s
|
||||
l = len(letters)
|
||||
for i in (0,1):
|
||||
b = crc & 0xff
|
||||
pos = (b // l) ^ (b % l)
|
||||
res += letters[pos%l]
|
||||
crc >>= 8
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def pidFromSerial(s, l):
|
||||
crc = crc32(s)
|
||||
|
||||
arr1 = [0]*l
|
||||
for i in xrange(len(s)):
|
||||
arr1[i%l] ^= ord(s[i])
|
||||
|
||||
crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff]
|
||||
for i in xrange(l):
|
||||
arr1[i] ^= crc_bytes[i&3]
|
||||
|
||||
pid = ''
|
||||
for i in xrange(l):
|
||||
b = arr1[i] & 0xff
|
||||
pid+=letters[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))]
|
||||
|
||||
return pid
|
||||
|
||||
def cli_main(argv=unicode_argv()):
|
||||
print u"Mobipocket PID calculator for Amazon Kindle. Copyright © 2007, 2009 Igor Skochinsky"
|
||||
if len(sys.argv)==2:
|
||||
serial = sys.argv[1]
|
||||
else:
|
||||
print u"Usage: kindlepid.py <Kindle Serial Number>/<iPhone/iPod Touch UDID>"
|
||||
return 1
|
||||
if len(serial)==16:
|
||||
if serial.startswith("B"):
|
||||
print u"Kindle serial number detected"
|
||||
else:
|
||||
print u"Warning: unrecognized serial number. Please recheck input."
|
||||
return 1
|
||||
pid = pidFromSerial(serial.encode("utf-8"),7)+'*'
|
||||
print u"Mobipocket PID for Kindle serial#{0} is {1} ".format(serial,checksumPid(pid))
|
||||
return 0
|
||||
elif len(serial)==40:
|
||||
print u"iPhone serial number (UDID) detected"
|
||||
pid = pidFromSerial(serial.encode("utf-8"),8)
|
||||
print u"Mobipocket PID for iPhone serial#{0} is {1} ".format(serial,checksumPid(pid))
|
||||
return 0
|
||||
print u"Warning: unrecognized serial number. Please recheck input."
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
|
@ -1,45 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Adapted and simplified from the kitchen project
|
||||
#
|
||||
# Kitchen Project Copyright (c) 2012 Red Hat, Inc.
|
||||
#
|
||||
# kitchen is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# kitchen is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with kitchen; if not, see <http://www.gnu.org/licenses/>
|
||||
#
|
||||
# Authors:
|
||||
# Toshio Kuratomi <toshio@fedoraproject.org>
|
||||
# Seth Vidal
|
||||
#
|
||||
# Portions of code taken from yum/i18n.py and
|
||||
# python-fedora: fedora/textutils.py
|
||||
|
||||
import codecs
|
||||
|
||||
# returns a char string unchanged
|
||||
# returns a unicode string converted to a char string of the passed encoding
|
||||
# return the empty string for anything else
|
||||
def getwriter(encoding):
|
||||
class _StreamWriter(codecs.StreamWriter):
|
||||
def __init__(self, stream):
|
||||
codecs.StreamWriter.__init__(self, stream, 'replace')
|
||||
|
||||
def encode(self, msg, errors='replace'):
|
||||
if isinstance(msg, basestring):
|
||||
if isinstance(msg, str):
|
||||
return (msg, len(msg))
|
||||
return (msg.encode(self.encoding, 'replace'), len(msg))
|
||||
return ('',0)
|
||||
|
||||
_StreamWriter.encoding = encoding
|
||||
return _StreamWriter
|
|
@ -1,68 +0,0 @@
|
|||
# A simple implementation of pbkdf2 using stock python modules. See RFC2898
|
||||
# for details. Basically, it derives a key from a password and salt.
|
||||
|
||||
# Copyright 2004 Matt Johnston <matt @ ucc asn au>
|
||||
# Copyright 2009 Daniel Holth <dholth@fastmail.fm>
|
||||
# This code may be freely used and modified for any purpose.
|
||||
|
||||
# Revision history
|
||||
# v0.1 October 2004 - Initial release
|
||||
# v0.2 8 March 2007 - Make usable with hashlib in Python 2.5 and use
|
||||
# v0.3 "" the correct digest_size rather than always 20
|
||||
# v0.4 Oct 2009 - Rescue from chandler svn, test and optimize.
|
||||
|
||||
import sys
|
||||
import hmac
|
||||
from struct import pack
|
||||
try:
|
||||
# only in python 2.5
|
||||
import hashlib
|
||||
sha = hashlib.sha1
|
||||
md5 = hashlib.md5
|
||||
sha256 = hashlib.sha256
|
||||
except ImportError: # pragma: NO COVERAGE
|
||||
# fallback
|
||||
import sha
|
||||
import md5
|
||||
|
||||
# this is what you want to call.
|
||||
def pbkdf2( password, salt, itercount, keylen, hashfn = sha ):
|
||||
try:
|
||||
# depending whether the hashfn is from hashlib or sha/md5
|
||||
digest_size = hashfn().digest_size
|
||||
except TypeError: # pragma: NO COVERAGE
|
||||
digest_size = hashfn.digest_size
|
||||
# l - number of output blocks to produce
|
||||
l = keylen / digest_size
|
||||
if keylen % digest_size != 0:
|
||||
l += 1
|
||||
|
||||
h = hmac.new( password, None, hashfn )
|
||||
|
||||
T = ""
|
||||
for i in range(1, l+1):
|
||||
T += pbkdf2_F( h, salt, itercount, i )
|
||||
|
||||
return T[0: keylen]
|
||||
|
||||
def xorstr( a, b ):
|
||||
if len(a) != len(b):
|
||||
raise ValueError("xorstr(): lengths differ")
|
||||
return ''.join((chr(ord(x)^ord(y)) for x, y in zip(a, b)))
|
||||
|
||||
def prf( h, data ):
|
||||
hm = h.copy()
|
||||
hm.update( data )
|
||||
return hm.digest()
|
||||
|
||||
# Helper as per the spec. h is a hmac which has been created seeded with the
|
||||
# password, it will be copy()ed and not modified.
|
||||
def pbkdf2_F( h, salt, itercount, blocknum ):
|
||||
U = prf( h, salt + pack('>i',blocknum ) )
|
||||
T = U
|
||||
|
||||
for i in range(2, itercount+1):
|
||||
U = prf( h, U )
|
||||
T = xorstr( T, U )
|
||||
|
||||
return T
|
|
@ -1,43 +1,90 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
class Unbuffered:
|
||||
# topazextract.py, version ?
|
||||
# Mostly written by some_updates based on code from many others
|
||||
|
||||
__version__ = '4.8'
|
||||
|
||||
import sys
|
||||
import os, csv, getopt
|
||||
import zlib, zipfile, tempfile, shutil
|
||||
import traceback
|
||||
from struct import pack
|
||||
from struct import unpack
|
||||
from alfcrypto import Topaz_Cipher
|
||||
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return [u"mobidedrm.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = 'utf-8'
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
buildXML = False
|
||||
|
||||
import os, csv, getopt
|
||||
import zlib, zipfile, tempfile, shutil
|
||||
from struct import pack
|
||||
from struct import unpack
|
||||
from alfcrypto import Topaz_Cipher
|
||||
|
||||
class TpzDRMError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# local support routines
|
||||
if inCalibre:
|
||||
from calibre_plugins.k4mobidedrm import kgenpids
|
||||
else:
|
||||
inCalibre = False
|
||||
import kgenpids
|
||||
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# recursive zip creation support routine
|
||||
def zipUpDir(myzip, tdir, localname):
|
||||
currentdir = tdir
|
||||
if localname != "":
|
||||
if localname != u"":
|
||||
currentdir = os.path.join(currentdir,localname)
|
||||
list = os.listdir(currentdir)
|
||||
for file in list:
|
||||
|
@ -73,7 +120,7 @@ def bookReadEncodedNumber(fo):
|
|||
# Get a length prefixed string from file
|
||||
def bookReadString(fo):
|
||||
stringLength = bookReadEncodedNumber(fo)
|
||||
return unpack(str(stringLength)+"s",fo.read(stringLength))[0]
|
||||
return unpack(str(stringLength)+'s',fo.read(stringLength))[0]
|
||||
|
||||
#
|
||||
# crypto routines
|
||||
|
@ -112,13 +159,13 @@ def decryptRecord(data,PID):
|
|||
# Try to decrypt a dkey record (contains the bookPID)
|
||||
def decryptDkeyRecord(data,PID):
|
||||
record = decryptRecord(data,PID)
|
||||
fields = unpack("3sB8sB8s3s",record)
|
||||
if fields[0] != "PID" or fields[5] != "pid" :
|
||||
raise TpzDRMError("Didn't find PID magic numbers in record")
|
||||
fields = unpack('3sB8sB8s3s',record)
|
||||
if fields[0] != 'PID' or fields[5] != 'pid' :
|
||||
raise DrmException(u"Didn't find PID magic numbers in record")
|
||||
elif fields[1] != 8 or fields[3] != 8 :
|
||||
raise TpzDRMError("Record didn't contain correct length fields")
|
||||
raise DrmException(u"Record didn't contain correct length fields")
|
||||
elif fields[2] != PID :
|
||||
raise TpzDRMError("Record didn't contain PID")
|
||||
raise DrmException(u"Record didn't contain PID")
|
||||
return fields[4]
|
||||
|
||||
# Decrypt all dkey records (contain the book PID)
|
||||
|
@ -131,11 +178,11 @@ def decryptDkeyRecords(data,PID):
|
|||
try:
|
||||
key = decryptDkeyRecord(data[1:length+1],PID)
|
||||
records.append(key)
|
||||
except TpzDRMError:
|
||||
except DrmException:
|
||||
pass
|
||||
data = data[1+length:]
|
||||
if len(records) == 0:
|
||||
raise TpzDRMError("BookKey Not Found")
|
||||
raise DrmException(u"BookKey Not Found")
|
||||
return records
|
||||
|
||||
|
||||
|
@ -148,9 +195,9 @@ class TopazBook:
|
|||
self.bookHeaderRecords = {}
|
||||
self.bookMetadata = {}
|
||||
self.bookKey = None
|
||||
magic = unpack("4s",self.fo.read(4))[0]
|
||||
magic = unpack('4s',self.fo.read(4))[0]
|
||||
if magic != 'TPZ0':
|
||||
raise TpzDRMError("Parse Error : Invalid Header, not a Topaz file")
|
||||
raise DrmException(u"Parse Error : Invalid Header, not a Topaz file")
|
||||
self.parseTopazHeaders()
|
||||
self.parseMetadata()
|
||||
|
||||
|
@ -167,7 +214,7 @@ class TopazBook:
|
|||
# Read and parse one header record at the current book file position and return the associated data
|
||||
# [[offset,decompressedLength,compressedLength],...]
|
||||
if ord(self.fo.read(1)) != 0x63:
|
||||
raise TpzDRMError("Parse Error : Invalid Header")
|
||||
raise DrmException(u"Parse Error : Invalid Header")
|
||||
tag = bookReadString(self.fo)
|
||||
record = bookReadHeaderRecordData()
|
||||
return [tag,record]
|
||||
|
@ -177,15 +224,15 @@ class TopazBook:
|
|||
# print result[0], result[1]
|
||||
self.bookHeaderRecords[result[0]] = result[1]
|
||||
if ord(self.fo.read(1)) != 0x64 :
|
||||
raise TpzDRMError("Parse Error : Invalid Header")
|
||||
raise DrmException(u"Parse Error : Invalid Header")
|
||||
self.bookPayloadOffset = self.fo.tell()
|
||||
|
||||
def parseMetadata(self):
|
||||
# Parse the metadata record from the book payload and return a list of [key,values]
|
||||
self.fo.seek(self.bookPayloadOffset + self.bookHeaderRecords["metadata"][0][0])
|
||||
self.fo.seek(self.bookPayloadOffset + self.bookHeaderRecords['metadata'][0][0])
|
||||
tag = bookReadString(self.fo)
|
||||
if tag != "metadata" :
|
||||
raise TpzDRMError("Parse Error : Record Names Don't Match")
|
||||
if tag != 'metadata' :
|
||||
raise DrmException(u"Parse Error : Record Names Don't Match")
|
||||
flags = ord(self.fo.read(1))
|
||||
nbRecords = ord(self.fo.read(1))
|
||||
# print nbRecords
|
||||
|
@ -210,7 +257,7 @@ class TopazBook:
|
|||
title = ''
|
||||
if 'Title' in self.bookMetadata:
|
||||
title = self.bookMetadata['Title']
|
||||
return title
|
||||
return title.decode('utf-8')
|
||||
|
||||
def setBookKey(self, key):
|
||||
self.bookKey = key
|
||||
|
@ -223,13 +270,13 @@ class TopazBook:
|
|||
try:
|
||||
recordOffset = self.bookHeaderRecords[name][index][0]
|
||||
except:
|
||||
raise TpzDRMError("Parse Error : Invalid Record, record not found")
|
||||
raise DrmException("Parse Error : Invalid Record, record not found")
|
||||
|
||||
self.fo.seek(self.bookPayloadOffset + recordOffset)
|
||||
|
||||
tag = bookReadString(self.fo)
|
||||
if tag != name :
|
||||
raise TpzDRMError("Parse Error : Invalid Record, record name doesn't match")
|
||||
raise DrmException("Parse Error : Invalid Record, record name doesn't match")
|
||||
|
||||
recordIndex = bookReadEncodedNumber(self.fo)
|
||||
if recordIndex < 0 :
|
||||
|
@ -237,7 +284,7 @@ class TopazBook:
|
|||
recordIndex = -recordIndex -1
|
||||
|
||||
if recordIndex != index :
|
||||
raise TpzDRMError("Parse Error : Invalid Record, index doesn't match")
|
||||
raise DrmException("Parse Error : Invalid Record, index doesn't match")
|
||||
|
||||
if (self.bookHeaderRecords[name][index][2] > 0):
|
||||
compressed = True
|
||||
|
@ -250,7 +297,7 @@ class TopazBook:
|
|||
ctx = topazCryptoInit(self.bookKey)
|
||||
record = topazCryptoDecrypt(record,ctx)
|
||||
else :
|
||||
raise TpzDRMError("Error: Attempt to decrypt without bookKey")
|
||||
raise DrmException("Error: Attempt to decrypt without bookKey")
|
||||
|
||||
if compressed:
|
||||
record = zlib.decompress(record)
|
||||
|
@ -262,12 +309,12 @@ class TopazBook:
|
|||
fixedimage=True
|
||||
try:
|
||||
keydata = self.getBookPayloadRecord('dkey', 0)
|
||||
except TpzDRMError, e:
|
||||
print "no dkey record found, book may not be encrypted"
|
||||
print "attempting to extrct files without a book key"
|
||||
except DrmException, e:
|
||||
print u"no dkey record found, book may not be encrypted"
|
||||
print u"attempting to extrct files without a book key"
|
||||
self.createBookDirectory()
|
||||
self.extractFiles()
|
||||
print "Successfully Extracted Topaz contents"
|
||||
print u"Successfully Extracted Topaz contents"
|
||||
if inCalibre:
|
||||
from calibre_plugins.k4mobidedrm import genbook
|
||||
else:
|
||||
|
@ -275,7 +322,7 @@ class TopazBook:
|
|||
|
||||
rv = genbook.generateBook(self.outdir, raw, fixedimage)
|
||||
if rv == 0:
|
||||
print "\nBook Successfully generated"
|
||||
print u"Book Successfully generated."
|
||||
return rv
|
||||
|
||||
# try each pid to decode the file
|
||||
|
@ -283,25 +330,25 @@ class TopazBook:
|
|||
for pid in pidlst:
|
||||
# use 8 digit pids here
|
||||
pid = pid[0:8]
|
||||
print "\nTrying: ", pid
|
||||
print u"Trying: {0}".format(pid)
|
||||
bookKeys = []
|
||||
data = keydata
|
||||
try:
|
||||
bookKeys+=decryptDkeyRecords(data,pid)
|
||||
except TpzDRMError, e:
|
||||
except DrmException, e:
|
||||
pass
|
||||
else:
|
||||
bookKey = bookKeys[0]
|
||||
print "Book Key Found!"
|
||||
print u"Book Key Found! ({0})".format(bookKey.encode('hex'))
|
||||
break
|
||||
|
||||
if not bookKey:
|
||||
raise TpzDRMError("Topaz Book. No key found in " + str(len(pidlst)) + " keys tried. Read the FAQs at Alf's blog. Only if none apply, report this failure for help.")
|
||||
raise DrmException(u"No key found in {0:d} keys tried. Read the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(len(pidlst)))
|
||||
|
||||
self.setBookKey(bookKey)
|
||||
self.createBookDirectory()
|
||||
self.extractFiles()
|
||||
print "Successfully Extracted Topaz contents"
|
||||
print u"Successfully Extracted Topaz contents"
|
||||
if inCalibre:
|
||||
from calibre_plugins.k4mobidedrm import genbook
|
||||
else:
|
||||
|
@ -309,7 +356,7 @@ class TopazBook:
|
|||
|
||||
rv = genbook.generateBook(self.outdir, raw, fixedimage)
|
||||
if rv == 0:
|
||||
print "\nBook Successfully generated"
|
||||
print u"Book Successfully generated"
|
||||
return rv
|
||||
|
||||
def createBookDirectory(self):
|
||||
|
@ -317,16 +364,16 @@ class TopazBook:
|
|||
# create output directory structure
|
||||
if not os.path.exists(outdir):
|
||||
os.makedirs(outdir)
|
||||
destdir = os.path.join(outdir,'img')
|
||||
destdir = os.path.join(outdir,u"img")
|
||||
if not os.path.exists(destdir):
|
||||
os.makedirs(destdir)
|
||||
destdir = os.path.join(outdir,'color_img')
|
||||
destdir = os.path.join(outdir,u"color_img")
|
||||
if not os.path.exists(destdir):
|
||||
os.makedirs(destdir)
|
||||
destdir = os.path.join(outdir,'page')
|
||||
destdir = os.path.join(outdir,u"page")
|
||||
if not os.path.exists(destdir):
|
||||
os.makedirs(destdir)
|
||||
destdir = os.path.join(outdir,'glyphs')
|
||||
destdir = os.path.join(outdir,u"glyphs")
|
||||
if not os.path.exists(destdir):
|
||||
os.makedirs(destdir)
|
||||
|
||||
|
@ -334,149 +381,148 @@ class TopazBook:
|
|||
outdir = self.outdir
|
||||
for headerRecord in self.bookHeaderRecords:
|
||||
name = headerRecord
|
||||
if name != "dkey" :
|
||||
ext = '.dat'
|
||||
if name == 'img' : ext = '.jpg'
|
||||
if name == 'color' : ext = '.jpg'
|
||||
print "\nProcessing Section: %s " % name
|
||||
if name != 'dkey':
|
||||
ext = u".dat"
|
||||
if name == 'img': ext = u".jpg"
|
||||
if name == 'color' : ext = u".jpg"
|
||||
print u"Processing Section: {0}\n. . .".format(name),
|
||||
for index in range (0,len(self.bookHeaderRecords[name])) :
|
||||
fnum = "%04d" % index
|
||||
fname = name + fnum + ext
|
||||
fname = u"{0}{1:04d}{2}".format(name,index,ext)
|
||||
destdir = outdir
|
||||
if name == 'img':
|
||||
destdir = os.path.join(outdir,'img')
|
||||
destdir = os.path.join(outdir,u"img")
|
||||
if name == 'color':
|
||||
destdir = os.path.join(outdir,'color_img')
|
||||
destdir = os.path.join(outdir,u"color_img")
|
||||
if name == 'page':
|
||||
destdir = os.path.join(outdir,'page')
|
||||
destdir = os.path.join(outdir,u"page")
|
||||
if name == 'glyphs':
|
||||
destdir = os.path.join(outdir,'glyphs')
|
||||
destdir = os.path.join(outdir,u"glyphs")
|
||||
outputFile = os.path.join(destdir,fname)
|
||||
print ".",
|
||||
print u".",
|
||||
record = self.getBookPayloadRecord(name,index)
|
||||
if record != '':
|
||||
file(outputFile, 'wb').write(record)
|
||||
print " "
|
||||
print u" "
|
||||
|
||||
def getHTMLZip(self, zipname):
|
||||
def getFile(self, zipname):
|
||||
htmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
htmlzip.write(os.path.join(self.outdir,'book.html'),'book.html')
|
||||
htmlzip.write(os.path.join(self.outdir,'book.opf'),'book.opf')
|
||||
if os.path.isfile(os.path.join(self.outdir,'cover.jpg')):
|
||||
htmlzip.write(os.path.join(self.outdir,'cover.jpg'),'cover.jpg')
|
||||
htmlzip.write(os.path.join(self.outdir,'style.css'),'style.css')
|
||||
zipUpDir(htmlzip, self.outdir, 'img')
|
||||
htmlzip.write(os.path.join(self.outdir,u"book.html"),u"book.html")
|
||||
htmlzip.write(os.path.join(self.outdir,u"book.opf"),u"book.opf")
|
||||
if os.path.isfile(os.path.join(self.outdir,u"cover.jpg")):
|
||||
htmlzip.write(os.path.join(self.outdir,u"cover.jpg"),u"cover.jpg")
|
||||
htmlzip.write(os.path.join(self.outdir,u"style.css"),u"style.css")
|
||||
zipUpDir(htmlzip, self.outdir, u"img")
|
||||
htmlzip.close()
|
||||
|
||||
def getBookType(self):
|
||||
return u"Topaz"
|
||||
|
||||
def getBookExtension(self):
|
||||
return u".htmlz"
|
||||
|
||||
def getSVGZip(self, zipname):
|
||||
svgzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
svgzip.write(os.path.join(self.outdir,'index_svg.xhtml'),'index_svg.xhtml')
|
||||
zipUpDir(svgzip, self.outdir, 'svg')
|
||||
zipUpDir(svgzip, self.outdir, 'img')
|
||||
svgzip.write(os.path.join(self.outdir,u"index_svg.xhtml"),u"index_svg.xhtml")
|
||||
zipUpDir(svgzip, self.outdir, u"svg")
|
||||
zipUpDir(svgzip, self.outdir, u"img")
|
||||
svgzip.close()
|
||||
|
||||
def getXMLZip(self, zipname):
|
||||
xmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
targetdir = os.path.join(self.outdir,'xml')
|
||||
zipUpDir(xmlzip, targetdir, '')
|
||||
zipUpDir(xmlzip, self.outdir, 'img')
|
||||
xmlzip.close()
|
||||
|
||||
def cleanup(self):
|
||||
if os.path.isdir(self.outdir):
|
||||
shutil.rmtree(self.outdir, True)
|
||||
|
||||
def usage(progname):
|
||||
print "Removes DRM protection from Topaz ebooks and extract the contents"
|
||||
print "Usage:"
|
||||
print " %s [-k <kindle.info>] [-p <pidnums>] [-s <kindleSerialNumbers>] <infile> <outdir> " % progname
|
||||
|
||||
print u"Removes DRM protection from Topaz ebooks and extracts the contents"
|
||||
print u"Usage:"
|
||||
print u" {0} [-k <kindle.info>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] <infile> <outdir>".format(progname)
|
||||
|
||||
# Main
|
||||
def main(argv=sys.argv):
|
||||
global buildXML
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
k4 = False
|
||||
pids = []
|
||||
serials = []
|
||||
kInfoFiles = []
|
||||
print u"TopazExtract v{0}.".format(__version__)
|
||||
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:")
|
||||
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:x")
|
||||
except getopt.GetoptError, err:
|
||||
print str(err)
|
||||
print u"Error in options or arguments: {0}".format(err.args[0])
|
||||
usage(progname)
|
||||
return 1
|
||||
if len(args)<2:
|
||||
usage(progname)
|
||||
return 1
|
||||
|
||||
for o, a in opts:
|
||||
if o == "-k":
|
||||
if a == None :
|
||||
print "Invalid parameter for -k"
|
||||
return 1
|
||||
kInfoFiles.append(a)
|
||||
if o == "-p":
|
||||
if a == None :
|
||||
print "Invalid parameter for -p"
|
||||
return 1
|
||||
pids = a.split(',')
|
||||
if o == "-s":
|
||||
if a == None :
|
||||
print "Invalid parameter for -s"
|
||||
return 1
|
||||
serials = a.split(',')
|
||||
k4 = True
|
||||
|
||||
infile = args[0]
|
||||
outdir = args[1]
|
||||
|
||||
if not os.path.isfile(infile):
|
||||
print "Input File Does Not Exist"
|
||||
print u"Input File {0} Does Not Exist.".format(infile)
|
||||
return 1
|
||||
|
||||
if not os.path.exists(outdir):
|
||||
print u"Output Directory {0} Does Not Exist.".format(outdir)
|
||||
return 1
|
||||
|
||||
kInfoFiles = []
|
||||
serials = []
|
||||
pids = []
|
||||
|
||||
for o, a in opts:
|
||||
if o == '-k':
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -k")
|
||||
kInfoFiles.append(a)
|
||||
if o == '-p':
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -p")
|
||||
pids = a.split(',')
|
||||
if o == '-s':
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -s")
|
||||
serials = [serial.replace(" ","") for serial in a.split(',')]
|
||||
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
|
||||
tb = TopazBook(infile)
|
||||
title = tb.getBookTitle()
|
||||
print "Processing Book: ", title
|
||||
keysRecord, keysRecordRecord = tb.getPIDMetaInfo()
|
||||
pids.extend(kgenpids.getPidList(keysRecord, keysRecordRecord, k4, serials, kInfoFiles))
|
||||
print u"Processing Book: {0}".format(title)
|
||||
md1, md2 = tb.getPIDMetaInfo()
|
||||
pids.extend(kgenpids.getPidList(md1, md2, serials, kInfoFiles))
|
||||
|
||||
try:
|
||||
print "Decrypting Book"
|
||||
print u"Decrypting Book"
|
||||
tb.processBook(pids)
|
||||
|
||||
print " Creating HTML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_nodrm' + '.htmlz')
|
||||
tb.getHTMLZip(zipname)
|
||||
print u" Creating HTML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + u"_nodrm.htmlz")
|
||||
tb.getFile(zipname)
|
||||
|
||||
print " Creating SVG ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_SVG' + '.zip')
|
||||
print u" Creating SVG ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + u"_SVG.zip")
|
||||
tb.getSVGZip(zipname)
|
||||
|
||||
if buildXML:
|
||||
print " Creating XML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_XML' + '.zip')
|
||||
tb.getXMLZip(zipname)
|
||||
|
||||
# removing internal temporary directory of pieces
|
||||
tb.cleanup()
|
||||
|
||||
except TpzDRMError, e:
|
||||
print str(e)
|
||||
# tb.cleanup()
|
||||
except DrmException, e:
|
||||
print u"Decryption failed\n{0}".format(traceback.format_exc())
|
||||
|
||||
try:
|
||||
tb.cleanup()
|
||||
except:
|
||||
pass
|
||||
return 1
|
||||
|
||||
except Exception, e:
|
||||
print str(e)
|
||||
# tb.cleanup
|
||||
print u"Decryption failed\m{0}".format(traceback.format_exc())
|
||||
try:
|
||||
tb.cleanup()
|
||||
except:
|
||||
pass
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
sys.exit(main())
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
|
|
|
@ -1,26 +1,27 @@
|
|||
eReader PDB2PML - eReaderPDB2PML_v07_plugin.zip
|
||||
eReader PDB2PML - eReaderPDB2PML_v08_plugin.zip
|
||||
===============================================
|
||||
|
||||
All credit given to The Dark Reverser for the original standalone script. I had the much easier job of converting it to a Calibre plugin.
|
||||
|
||||
This plugin is meant to convert secure Ereader files (PDB) to unsecured PMLZ files. Calibre can then convert it to whatever format you desire. It is meant to function without having to install any dependencies... other than having Calibre installed, of course. I've included the psyco libraries (compiled for each platform) for speed. If your system can use them, great! Otherwise, they won't be used and things will just work slower.
|
||||
|
||||
|
||||
Installation:
|
||||
Installation
|
||||
------------
|
||||
|
||||
Go to Calibre's Preferences page. Do **NOT** select "Get Plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior". Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (eReaderPDB2PML_v07_plugin.zip) and click the 'Add' button. You're done.
|
||||
Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior" to go to Calibre's Preferences page. Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (eReaderPDB2PML_v08_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog.
|
||||
|
||||
|
||||
Please note: Calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added.
|
||||
|
||||
|
||||
Configuration:
|
||||
Customization
|
||||
-------------
|
||||
|
||||
Highlight the plugin (eReader PDB 2 PML under the "File type plugins" category) and click the "Customize Plugin" button on Calibre's Preferences->Plugins page. Enter your name and last 8 digits of the credit card number separated by a comma: Your Name,12341234
|
||||
|
||||
If you've purchased books with more than one credit card, separate the info with a colon: Your Name,12341234:Other Name,23452345
|
||||
|
||||
|
||||
Troubleshooting:
|
||||
Troubleshooting
|
||||
---------------
|
||||
|
||||
If you find that it's not working for you (imported ebooks still have DRM), you can save a lot of time and trouble by first deleting the DRMed ebook from calibre and then trying to add the ebook to calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might as well get used to it. ;)
|
||||
|
||||
|
@ -38,4 +39,4 @@ Now copy the output from the terminal window.
|
|||
On Windows, you must use the window menu (little icon at left of window bar) to select all the text and then to copy it.
|
||||
On Macintosh and Linux, just use the normal text select and copy commands.
|
||||
|
||||
Paste the information into a comment at my blog, describing your problem.
|
||||
Paste the information into a comment at my blog, http://apprenticealf.wordpress.com/ describing your problem.
|
||||
|
|
Binary file not shown.
|
@ -1,9 +1,9 @@
|
|||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# eReaderPDB2PML_plugin.py
|
||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||
# later. <http://www.gnu.org/licenses/>
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
#
|
||||
# All credit given to The Dark Reverser for the original standalone script.
|
||||
# I had the much easier job of converting it to Calibre a plugin.
|
||||
|
@ -11,7 +11,7 @@
|
|||
# This plugin is meant to convert secure Ereader files (PDB) to unsecured PMLZ files.
|
||||
# Calibre can then convert it to whatever format you desire.
|
||||
# It is meant to function without having to install any dependencies...
|
||||
# other than having Calibre installed, of course.
|
||||
# other than having Calibre installed, of course.
|
||||
#
|
||||
# Installation:
|
||||
# Go to Calibre's Preferences page... click on the Plugins button. Use the file
|
||||
|
@ -36,6 +36,11 @@
|
|||
# 0.0.5 - updated to the new calibre plugin interface
|
||||
# 0.0.6 - unknown changes
|
||||
# 0.0.7 - improved config dialog processing and fix possible output/unicode problem
|
||||
# 0.0.8 - Proper fix for unicode problems, separate out erdr2pml from plugin
|
||||
|
||||
PLUGIN_NAME = u"eReader PDB 2 PML"
|
||||
PLUGIN_VERSION_TUPLE = (0, 0, 8)
|
||||
PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE])
|
||||
|
||||
import sys, os
|
||||
|
||||
|
@ -43,113 +48,77 @@ from calibre.customize import FileTypePlugin
|
|||
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||
from calibre.constants import iswindows, isosx
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
|
||||
class eRdrDeDRM(FileTypePlugin):
|
||||
name = 'eReader PDB 2 PML' # Name of the plugin
|
||||
description = 'Removes DRM from secure pdb files. \
|
||||
Credit given to The Dark Reverser for the original standalone script.'
|
||||
name = PLUGIN_NAME
|
||||
description = u"Removes DRM from secure pdb files. Credit given to The Dark Reverser for the original standalone script."
|
||||
supported_platforms = ['linux', 'osx', 'windows'] # Platforms this plugin will run on
|
||||
author = 'DiapDealer' # The author of this plugin
|
||||
version = (0, 0, 7) # The version number of this plugin
|
||||
author = u"DiapDealer, Apprentice Alf and The Dark Reverser"
|
||||
version = PLUGIN_VERSION_TUPLE
|
||||
file_types = set(['pdb']) # The file types that this plugin will be applied to
|
||||
on_import = True # Run this plugin during the import
|
||||
minimum_calibre_version = (0, 7, 55)
|
||||
priority = 100
|
||||
|
||||
def run(self, path_to_ebook):
|
||||
from calibre_plugins.erdrpdb2pml import erdr2pml, outputfix
|
||||
|
||||
if sys.stdout.encoding == None:
|
||||
sys.stdout = outputfix.getwriter('utf-8')(sys.stdout)
|
||||
else:
|
||||
sys.stdout = outputfix.getwriter(sys.stdout.encoding)(sys.stdout)
|
||||
if sys.stderr.encoding == None:
|
||||
sys.stderr = outputfix.getwriter('utf-8')(sys.stderr)
|
||||
else:
|
||||
sys.stderr = outputfix.getwriter(sys.stderr.encoding)(sys.stderr)
|
||||
|
||||
global bookname, erdr2pml
|
||||
|
||||
|
||||
# make sure any unicode output gets converted safely with 'replace'
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
|
||||
print u"{0} v{1}: Trying to decrypt {2}.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))
|
||||
|
||||
infile = path_to_ebook
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
outdir = PersistentTemporaryDirectory()
|
||||
pmlzfile = self.temporary_file(bookname + '.pmlz')
|
||||
|
||||
|
||||
if self.site_customization:
|
||||
from calibre_plugins.erdrpdb2pml import erdr2pml
|
||||
|
||||
keydata = self.site_customization
|
||||
ar = keydata.split(':')
|
||||
for i in ar:
|
||||
try:
|
||||
name, cc = i.split(',')
|
||||
#remove spaces at start or end of name, and anywhere in CC
|
||||
name = name.strip()
|
||||
cc = cc.replace(" ","")
|
||||
user_key = erdr2pml.getuser_key(name,cc)
|
||||
except ValueError:
|
||||
print ' Error parsing user supplied data.'
|
||||
print u"{0} v{1}: Error parsing user supplied data.".format(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
return path_to_ebook
|
||||
|
||||
|
||||
try:
|
||||
print "Processing..."
|
||||
print u"{0} v{1}: Processing...".format(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
import time
|
||||
start_time = time.time()
|
||||
pmlfilepath = self.convertEreaderToPml(infile, name, cc, outdir)
|
||||
|
||||
if pmlfilepath and pmlfilepath != 1:
|
||||
import zipfile
|
||||
print " Creating PMLZ file"
|
||||
myZipFile = zipfile.ZipFile(pmlzfile.name,'w',zipfile.ZIP_STORED, False)
|
||||
list = os.listdir(outdir)
|
||||
for file in list:
|
||||
localname = file
|
||||
filePath = os.path.join(outdir,file)
|
||||
if os.path.isfile(filePath):
|
||||
myZipFile.write(filePath, localname)
|
||||
elif os.path.isdir(filePath):
|
||||
imageList = os.listdir(filePath)
|
||||
localimgdir = os.path.basename(filePath)
|
||||
for image in imageList:
|
||||
localname = os.path.join(localimgdir,image)
|
||||
imagePath = os.path.join(filePath,image)
|
||||
if os.path.isfile(imagePath):
|
||||
myZipFile.write(imagePath, localname)
|
||||
myZipFile.close()
|
||||
end_time = time.time()
|
||||
search_time = end_time - start_time
|
||||
print 'elapsed time: %.2f seconds' % (search_time, )
|
||||
print "done"
|
||||
if erdr2pml.decryptBook(infile,pmlzfile.name,True,user_key) == 0:
|
||||
print u"{0} v{1}: Elapsed time: {2:.2f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-start_time)
|
||||
return pmlzfile.name
|
||||
else:
|
||||
raise ValueError('Error Creating PML file.')
|
||||
raise ValueError(u"{0} v{1}: Error Creating PML file.".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
except ValueError, e:
|
||||
print "Error: %s" % e
|
||||
print u"{0} v{1}: Error: {2}".format(PLUGIN_NAME, PLUGIN_VERSION,e.args[0])
|
||||
pass
|
||||
raise Exception('Couldn\'t decrypt pdb file.')
|
||||
raise Exception(u"{0} v{1}: Couldn\'t decrypt pdb file. See Apprentice Alf's blog for help.".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
else:
|
||||
raise Exception('No name and CC# provided.')
|
||||
|
||||
def convertEreaderToPml(self, infile, name, cc, outdir):
|
||||
raise Exception(u"{0} v{1}: No name and CC# provided.".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
|
||||
print " Decoding File"
|
||||
sect = erdr2pml.Sectionizer(infile, 'PNRdPPrs')
|
||||
er = erdr2pml.EreaderProcessor(sect, name, cc)
|
||||
|
||||
if er.getNumImages() > 0:
|
||||
print " Extracting images"
|
||||
#imagedir = bookname + '_img/'
|
||||
imagedir = 'images/'
|
||||
imagedirpath = os.path.join(outdir,imagedir)
|
||||
if not os.path.exists(imagedirpath):
|
||||
os.makedirs(imagedirpath)
|
||||
for i in xrange(er.getNumImages()):
|
||||
name, contents = er.getImage(i)
|
||||
file(os.path.join(imagedirpath, name), 'wb').write(contents)
|
||||
|
||||
print " Extracting pml"
|
||||
pml_string = er.getText()
|
||||
pmlfilename = bookname + ".pml"
|
||||
try:
|
||||
file(os.path.join(outdir, pmlfilename),'wb').write(erdr2pml.cleanPML(pml_string))
|
||||
return os.path.join(outdir, pmlfilename)
|
||||
except:
|
||||
return 1
|
||||
|
||||
def customization_help(self, gui=False):
|
||||
return 'Enter Account Name & Last 8 digits of Credit Card number (separate with a comma)'
|
||||
return u"Enter Account Name & Last 8 digits of Credit Card number (separate with a comma, multiple pairs with a colon)"
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
#!/usr/bin/env python
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# erdr2pml.py
|
||||
# Copyright © 2008 The Dark Reverser
|
||||
#
|
||||
# Modified 2008–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
|
||||
# This is a python script. You need a Python interpreter to run it.
|
||||
# For example, ActiveState Python, which exists for windows.
|
||||
# Changelog
|
||||
|
@ -16,7 +19,7 @@
|
|||
# Custom version 0.03 - no change to eReader support, only usability changes
|
||||
# - start of pep-8 indentation (spaces not tab), fix trailing blanks
|
||||
# - version variable, only one place to change
|
||||
# - added main routine, now callable as a library/module,
|
||||
# - added main routine, now callable as a library/module,
|
||||
# means tools can add optional support for ereader2html
|
||||
# - outdir is no longer a mandatory parameter (defaults based on input name if missing)
|
||||
# - time taken output to stdout
|
||||
|
@ -59,22 +62,14 @@
|
|||
# 0.18 - on Windows try PyCrypto first and OpenSSL next
|
||||
# 0.19 - Modify the interface to allow use of import
|
||||
# 0.20 - modify to allow use inside new interface for calibre plugins
|
||||
# 0.21 - Support eReader (drm) version 11.
|
||||
# - Don't reject dictionary format.
|
||||
# 0.21 - Support eReader (drm) version 11.
|
||||
# - Don't reject dictionary format.
|
||||
# - Ignore sidebars for dictionaries (different format?)
|
||||
# 0.22 - Unicode and plugin support, different image folders for PMLZ and source
|
||||
|
||||
__version__='0.21'
|
||||
__version__='0.22'
|
||||
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
def write(self, data):
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
import sys, re
|
||||
import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
|
@ -82,8 +77,66 @@ if 'calibre' in sys.modules:
|
|||
else:
|
||||
inCalibre = False
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return [u"mobidedrm.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
Des = None
|
||||
if sys.platform.startswith('win'):
|
||||
if iswindows:
|
||||
# first try with pycrypto
|
||||
if inCalibre:
|
||||
from calibre_plugins.erdrpdb2pml import pycrypto_des
|
||||
|
@ -168,17 +221,30 @@ class Sectionizer(object):
|
|||
off = self.sections[section][0]
|
||||
return self.contents[off:end_off]
|
||||
|
||||
def sanitizeFileName(s):
|
||||
r = ''
|
||||
for c in s:
|
||||
if c in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-":
|
||||
r += c
|
||||
return r
|
||||
# cleanup unicode filenames
|
||||
# borrowed from calibre from calibre/src/calibre/__init__.py
|
||||
# added in removal of control (<32) chars
|
||||
# and removal of . at start and end
|
||||
# and with some (heavily edited) code from Paul Durrant's kindlenamer.py
|
||||
def sanitizeFileName(name):
|
||||
# substitute filename unfriendly characters
|
||||
name = name.replace(u"<",u"[").replace(u">",u"]").replace(u" : ",u" – ").replace(u": ",u" – ").replace(u":",u"—").replace(u"/",u"_").replace(u"\\",u"_").replace(u"|",u"_").replace(u"\"",u"\'")
|
||||
# delete control characters
|
||||
name = u"".join(char for char in name if ord(char)>=32)
|
||||
# white space to single space, delete leading and trailing while space
|
||||
name = re.sub(ur"\s", u" ", name).strip()
|
||||
# remove leading dots
|
||||
while len(name)>0 and name[0] == u".":
|
||||
name = name[1:]
|
||||
# remove trailing dots (Windows doesn't like them)
|
||||
if name.endswith(u'.'):
|
||||
name = name[:-1]
|
||||
return name
|
||||
|
||||
def fixKey(key):
|
||||
def fixByte(b):
|
||||
return b ^ ((b ^ (b<<1) ^ (b<<2) ^ (b<<3) ^ (b<<4) ^ (b<<5) ^ (b<<6) ^ (b<<7) ^ 0x80) & 0x80)
|
||||
return "".join([chr(fixByte(ord(a))) for a in key])
|
||||
return "".join([chr(fixByte(ord(a))) for a in key])
|
||||
|
||||
def deXOR(text, sp, table):
|
||||
r=''
|
||||
|
@ -191,7 +257,7 @@ def deXOR(text, sp, table):
|
|||
return r
|
||||
|
||||
class EreaderProcessor(object):
|
||||
def __init__(self, sect, username, creditcard):
|
||||
def __init__(self, sect, user_key):
|
||||
self.section_reader = sect.loadSection
|
||||
data = self.section_reader(0)
|
||||
version, = struct.unpack('>H', data[0:2])
|
||||
|
@ -212,18 +278,10 @@ class EreaderProcessor(object):
|
|||
for i in xrange(len(data)):
|
||||
j = (j + shuf) % len(data)
|
||||
r[j] = data[i]
|
||||
assert len("".join(r)) == len(data)
|
||||
assert len("".join(r)) == len(data)
|
||||
return "".join(r)
|
||||
r = unshuff(input[0:-8], cookie_shuf)
|
||||
|
||||
def fixUsername(s):
|
||||
r = ''
|
||||
for c in s.lower():
|
||||
if (c >= 'a' and c <= 'z' or c >= '0' and c <= '9'):
|
||||
r += c
|
||||
return r
|
||||
|
||||
user_key = struct.pack('>LL', binascii.crc32(fixUsername(username)) & 0xffffffff, binascii.crc32(creditcard[-8:])& 0xffffffff)
|
||||
drm_sub_version = struct.unpack('>H', r[0:2])[0]
|
||||
self.num_text_pages = struct.unpack('>H', r[2:4])[0] - 1
|
||||
self.num_image_pages = struct.unpack('>H', r[26:26+2])[0]
|
||||
|
@ -302,7 +360,7 @@ class EreaderProcessor(object):
|
|||
sect = self.section_reader(self.first_image_page + i)
|
||||
name = sect[4:4+32].strip('\0')
|
||||
data = sect[62:]
|
||||
return sanitizeFileName(name), data
|
||||
return sanitizeFileName(unicode(name,'windows-1252')), data
|
||||
|
||||
|
||||
# def getChapterNamePMLOffsetData(self):
|
||||
|
@ -314,7 +372,7 @@ class EreaderProcessor(object):
|
|||
# offname = deXOR(chaps, j, self.xortable)
|
||||
# offset = struct.unpack('>L', offname[0:4])[0]
|
||||
# name = offname[4:].strip('\0')
|
||||
# cv += '%d|%s\n' % (offset, name)
|
||||
# cv += '%d|%s\n' % (offset, name)
|
||||
# return cv
|
||||
|
||||
# def getLinkNamePMLOffsetData(self):
|
||||
|
@ -326,7 +384,7 @@ class EreaderProcessor(object):
|
|||
# offname = deXOR(links, j, self.xortable)
|
||||
# offset = struct.unpack('>L', offname[0:4])[0]
|
||||
# name = offname[4:].strip('\0')
|
||||
# lv += '%d|%s\n' % (offset, name)
|
||||
# lv += '%d|%s\n' % (offset, name)
|
||||
# return lv
|
||||
|
||||
# def getExpandedTextSizesData(self):
|
||||
|
@ -354,7 +412,7 @@ class EreaderProcessor(object):
|
|||
for i in xrange(self.num_text_pages):
|
||||
logging.debug('get page %d', i)
|
||||
r += zlib.decompress(des.decrypt(self.section_reader(1 + i)))
|
||||
|
||||
|
||||
# now handle footnotes pages
|
||||
if self.num_footnote_pages > 0:
|
||||
r += '\n'
|
||||
|
@ -399,60 +457,53 @@ class EreaderProcessor(object):
|
|||
return r
|
||||
|
||||
def cleanPML(pml):
|
||||
# Convert special characters to proper PML code. High ASCII start at (\x80, \a128) and go up to (\xff, \a255)
|
||||
pml2 = pml
|
||||
for k in xrange(128,256):
|
||||
badChar = chr(k)
|
||||
pml2 = pml2.replace(badChar, '\\a%03d' % k)
|
||||
return pml2
|
||||
# Convert special characters to proper PML code. High ASCII start at (\x80, \a128) and go up to (\xff, \a255)
|
||||
pml2 = pml
|
||||
for k in xrange(128,256):
|
||||
badChar = chr(k)
|
||||
pml2 = pml2.replace(badChar, '\\a%03d' % k)
|
||||
return pml2
|
||||
|
||||
def convertEreaderToPml(infile, name, cc, outdir):
|
||||
if not os.path.exists(outdir):
|
||||
os.makedirs(outdir)
|
||||
def decryptBook(infile, outpath, make_pmlz, user_key):
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
print " Decoding File"
|
||||
sect = Sectionizer(infile, 'PNRdPPrs')
|
||||
er = EreaderProcessor(sect, name, cc)
|
||||
|
||||
if er.getNumImages() > 0:
|
||||
print " Extracting images"
|
||||
imagedir = bookname + '_img/'
|
||||
imagedirpath = os.path.join(outdir,imagedir)
|
||||
if not os.path.exists(imagedirpath):
|
||||
os.makedirs(imagedirpath)
|
||||
for i in xrange(er.getNumImages()):
|
||||
name, contents = er.getImage(i)
|
||||
file(os.path.join(imagedirpath, name), 'wb').write(contents)
|
||||
|
||||
print " Extracting pml"
|
||||
pml_string = er.getText()
|
||||
pmlfilename = bookname + ".pml"
|
||||
file(os.path.join(outdir, pmlfilename),'wb').write(cleanPML(pml_string))
|
||||
|
||||
# bkinfo = er.getBookInfo()
|
||||
# if bkinfo != '':
|
||||
# print " Extracting book meta information"
|
||||
# file(os.path.join(outdir, 'bookinfo.txt'),'wb').write(bkinfo)
|
||||
|
||||
|
||||
|
||||
def decryptBook(infile, outdir, name, cc, make_pmlz):
|
||||
if make_pmlz :
|
||||
# ignore specified outdir, use tempdir instead
|
||||
if make_pmlz:
|
||||
# outpath is actually pmlz name
|
||||
pmlzname = outpath
|
||||
outdir = tempfile.mkdtemp()
|
||||
imagedirpath = os.path.join(outdir,u"images")
|
||||
else:
|
||||
pmlzname = None
|
||||
outdir = outpath
|
||||
imagedirpath = os.path.join(outdir,bookname + u"_img")
|
||||
|
||||
try:
|
||||
print "Processing..."
|
||||
convertEreaderToPml(infile, name, cc, outdir)
|
||||
if make_pmlz :
|
||||
if not os.path.exists(outdir):
|
||||
os.makedirs(outdir)
|
||||
print u"Decoding File"
|
||||
sect = Sectionizer(infile, 'PNRdPPrs')
|
||||
er = EreaderProcessor(sect, user_key)
|
||||
|
||||
if er.getNumImages() > 0:
|
||||
print u"Extracting images"
|
||||
if not os.path.exists(imagedirpath):
|
||||
os.makedirs(imagedirpath)
|
||||
for i in xrange(er.getNumImages()):
|
||||
name, contents = er.getImage(i)
|
||||
file(os.path.join(imagedirpath, name), 'wb').write(contents)
|
||||
|
||||
print u"Extracting pml"
|
||||
pml_string = er.getText()
|
||||
pmlfilename = bookname + ".pml"
|
||||
file(os.path.join(outdir, pmlfilename),'wb').write(cleanPML(pml_string))
|
||||
if pmlzname is not None:
|
||||
import zipfile
|
||||
import shutil
|
||||
print " Creating PMLZ file"
|
||||
zipname = infile[:-4] + '.pmlz'
|
||||
myZipFile = zipfile.ZipFile(zipname,'w',zipfile.ZIP_STORED, False)
|
||||
print u"Creating PMLZ file {0}".format(os.path.basename(pmlzname))
|
||||
myZipFile = zipfile.ZipFile(pmlzname,'w',zipfile.ZIP_STORED, False)
|
||||
list = os.listdir(outdir)
|
||||
for file in list:
|
||||
localname = file
|
||||
filePath = os.path.join(outdir,file)
|
||||
for filename in list:
|
||||
localname = filename
|
||||
filePath = os.path.join(outdir,filename)
|
||||
if os.path.isfile(filePath):
|
||||
myZipFile.write(filePath, localname)
|
||||
elif os.path.isdir(filePath):
|
||||
|
@ -466,36 +517,46 @@ def decryptBook(infile, outdir, name, cc, make_pmlz):
|
|||
myZipFile.close()
|
||||
# remove temporary directory
|
||||
shutil.rmtree(outdir, True)
|
||||
print 'output is %s' % zipname
|
||||
print u"Output is {0}".format(pmlzname)
|
||||
else :
|
||||
print 'output in %s' % outdir
|
||||
print u"Output is in {0}".format(outdir)
|
||||
print "done"
|
||||
except ValueError, e:
|
||||
print "Error: %s" % e
|
||||
print u"Error: {0}".format(e.args[0])
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def usage():
|
||||
print "Converts DRMed eReader books to PML Source"
|
||||
print "Usage:"
|
||||
print " erdr2pml [options] infile.pdb [outdir] \"your name\" credit_card_number "
|
||||
print " "
|
||||
print "Options: "
|
||||
print " -h prints this message"
|
||||
print " --make-pmlz create PMLZ instead of using output directory"
|
||||
print " "
|
||||
print "Note:"
|
||||
print " if ommitted, outdir defaults based on 'infile.pdb'"
|
||||
print " It's enough to enter the last 8 digits of the credit card number"
|
||||
print u"Converts DRMed eReader books to PML Source"
|
||||
print u"Usage:"
|
||||
print u" erdr2pml [options] infile.pdb [outpath] \"your name\" credit_card_number"
|
||||
print u" "
|
||||
print u"Options: "
|
||||
print u" -h prints this message"
|
||||
print u" -p create PMLZ instead of source folder"
|
||||
print u" --make-pmlz create PMLZ instead of source folder"
|
||||
print u" "
|
||||
print u"Note:"
|
||||
print u" if outpath is ommitted, creates source in 'infile_Source' folder"
|
||||
print u" if outpath is ommitted and pmlz option, creates PMLZ 'infile.pmlz'"
|
||||
print u" if source folder created, images are in infile_img folder"
|
||||
print u" if pmlz file created, images are in images folder"
|
||||
print u" It's enough to enter the last 8 digits of the credit card number"
|
||||
return
|
||||
|
||||
def getuser_key(name,cc):
|
||||
newname = "".join(c for c in name.lower() if c >= 'a' and c <= 'z' or c >= '0' and c <= '9')
|
||||
cc = cc.replace(" ","")
|
||||
return struct.pack('>LL', binascii.crc32(newname) & 0xffffffff,binascii.crc32(cc[-8:])& 0xffffffff)
|
||||
|
||||
def cli_main(argv=unicode_argv()):
|
||||
print u"eRdr2Pml v{0}. Copyright © 2009–2012 The Dark Reverser et al.".format(__version__)
|
||||
|
||||
def main(argv=None):
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], "h", ["make-pmlz"])
|
||||
opts, args = getopt.getopt(argv[1:], "hp", ["make-pmlz"])
|
||||
except getopt.GetoptError, err:
|
||||
print str(err)
|
||||
print err.args[0]
|
||||
usage()
|
||||
return 1
|
||||
make_pmlz = False
|
||||
|
@ -503,25 +564,31 @@ def main(argv=None):
|
|||
if o == "-h":
|
||||
usage()
|
||||
return 0
|
||||
elif o == "-p":
|
||||
make_pmlz = True
|
||||
elif o == "--make-pmlz":
|
||||
make_pmlz = True
|
||||
|
||||
print "eRdr2Pml v%s. Copyright (c) 2009 The Dark Reverser" % __version__
|
||||
|
||||
if len(args)!=3 and len(args)!=4:
|
||||
usage()
|
||||
return 1
|
||||
|
||||
if len(args)==3:
|
||||
infile, name, cc = args[0], args[1], args[2]
|
||||
outdir = infile[:-4] + '_Source'
|
||||
infile, name, cc = args
|
||||
if make_pmlz:
|
||||
outpath = os.path.splitext(infile)[0] + u".pmlz"
|
||||
else:
|
||||
outpath = os.path.splitext(infile)[0] + u"_Source"
|
||||
elif len(args)==4:
|
||||
infile, outdir, name, cc = args[0], args[1], args[2], args[3]
|
||||
infile, outpath, name, cc = args
|
||||
|
||||
return decryptBook(infile, outdir, name, cc, make_pmlz)
|
||||
print getuser_key(name,cc).encode('hex')
|
||||
|
||||
return decryptBook(infile, outpath, make_pmlz, getuser_key(name,cc))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
sys.exit(main())
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ def load_libcrypto():
|
|||
return None
|
||||
|
||||
libcrypto = CDLL(libcrypto)
|
||||
|
||||
|
||||
# typedef struct DES_ks
|
||||
# {
|
||||
# union
|
||||
|
@ -30,7 +30,7 @@ def load_libcrypto():
|
|||
# } ks[16];
|
||||
# } DES_key_schedule;
|
||||
|
||||
# just create a big enough place to hold everything
|
||||
# just create a big enough place to hold everything
|
||||
# it will have alignment of structure so we should be okay (16 byte aligned?)
|
||||
class DES_KEY_SCHEDULE(Structure):
|
||||
_fields_ = [('DES_cblock1', c_char * 16),
|
||||
|
@ -61,7 +61,7 @@ def load_libcrypto():
|
|||
DES_set_key = F(None, 'DES_set_key',[c_char_p, DES_KEY_SCHEDULE_p])
|
||||
DES_ecb_encrypt = F(None, 'DES_ecb_encrypt',[c_char_p, c_char_p, DES_KEY_SCHEDULE_p, c_int])
|
||||
|
||||
|
||||
|
||||
class DES(object):
|
||||
def __init__(self, key):
|
||||
if len(key) != 8 :
|
||||
|
@ -87,4 +87,3 @@ def load_libcrypto():
|
|||
return ''.join(result)
|
||||
|
||||
return DES
|
||||
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Adapted and simplified from the kitchen project
|
||||
#
|
||||
# Kitchen Project Copyright (c) 2012 Red Hat, Inc.
|
||||
#
|
||||
# kitchen is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# kitchen is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with kitchen; if not, see <http://www.gnu.org/licenses/>
|
||||
#
|
||||
# Authors:
|
||||
# Toshio Kuratomi <toshio@fedoraproject.org>
|
||||
# Seth Vidal
|
||||
#
|
||||
# Portions of code taken from yum/i18n.py and
|
||||
# python-fedora: fedora/textutils.py
|
||||
|
||||
import codecs
|
||||
|
||||
# returns a char string unchanged
|
||||
# returns a unicode string converted to a char string of the passed encoding
|
||||
# return the empty string for anything else
|
||||
def getwriter(encoding):
|
||||
class _StreamWriter(codecs.StreamWriter):
|
||||
def __init__(self, stream):
|
||||
codecs.StreamWriter.__init__(self, stream, 'replace')
|
||||
|
||||
def encode(self, msg, errors='replace'):
|
||||
if isinstance(msg, basestring):
|
||||
if isinstance(msg, str):
|
||||
return (msg, len(msg))
|
||||
return (msg.encode(self.encoding, 'replace'), len(msg))
|
||||
return ('',0)
|
||||
|
||||
_StreamWriter.encoding = encoding
|
||||
return _StreamWriter
|
|
@ -28,4 +28,3 @@ def load_pycrypto():
|
|||
i += 8
|
||||
return ''.join(result)
|
||||
return DES
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
import sys
|
||||
|
||||
ECB = 0
|
||||
CBC = 1
|
||||
ECB = 0
|
||||
CBC = 1
|
||||
class Des(object):
|
||||
__pc1 = [56, 48, 40, 32, 24, 16, 8, 0, 57, 49, 41, 33, 25, 17,
|
||||
9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35,
|
||||
|
@ -11,13 +11,13 @@ class Des(object):
|
|||
13, 5, 60, 52, 44, 36, 28, 20, 12, 4, 27, 19, 11, 3]
|
||||
__left_rotations = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1]
|
||||
__pc2 = [13, 16, 10, 23, 0, 4,2, 27, 14, 5, 20, 9,
|
||||
22, 18, 11, 3, 25, 7, 15, 6, 26, 19, 12, 1,
|
||||
40, 51, 30, 36, 46, 54, 29, 39, 50, 44, 32, 47,
|
||||
43, 48, 38, 55, 33, 52, 45, 41, 49, 35, 28, 31]
|
||||
__ip = [57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3,
|
||||
61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7,
|
||||
56, 48, 40, 32, 24, 16, 8, 0, 58, 50, 42, 34, 26, 18, 10, 2,
|
||||
60, 52, 44, 36, 28, 20, 12, 4, 62, 54, 46, 38, 30, 22, 14, 6]
|
||||
22, 18, 11, 3, 25, 7, 15, 6, 26, 19, 12, 1,
|
||||
40, 51, 30, 36, 46, 54, 29, 39, 50, 44, 32, 47,
|
||||
43, 48, 38, 55, 33, 52, 45, 41, 49, 35, 28, 31]
|
||||
__ip = [57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3,
|
||||
61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7,
|
||||
56, 48, 40, 32, 24, 16, 8, 0, 58, 50, 42, 34, 26, 18, 10, 2,
|
||||
60, 52, 44, 36, 28, 20, 12, 4, 62, 54, 46, 38, 30, 22, 14, 6]
|
||||
__expansion_table = [31, 0, 1, 2, 3, 4, 3, 4, 5, 6, 7, 8,
|
||||
7, 8, 9, 10, 11, 12,11, 12, 13, 14, 15, 16,
|
||||
15, 16, 17, 18, 19, 20,19, 20, 21, 22, 23, 24,
|
||||
|
@ -61,8 +61,8 @@ class Des(object):
|
|||
35, 3, 43, 11, 51, 19, 59, 27,34, 2, 42, 10, 50, 18, 58, 26,
|
||||
33, 1, 41, 9, 49, 17, 57, 25,32, 0, 40, 8, 48, 16, 56, 24]
|
||||
# Type of crypting being done
|
||||
ENCRYPT = 0x00
|
||||
DECRYPT = 0x01
|
||||
ENCRYPT = 0x00
|
||||
DECRYPT = 0x01
|
||||
def __init__(self, key, mode=ECB, IV=None):
|
||||
if len(key) != 8:
|
||||
raise ValueError("Invalid DES key size. Key must be exactly 8 bytes long.")
|
||||
|
@ -74,7 +74,7 @@ class Des(object):
|
|||
self.setIV(IV)
|
||||
self.L = []
|
||||
self.R = []
|
||||
self.Kn = [ [0] * 48 ] * 16 # 16 48-bit keys (K1 - K16)
|
||||
self.Kn = [ [0] * 48 ] * 16 # 16 48-bit keys (K1 - K16)
|
||||
self.final = []
|
||||
self.setKey(key)
|
||||
def getKey(self):
|
||||
|
|
Binary file not shown.
|
@ -1,5 +1,4 @@
|
|||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
@ -7,17 +6,17 @@ __license__ = 'GPL v3'
|
|||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||
# later. <http://www.gnu.org/licenses/>
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
#
|
||||
# Requires Calibre version 0.7.55 or higher.
|
||||
#
|
||||
# All credit given to I ♥ Cabbages for the original standalone scripts.
|
||||
# I had the much easier job of converting them to Calibre a plugin.
|
||||
# All credit given to i♥cabbages for the original standalone scripts.
|
||||
# I had the much easier job of converting them to a calibre plugin.
|
||||
#
|
||||
# This plugin is meant to decrypt Barnes & Noble Epubs that are protected
|
||||
# with a version of Adobe's Adept encryption. It is meant to function without having to
|
||||
# install any dependencies... other than having Calibre installed, of course. It will still
|
||||
# install any dependencies... other than having calibre installed, of course. It will still
|
||||
# work if you have Python and PyCrypto already installed, but they aren't necessary.
|
||||
#
|
||||
# Configuration:
|
||||
|
@ -38,227 +37,75 @@ __docformat__ = 'restructuredtext en'
|
|||
# 0.1.7 - Fix for potential problem with PyCrypto
|
||||
# 0.1.8 - an updated/modified zipfix.py and included zipfilerugged.py
|
||||
# 0.2.0 - Completely overhauled plugin configuration dialog and key management/storage
|
||||
# 0.2.1 - an updated/modified zipfix.py and included zipfilerugged.py
|
||||
# 0.2.1 - added zipfix.py and included zipfilerugged.py from 0.1.8
|
||||
# 0.2.2 - added in potential fixes from 0.1.7 that had been missed.
|
||||
# 0.2.3 - fixed possible output/unicode problem
|
||||
# 0.2.4 - ditched nearly hopeless caselessStrCmp method in favor of uStrCmp.
|
||||
# - added ability to rename existing keys.
|
||||
# 0.2.5 - Major code change to use unaltered ignobleepub.py 3.6 and
|
||||
# - ignoblekeygen 2.4 and later.
|
||||
|
||||
"""
|
||||
Decrypt Barnes & Noble ADEPT encrypted EPUB books.
|
||||
"""
|
||||
|
||||
PLUGIN_NAME = 'Ignoble Epub DeDRM'
|
||||
PLUGIN_VERSION_TUPLE = (0, 2, 4)
|
||||
PLUGIN_NAME = u"Ignoble Epub DeDRM"
|
||||
PLUGIN_VERSION_TUPLE = (0, 2, 5)
|
||||
PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE])
|
||||
# Include an html helpfile in the plugin's zipfile with the following name.
|
||||
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
|
||||
|
||||
import sys, os, zlib, re
|
||||
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||
from zipfile import ZipInfo as _ZipInfo
|
||||
#from lxml import etree
|
||||
try:
|
||||
import xml.etree.cElementTree as etree
|
||||
except ImportError:
|
||||
import xml.etree.ElementTree as etree
|
||||
from contextlib import closing
|
||||
|
||||
global AES
|
||||
|
||||
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
|
||||
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
||||
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
||||
import sys, os, re
|
||||
import zipfile
|
||||
from zipfile import ZipFile
|
||||
|
||||
class IGNOBLEError(Exception):
|
||||
pass
|
||||
|
||||
def _load_crypto_libcrypto():
|
||||
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
||||
Structure, c_ulong, create_string_buffer, cast
|
||||
from ctypes.util import find_library
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
libcrypto = find_library('libeay32')
|
||||
else:
|
||||
libcrypto = find_library('crypto')
|
||||
if libcrypto is None:
|
||||
raise IGNOBLEError('%s Plugin v%s: libcrypto not found' % (PLUGIN_NAME, PLUGIN_VERSION))
|
||||
libcrypto = CDLL(libcrypto)
|
||||
|
||||
AES_MAXNR = 14
|
||||
|
||||
c_char_pp = POINTER(c_char_p)
|
||||
c_int_p = POINTER(c_int)
|
||||
|
||||
class AES_KEY(Structure):
|
||||
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
|
||||
('rounds', c_int)]
|
||||
AES_KEY_p = POINTER(AES_KEY)
|
||||
|
||||
def F(restype, name, argtypes):
|
||||
func = getattr(libcrypto, name)
|
||||
func.restype = restype
|
||||
func.argtypes = argtypes
|
||||
return func
|
||||
|
||||
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
|
||||
[c_char_p, c_int, AES_KEY_p])
|
||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
||||
c_int])
|
||||
|
||||
class AES(object):
|
||||
def __init__(self, userkey):
|
||||
self._blocksize = len(userkey)
|
||||
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
||||
raise IGNOBLEError('%s Plugin v%s: AES improper key used' % (PLUGIN_NAME, PLUGIN_VERSION))
|
||||
return
|
||||
key = self._key = AES_KEY()
|
||||
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
|
||||
if rv < 0:
|
||||
raise IGNOBLEError('%s Plugin v%s: Failed to initialize AES key' % (PLUGIN_NAME, PLUGIN_VERSION))
|
||||
|
||||
def decrypt(self, data):
|
||||
out = create_string_buffer(len(data))
|
||||
iv = ("\x00" * self._blocksize)
|
||||
rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
|
||||
if rv == 0:
|
||||
raise IGNOBLEError('%s Plugin v%s: AES decryption failed' % (PLUGIN_NAME, PLUGIN_VERSION))
|
||||
return out.raw
|
||||
|
||||
print '%s Plugin v%s: Using libcrypto.' %(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
return AES
|
||||
|
||||
def _load_crypto_pycrypto():
|
||||
from Crypto.Cipher import AES as _AES
|
||||
|
||||
class AES(object):
|
||||
def __init__(self, key):
|
||||
self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16)
|
||||
|
||||
def decrypt(self, data):
|
||||
return self._aes.decrypt(data)
|
||||
|
||||
print '%s Plugin v%s: Using PyCrypto.' %(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
return AES
|
||||
|
||||
def _load_crypto():
|
||||
_aes = None
|
||||
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
||||
if sys.platform.startswith('win'):
|
||||
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
||||
for loader in cryptolist:
|
||||
try:
|
||||
_aes = loader()
|
||||
break
|
||||
except (ImportError, IGNOBLEError):
|
||||
pass
|
||||
return _aes
|
||||
|
||||
class ZipInfo(_ZipInfo):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'compress_type' in kwargs:
|
||||
compress_type = kwargs.pop('compress_type')
|
||||
super(ZipInfo, self).__init__(*args, **kwargs)
|
||||
self.compress_type = compress_type
|
||||
|
||||
class Decryptor(object):
|
||||
def __init__(self, bookkey, encryption):
|
||||
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
|
||||
self._aes = AES(bookkey)
|
||||
encryption = etree.fromstring(encryption)
|
||||
self._encrypted = encrypted = set()
|
||||
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
|
||||
enc('CipherReference'))
|
||||
for elem in encryption.findall(expr):
|
||||
path = elem.get('URI', None)
|
||||
path = path.encode('utf-8')
|
||||
if path is not None:
|
||||
encrypted.add(path)
|
||||
|
||||
def decompress(self, bytes):
|
||||
dc = zlib.decompressobj(-15)
|
||||
bytes = dc.decompress(bytes)
|
||||
ex = dc.decompress('Z') + dc.flush()
|
||||
if ex:
|
||||
bytes = bytes + ex
|
||||
return bytes
|
||||
|
||||
def decrypt(self, path, data):
|
||||
if path in self._encrypted:
|
||||
data = self._aes.decrypt(data)[16:]
|
||||
data = data[:-ord(data[-1])]
|
||||
data = self.decompress(data)
|
||||
return data
|
||||
|
||||
def plugin_main(userkey, inpath, outpath):
|
||||
key = userkey.decode('base64')[:16]
|
||||
aes = AES(key)
|
||||
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or 'META-INF/encryption.xml' not in namelist:
|
||||
print '%s Plugin: Not Encrypted.' % PLUGIN_NAME
|
||||
return 1
|
||||
for name in META_NAMES:
|
||||
namelist.remove(name)
|
||||
try: # If the generated keyfile doesn't match the bookkey, this is where it's likely to blow up.
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
bookkey = aes.decrypt(bookkey.decode('base64'))
|
||||
bookkey = bookkey[:-ord(bookkey[-1])]
|
||||
encryption = inf.read('META-INF/encryption.xml')
|
||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
||||
outf.writestr(zi, inf.read('mimetype'))
|
||||
for path in namelist:
|
||||
data = inf.read(path)
|
||||
outf.writestr(path, decryptor.decrypt(path, data))
|
||||
except:
|
||||
return 2
|
||||
return 0
|
||||
|
||||
from calibre.customize import FileTypePlugin
|
||||
from calibre.constants import iswindows, isosx
|
||||
from calibre.gui2 import is_ok_to_use_qt
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
|
||||
class IgnobleDeDRM(FileTypePlugin):
|
||||
name = PLUGIN_NAME
|
||||
description = 'Removes DRM from secure Barnes & Noble epub files. Credit given to I ♥ Cabbages for the original stand-alone scripts.'
|
||||
description = u"Removes DRM from secure Barnes & Noble epub files. Credit given to i♥cabbages for the original stand-alone scripts."
|
||||
supported_platforms = ['linux', 'osx', 'windows']
|
||||
author = 'DiapDealer'
|
||||
author = u"DiapDealer, Apprentice Alf and i♥cabbages"
|
||||
version = PLUGIN_VERSION_TUPLE
|
||||
minimum_calibre_version = (0, 7, 55) # Compiled python libraries cannot be imported in earlier versions.
|
||||
file_types = set(['epub'])
|
||||
on_import = True
|
||||
|
||||
def run(self, path_to_ebook):
|
||||
from calibre_plugins.ignoble_epub import outputfix
|
||||
|
||||
if sys.stdout.encoding == None:
|
||||
sys.stdout = outputfix.getwriter('utf-8')(sys.stdout)
|
||||
else:
|
||||
sys.stdout = outputfix.getwriter(sys.stdout.encoding)(sys.stdout)
|
||||
if sys.stderr.encoding == None:
|
||||
sys.stderr = outputfix.getwriter('utf-8')(sys.stderr)
|
||||
else:
|
||||
sys.stderr = outputfix.getwriter(sys.stderr.encoding)(sys.stderr)
|
||||
|
||||
global AES
|
||||
priority = 101
|
||||
|
||||
print '\n\nRunning {0} v{1} on "{2}"'.format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))
|
||||
AES = _load_crypto()
|
||||
if AES == None:
|
||||
# Failed to load libcrypto or PyCrypto... Adobe Epubs can't be decrypted.'
|
||||
raise Exception('%s Plugin v%s: Failed to load crypto libs.' % (PLUGIN_NAME, PLUGIN_VERSION))
|
||||
def run(self, path_to_ebook):
|
||||
|
||||
# make sure any unicode output gets converted safely with 'replace'
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
|
||||
print u"{0} v{1}: Trying to decrypt {2}.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))
|
||||
|
||||
# First time use or first time after upgrade to new key-handling/storage method
|
||||
# or no keys configured. Give a visual prompt to configure.
|
||||
import calibre_plugins.ignoble_epub.config as cfg
|
||||
import calibre_plugins.ignobleepub.config as cfg
|
||||
if not cfg.prefs['configured']:
|
||||
titlemsg = '%s v%s' % (PLUGIN_NAME, PLUGIN_VERSION)
|
||||
errmsg = titlemsg + ' not (properly) configured!\n' + \
|
||||
|
@ -275,56 +122,67 @@ class IgnobleDeDRM(FileTypePlugin):
|
|||
d.exec_()
|
||||
raise Exception('%s Plugin v%s: Plugin not configured.' % (PLUGIN_NAME, PLUGIN_VERSION))
|
||||
|
||||
# Create a TemporaryPersistent file to work with.
|
||||
# Check original epub archive for zip errors.
|
||||
from calibre_plugins.ignoble_epub import zipfix
|
||||
inf = self.temporary_file('.epub')
|
||||
from calibre_plugins.ignobleepub import zipfix
|
||||
inf = self.temporary_file(u".epub")
|
||||
try:
|
||||
print '%s Plugin: Verifying zip archive integrity.' % PLUGIN_NAME
|
||||
print u"{0} v{1}: Verifying zip archive integrity.".format(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
fr = zipfix.fixZip(path_to_ebook, inf.name)
|
||||
fr.fix()
|
||||
except Exception, e:
|
||||
print '%s Plugin: unforeseen zip archive issue.' % PLUGIN_NAME
|
||||
print u"{0} v{1}: Error \'{2}\' when checking zip archive.".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0])
|
||||
raise Exception(e)
|
||||
# Create a TemporaryPersistent file to work with.
|
||||
of = self.temporary_file('.epub')
|
||||
|
||||
# Attempt to decrypt epub with each encryption key (generated or provided).
|
||||
key_counter = 1
|
||||
for keyname, userkey in cfg.prefs['keys'].items():
|
||||
keyname_masked = keyname[:4] + ''.join('x' for x in keyname[4:])
|
||||
# Give the user key, ebook and TemporaryPersistent file to the Stripper function.
|
||||
result = plugin_main(userkey, inf.name, of.name)
|
||||
return
|
||||
|
||||
# Ebook is not a B&N Adept epub... do nothing and pass it on.
|
||||
#check the book
|
||||
from calibre_plugins.ignobleepub import ignobleepub
|
||||
if not ignobleepub.ignobleBook(inf.name):
|
||||
print u"{0} v{1}: {2} is not a secure Barnes & Noble ePub.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))
|
||||
# return the original file, so that no error message is generated in the GUI
|
||||
return path_to_ebook
|
||||
|
||||
|
||||
# Attempt to decrypt epub with each encryption key (generated or provided).
|
||||
for keyname, userkey in cfg.prefs['keys'].items():
|
||||
keyname_masked = u"".join((u'X' if (x.isdigit()) else x) for x in keyname)
|
||||
print u"{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname_masked)
|
||||
of = self.temporary_file(u".epub")
|
||||
|
||||
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
|
||||
result = ignobleepub.decryptBook(userkey, inf.name, of.name)
|
||||
|
||||
# Ebook is not a B&N epub... do nothing and pass it on.
|
||||
# This allows a non-encrypted epub to be imported without error messages.
|
||||
if result == 1:
|
||||
print '%s Plugin: Not a B&N Epub - doing nothing.\n' % PLUGIN_NAME
|
||||
if result[0] == 1:
|
||||
print u"{0} v{1}: {2}".format(PLUGIN_NAME, PLUGIN_VERSION, result[1])
|
||||
of.close()
|
||||
return path_to_ebook
|
||||
break
|
||||
|
||||
# Decryption was successful return the modified PersistentTemporary
|
||||
# file to Calibre's import process.
|
||||
if result == 0:
|
||||
print '{0} Plugin: Encryption key {1} ("{2}") correct!'.format(PLUGIN_NAME, key_counter, keyname_masked)
|
||||
if result[0] == 0:
|
||||
print u"{0} v{1}: Encryption successfully removed.".format(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
of.close()
|
||||
return of.name
|
||||
break
|
||||
|
||||
print '{0} Plugin: Encryption key {1} ("{2}") incorrect!'.format(PLUGIN_NAME, key_counter, keyname_masked)
|
||||
key_counter += 1
|
||||
print u"{0} v{1}: {2}".format(PLUGIN_NAME, PLUGIN_VERSION, result[1])
|
||||
of.close()
|
||||
|
||||
|
||||
# Something went wrong with decryption.
|
||||
# Import the original unmolested epub.
|
||||
of.close
|
||||
raise Exception('%s Plugin v%s: Ultimately failed to decrypt.\n' % (PLUGIN_NAME, PLUGIN_VERSION))
|
||||
print(u"{0} v{1}: Ultimately failed to decrypt".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
return path_to_ebook
|
||||
|
||||
def is_customizable(self):
|
||||
# return true to allow customization via the Plugin->Preferences.
|
||||
return True
|
||||
|
||||
def config_widget(self):
|
||||
from calibre_plugins.ignoble_epub.config import ConfigWidget
|
||||
from calibre_plugins.ignobleepub.config import ConfigWidget
|
||||
# Extract the helpfile contents from in the plugin's zipfile.
|
||||
# The helpfile must be named <plugin name variable> + '_Help.htm'
|
||||
return ConfigWidget(self.load_resources(RESOURCE_NAME)[RESOURCE_NAME])
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
# Standard Python modules.
|
||||
|
@ -19,11 +20,11 @@ from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url,
|
|||
from calibre.utils.config import dynamic, config_dir, JSONConfig
|
||||
|
||||
# modules from this plugin's zipfile.
|
||||
from calibre_plugins.ignoble_epub.__init__ import PLUGIN_NAME, PLUGIN_VERSION
|
||||
from calibre_plugins.ignoble_epub.__init__ import RESOURCE_NAME as help_file_name
|
||||
from calibre_plugins.ignoble_epub.utilities import (_load_crypto, normalize_name,
|
||||
generate_keyfile, uStrCmp, DETAILED_MESSAGE, parseCustString)
|
||||
from calibre_plugins.ignoble_epub.dialogs import AddKeyDialog, RenameKeyDialog
|
||||
from calibre_plugins.ignobleepub.__init__ import PLUGIN_NAME, PLUGIN_VERSION
|
||||
from calibre_plugins.ignobleepub.__init__ import RESOURCE_NAME as help_file_name
|
||||
from calibre_plugins.ignobleepub.utilities import (uStrCmp, DETAILED_MESSAGE, parseCustString)
|
||||
from calibre_plugins.ignobleepub.dialogs import AddKeyDialog, RenameKeyDialog
|
||||
from calibre_plugins.ignobleepub.ignoblekeygen import generate_key
|
||||
|
||||
JSON_NAME = PLUGIN_NAME.strip().lower().replace(' ', '_')
|
||||
JSON_PATH = 'plugins/' + JSON_NAME + '.json'
|
||||
|
@ -40,7 +41,7 @@ prefs.defaults['configured'] = False
|
|||
class ConfigWidget(QWidget):
|
||||
def __init__(self, help_file_data):
|
||||
QWidget.__init__(self)
|
||||
|
||||
|
||||
self.help_file_data = help_file_data
|
||||
self.plugin_keys = prefs['keys']
|
||||
|
||||
|
@ -88,7 +89,7 @@ class ConfigWidget(QWidget):
|
|||
val = sc.pop(PLUGIN_NAME, None)
|
||||
if val is not None:
|
||||
config['plugin_customization'] = sc
|
||||
|
||||
|
||||
# First time run since upgrading to new key storage method, or 0 keys configured.
|
||||
# Prompt to import pre-existing key files.
|
||||
if not prefs['configured']:
|
||||
|
@ -102,7 +103,7 @@ class ConfigWidget(QWidget):
|
|||
# Start Qt Gui dialog layout
|
||||
layout = QVBoxLayout(self)
|
||||
self.setLayout(layout)
|
||||
|
||||
|
||||
help_layout = QHBoxLayout()
|
||||
layout.addLayout(help_layout)
|
||||
# Add hyperlink to a help file at the right. We will replace the correct name when it is clicked.
|
||||
|
@ -111,12 +112,12 @@ class ConfigWidget(QWidget):
|
|||
help_label.setAlignment(Qt.AlignRight)
|
||||
help_label.linkActivated.connect(self.help_link_activated)
|
||||
help_layout.addWidget(help_label)
|
||||
|
||||
|
||||
keys_group_box = QGroupBox(_('Configured Ignoble Keys:'), self)
|
||||
layout.addWidget(keys_group_box)
|
||||
keys_group_box_layout = QHBoxLayout()
|
||||
keys_group_box.setLayout(keys_group_box_layout)
|
||||
|
||||
|
||||
self.listy = QListWidget(self)
|
||||
self.listy.setToolTip(_('<p>Stored Ignoble keys that will be used for decryption'))
|
||||
self.listy.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
|
@ -130,7 +131,7 @@ class ConfigWidget(QWidget):
|
|||
self._add_key_button.setIcon(QIcon(I('plus.png')))
|
||||
self._add_key_button.clicked.connect(self.add_key)
|
||||
button_layout.addWidget(self._add_key_button)
|
||||
|
||||
|
||||
self._delete_key_button = QtGui.QToolButton(self)
|
||||
self._delete_key_button.setToolTip(_('Delete highlighted key'))
|
||||
self._delete_key_button.setIcon(QIcon(I('list_remove.png')))
|
||||
|
@ -142,7 +143,7 @@ class ConfigWidget(QWidget):
|
|||
self._rename_key_button.setIcon(QIcon(I('edit-select-all.png')))
|
||||
self._rename_key_button.clicked.connect(self.rename_key)
|
||||
button_layout.addWidget(self._rename_key_button)
|
||||
|
||||
|
||||
self.export_key_button = QtGui.QToolButton(self)
|
||||
self.export_key_button.setToolTip(_('Export highlighted key'))
|
||||
self.export_key_button.setIcon(QIcon(I('save.png')))
|
||||
|
@ -150,7 +151,7 @@ class ConfigWidget(QWidget):
|
|||
button_layout.addWidget(self.export_key_button)
|
||||
spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
button_layout.addItem(spacerItem)
|
||||
|
||||
|
||||
layout.addSpacing(20)
|
||||
migrate_layout = QHBoxLayout()
|
||||
layout.addLayout(migrate_layout)
|
||||
|
@ -159,7 +160,7 @@ class ConfigWidget(QWidget):
|
|||
self.migrate_btn.clicked.connect(self.migrate_wrapper)
|
||||
migrate_layout.setAlignment(Qt.AlignLeft)
|
||||
migrate_layout.addWidget(self.migrate_btn)
|
||||
|
||||
|
||||
self.resize(self.sizeHint())
|
||||
|
||||
def populate_list(self):
|
||||
|
@ -173,7 +174,7 @@ class ConfigWidget(QWidget):
|
|||
if d.result() != d.Accepted:
|
||||
# New key generation cancelled.
|
||||
return
|
||||
self.plugin_keys[d.key_name] = generate_keyfile(d.user_name, d.cc_number)
|
||||
self.plugin_keys[d.key_name] = generate_key(d.user_name, d.cc_number)
|
||||
|
||||
self.listy.clear()
|
||||
self.populate_list()
|
||||
|
@ -184,7 +185,7 @@ class ConfigWidget(QWidget):
|
|||
r = error_dialog(None, PLUGIN_NAME,
|
||||
_(errmsg), show=True, show_copy_button=False)
|
||||
return
|
||||
|
||||
|
||||
d = RenameKeyDialog(self)
|
||||
d.exec_()
|
||||
|
||||
|
@ -211,10 +212,10 @@ class ConfigWidget(QWidget):
|
|||
show_copy_button=False, default_yes=False):
|
||||
return
|
||||
del self.plugin_keys[keyname]
|
||||
|
||||
|
||||
self.listy.clear()
|
||||
self.populate_list()
|
||||
|
||||
|
||||
def help_link_activated(self, url):
|
||||
def get_help_file_resource():
|
||||
# Copy the HTML helpfile to the plugin directory each time the
|
||||
|
@ -225,7 +226,7 @@ class ConfigWidget(QWidget):
|
|||
return file_path
|
||||
url = 'file:///' + get_help_file_resource()
|
||||
open_url(QUrl(url))
|
||||
|
||||
|
||||
def save_settings(self):
|
||||
prefs['keys'] = self.plugin_keys
|
||||
if prefs['keys']:
|
||||
|
@ -301,4 +302,4 @@ class ConfigWidget(QWidget):
|
|||
if filename:
|
||||
fname = open(filename, 'w')
|
||||
fname.write(strdata)
|
||||
fname.close()
|
||||
fname.close()
|
||||
|
|
|
@ -8,8 +8,8 @@ from PyQt4.Qt import (Qt, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit,
|
|||
QGroupBox, QDialog, QDialogButtonBox)
|
||||
from calibre.gui2 import error_dialog
|
||||
|
||||
from calibre_plugins.ignoble_epub.__init__ import PLUGIN_NAME, PLUGIN_VERSION
|
||||
from calibre_plugins.ignoble_epub.utilities import uStrCmp
|
||||
from calibre_plugins.ignobleepub.__init__ import PLUGIN_NAME, PLUGIN_VERSION
|
||||
from calibre_plugins.ignobleepub.utilities import uStrCmp
|
||||
|
||||
class AddKeyDialog(QDialog):
|
||||
def __init__(self, parent=None,):
|
||||
|
@ -23,7 +23,7 @@ class AddKeyDialog(QDialog):
|
|||
layout.addWidget(data_group_box)
|
||||
data_group_box_layout = QVBoxLayout()
|
||||
data_group_box.setLayout(data_group_box_layout)
|
||||
|
||||
|
||||
key_group = QHBoxLayout()
|
||||
data_group_box_layout.addLayout(key_group)
|
||||
key_group.addWidget(QLabel('Unique Key Name:', self))
|
||||
|
@ -50,7 +50,7 @@ class AddKeyDialog(QDialog):
|
|||
name_disclaimer_label = QLabel(_('Will not be stored/saved in configuration data:'), self)
|
||||
name_disclaimer_label.setAlignment(Qt.AlignHCenter)
|
||||
data_group_box_layout.addWidget(name_disclaimer_label)
|
||||
|
||||
|
||||
ccn_group = QHBoxLayout()
|
||||
data_group_box_layout.addLayout(ccn_group)
|
||||
ccn_group.addWidget(QLabel('Credit Card#:', self))
|
||||
|
@ -103,10 +103,10 @@ class AddKeyDialog(QDialog):
|
|||
@property
|
||||
def user_name(self):
|
||||
return unicode(self.name_ledit.text().toUtf8(), 'utf8').strip().lower().replace(' ','')
|
||||
@property
|
||||
@property
|
||||
def cc_number(self):
|
||||
return unicode(self.cc_ledit.text().toUtf8(), 'utf8').strip().replace(' ', '').replace('-','')
|
||||
@property
|
||||
@property
|
||||
def key_name(self):
|
||||
return unicode(self.key_ledit.text().toUtf8(), 'utf8')
|
||||
|
||||
|
@ -122,7 +122,7 @@ class RenameKeyDialog(QDialog):
|
|||
layout.addWidget(data_group_box)
|
||||
data_group_box_layout = QVBoxLayout()
|
||||
data_group_box.setLayout(data_group_box_layout)
|
||||
|
||||
|
||||
data_group_box_layout.addWidget(QLabel('Key Name:', self))
|
||||
self.key_ledit = QLineEdit(self.parent.listy.currentItem().text(), self)
|
||||
self.key_ledit.setToolTip(_('<p>Enter a new name for this existing Ignoble key.'))
|
||||
|
@ -155,6 +155,6 @@ class RenameKeyDialog(QDialog):
|
|||
_(errmsg), show=True, show_copy_button=False)
|
||||
QDialog.accept(self)
|
||||
|
||||
@property
|
||||
@property
|
||||
def key_name(self):
|
||||
return unicode(self.key_ledit.text().toUtf8(), 'utf8')
|
||||
return unicode(self.key_ledit.text().toUtf8(), 'utf8')
|
||||
|
|
420
Calibre_Plugins/ignobleepub_plugin/ignobleepub.py
Normal file
420
Calibre_Plugins/ignobleepub_plugin/ignobleepub.py
Normal file
|
@ -0,0 +1,420 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ignobleepub.pyw, version 3.6
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# from <http://www.python.org/download/> and PyCrypto from
|
||||
# <http://www.voidspace.org.uk/python/modules.shtml#pycrypto> (make sure to
|
||||
# install the version for Python 2.6). Save this script file as
|
||||
# ineptepub.pyw and double-click on it to run it.
|
||||
#
|
||||
# Mac OS X users: Save this script file as ineptepub.pyw. You can run this
|
||||
# program from the command line (pythonw ineptepub.pyw) or by double-clicking
|
||||
# it when it has been associated with PythonLauncher.
|
||||
|
||||
# Revision history:
|
||||
# 1 - Initial release
|
||||
# 2 - Added OS X support by using OpenSSL when available
|
||||
# 3 - screen out improper key lengths to prevent segfaults on Linux
|
||||
# 3.1 - Allow Windows versions of libcrypto to be found
|
||||
# 3.2 - add support for encoding to 'utf-8' when building up list of files to cecrypt from encryption.xml
|
||||
# 3.3 - On Windows try PyCrypto first and OpenSSL next
|
||||
# 3.4 - Modify interace to allow use with import
|
||||
# 3.5 - Fix for potential problem with PyCrypto
|
||||
# 3.6 - Revised to allow use in calibre plugins to eliminate need for duplicate code
|
||||
|
||||
"""
|
||||
Decrypt Barnes & Noble encrypted ePub books.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "3.6"
|
||||
|
||||
import sys
|
||||
import os
|
||||
import traceback
|
||||
import zlib
|
||||
import zipfile
|
||||
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||
from contextlib import closing
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
try:
|
||||
from calibre.constants import iswindows, isosx
|
||||
except:
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
return [u"ineptepub.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
class IGNOBLEError(Exception):
|
||||
pass
|
||||
|
||||
def _load_crypto_libcrypto():
|
||||
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
||||
Structure, c_ulong, create_string_buffer, cast
|
||||
from ctypes.util import find_library
|
||||
|
||||
if iswindows:
|
||||
libcrypto = find_library('libeay32')
|
||||
else:
|
||||
libcrypto = find_library('crypto')
|
||||
|
||||
if libcrypto is None:
|
||||
raise IGNOBLEError('libcrypto not found')
|
||||
libcrypto = CDLL(libcrypto)
|
||||
|
||||
AES_MAXNR = 14
|
||||
|
||||
c_char_pp = POINTER(c_char_p)
|
||||
c_int_p = POINTER(c_int)
|
||||
|
||||
class AES_KEY(Structure):
|
||||
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
|
||||
('rounds', c_int)]
|
||||
AES_KEY_p = POINTER(AES_KEY)
|
||||
|
||||
def F(restype, name, argtypes):
|
||||
func = getattr(libcrypto, name)
|
||||
func.restype = restype
|
||||
func.argtypes = argtypes
|
||||
return func
|
||||
|
||||
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
|
||||
[c_char_p, c_int, AES_KEY_p])
|
||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
||||
c_int])
|
||||
|
||||
class AES(object):
|
||||
def __init__(self, userkey):
|
||||
self._blocksize = len(userkey)
|
||||
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
||||
raise IGNOBLEError('AES improper key used')
|
||||
return
|
||||
key = self._key = AES_KEY()
|
||||
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
|
||||
if rv < 0:
|
||||
raise IGNOBLEError('Failed to initialize AES key')
|
||||
|
||||
def decrypt(self, data):
|
||||
out = create_string_buffer(len(data))
|
||||
iv = ("\x00" * self._blocksize)
|
||||
rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
|
||||
if rv == 0:
|
||||
raise IGNOBLEError('AES decryption failed')
|
||||
return out.raw
|
||||
|
||||
return AES
|
||||
|
||||
def _load_crypto_pycrypto():
|
||||
from Crypto.Cipher import AES as _AES
|
||||
|
||||
class AES(object):
|
||||
def __init__(self, key):
|
||||
self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16)
|
||||
|
||||
def decrypt(self, data):
|
||||
return self._aes.decrypt(data)
|
||||
|
||||
return AES
|
||||
|
||||
def _load_crypto():
|
||||
AES = None
|
||||
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
||||
if sys.platform.startswith('win'):
|
||||
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
||||
for loader in cryptolist:
|
||||
try:
|
||||
AES = loader()
|
||||
break
|
||||
except (ImportError, IGNOBLEError):
|
||||
pass
|
||||
return AES
|
||||
|
||||
AES = _load_crypto()
|
||||
|
||||
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
|
||||
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
||||
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
||||
|
||||
class ZipInfo(zipfile.ZipInfo):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'compress_type' in kwargs:
|
||||
compress_type = kwargs.pop('compress_type')
|
||||
super(ZipInfo, self).__init__(*args, **kwargs)
|
||||
self.compress_type = compress_type
|
||||
|
||||
class Decryptor(object):
|
||||
def __init__(self, bookkey, encryption):
|
||||
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
|
||||
self._aes = AES(bookkey)
|
||||
encryption = etree.fromstring(encryption)
|
||||
self._encrypted = encrypted = set()
|
||||
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
|
||||
enc('CipherReference'))
|
||||
for elem in encryption.findall(expr):
|
||||
path = elem.get('URI', None)
|
||||
if path is not None:
|
||||
path = path.encode('utf-8')
|
||||
encrypted.add(path)
|
||||
|
||||
def decompress(self, bytes):
|
||||
dc = zlib.decompressobj(-15)
|
||||
bytes = dc.decompress(bytes)
|
||||
ex = dc.decompress('Z') + dc.flush()
|
||||
if ex:
|
||||
bytes = bytes + ex
|
||||
return bytes
|
||||
|
||||
def decrypt(self, path, data):
|
||||
if path in self._encrypted:
|
||||
data = self._aes.decrypt(data)[16:]
|
||||
data = data[:-ord(data[-1])]
|
||||
data = self.decompress(data)
|
||||
return data
|
||||
|
||||
# check file to make check whether it's probably an Adobe Adept encrypted ePub
|
||||
def ignobleBook(inpath):
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
return False
|
||||
try:
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) == 64:
|
||||
return True
|
||||
except:
|
||||
# if we couldn't check, assume it is
|
||||
return True
|
||||
return False
|
||||
|
||||
# return error code and error message duple
|
||||
def decryptBook(keyb64, inpath, outpath):
|
||||
if AES is None:
|
||||
# 1 means don't try again
|
||||
return (1, u"PyCrypto or OpenSSL must be installed.")
|
||||
key = keyb64.decode('base64')[:16]
|
||||
aes = AES(key)
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
return (1, u"Not a secure Barnes & Noble ePub.")
|
||||
for name in META_NAMES:
|
||||
namelist.remove(name)
|
||||
try:
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) != 64:
|
||||
return (1, u"Not a secure Barnes & Noble ePub.")
|
||||
bookkey = aes.decrypt(bookkey.decode('base64'))
|
||||
bookkey = bookkey[:-ord(bookkey[-1])]
|
||||
encryption = inf.read('META-INF/encryption.xml')
|
||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
||||
outf.writestr(zi, inf.read('mimetype'))
|
||||
for path in namelist:
|
||||
data = inf.read(path)
|
||||
outf.writestr(path, decryptor.decrypt(path, data))
|
||||
except Exception, e:
|
||||
return (2, u"{0}.".format(e.args[0]))
|
||||
return (0, u"Success")
|
||||
|
||||
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
if len(argv) != 4:
|
||||
print u"usage: {0} <keyfile.der> <inbook.epub> <outbook.epub>".format(progname)
|
||||
return 1
|
||||
keypath, inpath, outpath = argv[1:]
|
||||
userkey = open(keypath,'rb').read()
|
||||
result = decryptBook(userkey, inpath, outpath)
|
||||
print result[1]
|
||||
return result[0]
|
||||
|
||||
def gui_main():
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import traceback
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = Tkinter.Label(self, text=u"Select files for decryption")
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
Tkinter.Label(body, text=u"Key file").grid(row=0)
|
||||
self.keypath = Tkinter.Entry(body, width=30)
|
||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||
if os.path.exists(u"bnepubkey.b64"):
|
||||
self.keypath.insert(0, u"bnepubkey.b64")
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
Tkinter.Label(body, text=u"Input file").grid(row=1)
|
||||
self.inpath = Tkinter.Entry(body, width=30)
|
||||
self.inpath.grid(row=1, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
Tkinter.Label(body, text=u"Output file").grid(row=2)
|
||||
self.outpath = Tkinter.Entry(body, width=30)
|
||||
self.outpath.grid(row=2, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_outpath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = Tkinter.Button(
|
||||
buttons, text=u"Decrypt", width=10, command=self.decrypt)
|
||||
botton.pack(side=Tkconstants.LEFT)
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
button = Tkinter.Button(
|
||||
buttons, text=u"Quit", width=10, command=self.quit)
|
||||
button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkFileDialog.askopenfilename(
|
||||
parent=None, title=u"Select Barnes & Noble \'.b64\' key file",
|
||||
defaultextension=u".b64",
|
||||
filetypes=[('base64-encoded files', '.b64'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(keypath)
|
||||
self.keypath.delete(0, Tkconstants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
def get_inpath(self):
|
||||
inpath = tkFileDialog.askopenfilename(
|
||||
parent=None, title=u"Select B&N-encrypted ePub file to decrypt",
|
||||
defaultextension=u".epub", filetypes=[('ePub files', '.epub')])
|
||||
if inpath:
|
||||
inpath = os.path.normpath(inpath)
|
||||
self.inpath.delete(0, Tkconstants.END)
|
||||
self.inpath.insert(0, inpath)
|
||||
return
|
||||
|
||||
def get_outpath(self):
|
||||
outpath = tkFileDialog.asksaveasfilename(
|
||||
parent=None, title=u"Select unencrypted ePub file to produce",
|
||||
defaultextension=u".epub", filetypes=[('ePub files', '.epub')])
|
||||
if outpath:
|
||||
outpath = os.path.normpath(outpath)
|
||||
self.outpath.delete(0, Tkconstants.END)
|
||||
self.outpath.insert(0, outpath)
|
||||
return
|
||||
|
||||
def decrypt(self):
|
||||
keypath = self.keypath.get()
|
||||
inpath = self.inpath.get()
|
||||
outpath = self.outpath.get()
|
||||
if not keypath or not os.path.exists(keypath):
|
||||
self.status['text'] = u"Specified key file does not exist"
|
||||
return
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = u"Specified input file does not exist"
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = u"Output file not specified"
|
||||
return
|
||||
if inpath == outpath:
|
||||
self.status['text'] = u"Must have different input and output files"
|
||||
return
|
||||
userkey = open(keypath,'rb').read()
|
||||
self.status['text'] = u"Decrypting..."
|
||||
try:
|
||||
decrypt_status = decryptBook(userkey, inpath, outpath)
|
||||
except Exception, e:
|
||||
self.status['text'] = u"Error: {0}".format(e.args[0])
|
||||
return
|
||||
if decrypt_status[0] == 0:
|
||||
self.status['text'] = u"File successfully decrypted"
|
||||
else:
|
||||
self.status['text'] = decrypt_status[1]
|
||||
|
||||
root = Tkinter.Tk()
|
||||
root.title(u"Barnes & Noble ePub Decrypter v.{0}".format(__version__))
|
||||
root.resizable(True, False)
|
||||
root.minsize(300, 0)
|
||||
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
|
@ -1,13 +1,25 @@
|
|||
#! /usr/bin/python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ignoblekeygen.pyw, version 2.4
|
||||
# ignoblekeygen.pyw, version 2.5
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# To run this program install Python 2.6 from <http://www.python.org/download/>
|
||||
# and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
|
||||
# (make sure to install the version for Python 2.6). Save this script file as
|
||||
# ignoblekeygen.pyw and double-click on it to run it.
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# from <http://www.python.org/download/> and PyCrypto from
|
||||
# <http://www.voidspace.org.uk/python/modules.shtml#pycrypto> (make sure to
|
||||
# install the version for Python 2.6). Save this script file as
|
||||
# ignoblekeygen.pyw and double-click on it to run it.
|
||||
#
|
||||
# Mac OS X users: Save this script file as ignoblekeygen.pyw. You can run this
|
||||
# program from the command line (pythonw ignoblekeygen.pyw) or by double-clicking
|
||||
# it when it has been associated with PythonLauncher.
|
||||
|
||||
# Revision history:
|
||||
# 1 - Initial release
|
||||
|
@ -16,36 +28,92 @@ from __future__ import with_statement
|
|||
# 2.2 - On Windows try PyCrypto first and then OpenSSL next
|
||||
# 2.3 - Modify interface to allow use of import
|
||||
# 2.4 - Improvements to UI and now works in plugins
|
||||
# 2.5 - Additional improvement for unicode and plugin support
|
||||
|
||||
"""
|
||||
Generate Barnes & Noble EPUB user key from name and credit card number.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "2.5"
|
||||
|
||||
import sys
|
||||
import os
|
||||
import hashlib
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return [u"ignoblekeygen.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
# use openssl's libcrypt if it exists in place of pycrypto
|
||||
# code extracted from the Adobe Adept DRM removal code also by I HeartCabbages
|
||||
class IGNOBLEError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _load_crypto_libcrypto():
|
||||
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
||||
Structure, c_ulong, create_string_buffer, cast
|
||||
from ctypes.util import find_library
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
if iswindows:
|
||||
libcrypto = find_library('libeay32')
|
||||
else:
|
||||
libcrypto = find_library('crypto')
|
||||
|
||||
if libcrypto is None:
|
||||
print 'libcrypto not found'
|
||||
raise IGNOBLEError('libcrypto not found')
|
||||
libcrypto = CDLL(libcrypto)
|
||||
|
||||
|
@ -70,6 +138,7 @@ def _load_crypto_libcrypto():
|
|||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
||||
c_int])
|
||||
|
||||
class AES(object):
|
||||
def __init__(self, userkey, iv):
|
||||
self._blocksize = len(userkey)
|
||||
|
@ -88,7 +157,6 @@ def _load_crypto_libcrypto():
|
|||
|
||||
return AES
|
||||
|
||||
|
||||
def _load_crypto_pycrypto():
|
||||
from Crypto.Cipher import AES as _AES
|
||||
|
||||
|
@ -120,25 +188,28 @@ def normalize_name(name):
|
|||
return ''.join(x for x in name.lower() if x != ' ')
|
||||
|
||||
|
||||
def generate_keyfile(name, ccn, outpath):
|
||||
def generate_key(name, ccn):
|
||||
# remove spaces and case from name and CC numbers.
|
||||
if type(name)==unicode:
|
||||
name = name.encode('utf-8')
|
||||
if type(ccn)==unicode:
|
||||
ccn = ccn.encode('utf-8')
|
||||
|
||||
name = normalize_name(name) + '\x00'
|
||||
ccn = normalize_name(ccn) + '\x00'
|
||||
|
||||
|
||||
name_sha = hashlib.sha1(name).digest()[:16]
|
||||
ccn_sha = hashlib.sha1(ccn).digest()[:16]
|
||||
both_sha = hashlib.sha1(name + ccn).digest()
|
||||
aes = AES(ccn_sha, name_sha)
|
||||
crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c))
|
||||
userkey = hashlib.sha1(crypt).digest()
|
||||
with open(outpath, 'wb') as f:
|
||||
f.write(userkey.encode('base64'))
|
||||
return userkey
|
||||
return userkey.encode('base64')
|
||||
|
||||
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
if AES is None:
|
||||
print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \
|
||||
|
@ -146,10 +217,11 @@ def cli_main(argv=sys.argv):
|
|||
(progname,)
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print "usage: %s NAME CC# OUTFILE" % (progname,)
|
||||
print u"usage: {0} <Name> <CC#> <keyfileout.b64>".format(progname)
|
||||
return 1
|
||||
name, ccn, outpath = argv[1:]
|
||||
generate_keyfile(name, ccn, outpath)
|
||||
name, ccn, keypath = argv[1:]
|
||||
userkey = generate_key(name, ccn)
|
||||
open(keypath,'wb').write(userkey)
|
||||
return 0
|
||||
|
||||
|
||||
|
@ -162,38 +234,38 @@ def gui_main():
|
|||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = Tkinter.Label(self, text='Enter parameters')
|
||||
self.status = Tkinter.Label(self, text=u"Enter parameters")
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
Tkinter.Label(body, text='Account Name').grid(row=0)
|
||||
Tkinter.Label(body, text=u"Account Name").grid(row=0)
|
||||
self.name = Tkinter.Entry(body, width=40)
|
||||
self.name.grid(row=0, column=1, sticky=sticky)
|
||||
Tkinter.Label(body, text='CC#').grid(row=1)
|
||||
Tkinter.Label(body, text=u"CC#").grid(row=1)
|
||||
self.ccn = Tkinter.Entry(body, width=40)
|
||||
self.ccn.grid(row=1, column=1, sticky=sticky)
|
||||
Tkinter.Label(body, text='Output file').grid(row=2)
|
||||
Tkinter.Label(body, text=u"Output file").grid(row=2)
|
||||
self.keypath = Tkinter.Entry(body, width=40)
|
||||
self.keypath.grid(row=2, column=1, sticky=sticky)
|
||||
self.keypath.insert(2, 'bnepubkey.b64')
|
||||
button = Tkinter.Button(body, text="...", command=self.get_keypath)
|
||||
self.keypath.insert(2, u"bnepubkey.b64")
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = Tkinter.Button(
|
||||
buttons, text="Generate", width=10, command=self.generate)
|
||||
buttons, text=u"Generate", width=10, command=self.generate)
|
||||
botton.pack(side=Tkconstants.LEFT)
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
button = Tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quit)
|
||||
buttons, text=u"Quit", width=10, command=self.quit)
|
||||
button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkFileDialog.asksaveasfilename(
|
||||
parent=None, title='Select B&N EPUB key file to produce',
|
||||
defaultextension='.b64',
|
||||
parent=None, title=u"Select B&N ePub key file to produce",
|
||||
defaultextension=u".b64",
|
||||
filetypes=[('base64-encoded files', '.b64'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
|
@ -201,27 +273,28 @@ def gui_main():
|
|||
self.keypath.delete(0, Tkconstants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
|
||||
def generate(self):
|
||||
name = self.name.get()
|
||||
ccn = self.ccn.get()
|
||||
keypath = self.keypath.get()
|
||||
if not name:
|
||||
self.status['text'] = 'Name not specified'
|
||||
self.status['text'] = u"Name not specified"
|
||||
return
|
||||
if not ccn:
|
||||
self.status['text'] = 'Credit card number not specified'
|
||||
self.status['text'] = u"Credit card number not specified"
|
||||
return
|
||||
if not keypath:
|
||||
self.status['text'] = 'Output keyfile path not specified'
|
||||
self.status['text'] = u"Output keyfile path not specified"
|
||||
return
|
||||
self.status['text'] = 'Generating...'
|
||||
self.status['text'] = u"Generating..."
|
||||
try:
|
||||
generate_keyfile(name, ccn, keypath)
|
||||
userkey = generate_key(name, ccn)
|
||||
except Exception, e:
|
||||
self.status['text'] = 'Error: ' + str(e)
|
||||
self.status['text'] = u"Error: (0}".format(e.args[0])
|
||||
return
|
||||
self.status['text'] = 'Keyfile successfully generated'
|
||||
open(keypath,'wb').write(userkey)
|
||||
self.status['text'] = u"Keyfile successfully generated"
|
||||
|
||||
root = Tkinter.Tk()
|
||||
if AES is None:
|
||||
|
@ -231,7 +304,7 @@ def gui_main():
|
|||
"This script requires OpenSSL or PyCrypto, which must be installed "
|
||||
"separately. Read the top-of-script comment for details.")
|
||||
return 1
|
||||
root.title('Ignoble EPUB Keyfile Generator')
|
||||
root.title(u"Barnes & Noble ePub Keyfile Generator v.{0}".format(__version__))
|
||||
root.resizable(True, False)
|
||||
root.minsize(300, 0)
|
||||
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||
|
@ -240,5 +313,7 @@ def gui_main():
|
|||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
|
@ -1,45 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Adapted and simplified from the kitchen project
|
||||
#
|
||||
# Kitchen Project Copyright (c) 2012 Red Hat, Inc.
|
||||
#
|
||||
# kitchen is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# kitchen is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with kitchen; if not, see <http://www.gnu.org/licenses/>
|
||||
#
|
||||
# Authors:
|
||||
# Toshio Kuratomi <toshio@fedoraproject.org>
|
||||
# Seth Vidal
|
||||
#
|
||||
# Portions of code taken from yum/i18n.py and
|
||||
# python-fedora: fedora/textutils.py
|
||||
|
||||
import codecs
|
||||
|
||||
# returns a char string unchanged
|
||||
# returns a unicode string converted to a char string of the passed encoding
|
||||
# return the empty string for anything else
|
||||
def getwriter(encoding):
|
||||
class _StreamWriter(codecs.StreamWriter):
|
||||
def __init__(self, stream):
|
||||
codecs.StreamWriter.__init__(self, stream, 'replace')
|
||||
|
||||
def encode(self, msg, errors='replace'):
|
||||
if isinstance(msg, basestring):
|
||||
if isinstance(msg, str):
|
||||
return (msg, len(msg))
|
||||
return (msg.encode(self.encoding, 'replace'), len(msg))
|
||||
return ('',0)
|
||||
|
||||
_StreamWriter.encoding = encoding
|
||||
return _StreamWriter
|
|
@ -1,18 +1,10 @@
|
|||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
import hashlib
|
||||
|
||||
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
||||
Structure, c_ulong, create_string_buffer, cast
|
||||
from ctypes.util import find_library
|
||||
|
||||
from calibre.constants import iswindows
|
||||
from calibre_plugins.ignoble_epub.__init__ import PLUGIN_NAME, PLUGIN_VERSION
|
||||
|
||||
DETAILED_MESSAGE = \
|
||||
'You have personal information stored in this plugin\'s customization '+ \
|
||||
'string from a previous version of this plugin.\n\n'+ \
|
||||
|
@ -25,99 +17,6 @@ DETAILED_MESSAGE = \
|
|||
'this new version of the plugin will not be responsible for storing that personal '+ \
|
||||
'info in plain sight any longer.'
|
||||
|
||||
class IGNOBLEError(Exception):
|
||||
pass
|
||||
|
||||
def normalize_name(name): # Strip spaces and convert to lowercase.
|
||||
return ''.join(x for x in name.lower() if x != ' ')
|
||||
|
||||
# These are the key ENCRYPTING aes crypto functions
|
||||
def generate_keyfile(name, ccn):
|
||||
# Load the necessary crypto libs.
|
||||
AES = _load_crypto()
|
||||
name = normalize_name(name) + '\x00'
|
||||
ccn = ccn + '\x00'
|
||||
name_sha = hashlib.sha1(name).digest()[:16]
|
||||
ccn_sha = hashlib.sha1(ccn).digest()[:16]
|
||||
both_sha = hashlib.sha1(name + ccn).digest()
|
||||
aes = AES(ccn_sha, name_sha)
|
||||
crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c))
|
||||
userkey = hashlib.sha1(crypt).digest()
|
||||
|
||||
return userkey.encode('base64')
|
||||
|
||||
def _load_crypto_libcrypto():
|
||||
if iswindows:
|
||||
libcrypto = find_library('libeay32')
|
||||
else:
|
||||
libcrypto = find_library('crypto')
|
||||
if libcrypto is None:
|
||||
raise IGNOBLEError('libcrypto not found')
|
||||
libcrypto = CDLL(libcrypto)
|
||||
|
||||
AES_MAXNR = 14
|
||||
|
||||
c_char_pp = POINTER(c_char_p)
|
||||
c_int_p = POINTER(c_int)
|
||||
|
||||
class AES_KEY(Structure):
|
||||
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
|
||||
('rounds', c_int)]
|
||||
AES_KEY_p = POINTER(AES_KEY)
|
||||
|
||||
def F(restype, name, argtypes):
|
||||
func = getattr(libcrypto, name)
|
||||
func.restype = restype
|
||||
func.argtypes = argtypes
|
||||
return func
|
||||
|
||||
AES_set_encrypt_key = F(c_int, 'AES_set_encrypt_key',
|
||||
[c_char_p, c_int, AES_KEY_p])
|
||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
||||
c_int])
|
||||
|
||||
class AES(object):
|
||||
def __init__(self, userkey, iv):
|
||||
self._blocksize = len(userkey)
|
||||
self._iv = iv
|
||||
key = self._key = AES_KEY()
|
||||
rv = AES_set_encrypt_key(userkey, len(userkey) * 8, key)
|
||||
if rv < 0:
|
||||
raise IGNOBLEError('Failed to initialize AES Encrypt key')
|
||||
|
||||
def encrypt(self, data):
|
||||
out = create_string_buffer(len(data))
|
||||
rv = AES_cbc_encrypt(data, out, len(data), self._key, self._iv, 1)
|
||||
if rv == 0:
|
||||
raise IGNOBLEError('AES encryption failed')
|
||||
return out.raw
|
||||
return AES
|
||||
|
||||
def _load_crypto_pycrypto():
|
||||
from Crypto.Cipher import AES as _AES
|
||||
|
||||
class AES(object):
|
||||
def __init__(self, key, iv):
|
||||
self._aes = _AES.new(key, _AES.MODE_CBC, iv)
|
||||
|
||||
def encrypt(self, data):
|
||||
return self._aes.encrypt(data)
|
||||
return AES
|
||||
|
||||
def _load_crypto():
|
||||
_aes = None
|
||||
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
||||
if iswindows:
|
||||
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
||||
for loader in cryptolist:
|
||||
try:
|
||||
_aes = loader()
|
||||
break
|
||||
except (ImportError, IGNOBLEError):
|
||||
pass
|
||||
return _aes
|
||||
|
||||
def uStrCmp (s1, s2, caseless=False):
|
||||
import unicodedata as ud
|
||||
str1 = s1 if isinstance(s1, unicode) else unicode(s1)
|
||||
|
@ -133,8 +32,8 @@ def parseCustString(keystuff):
|
|||
for i in ar:
|
||||
try:
|
||||
name, ccn = i.split(',')
|
||||
# Generate Barnes & Noble EPUB user key from name and credit card number.
|
||||
userkeys.append(generate_key(name, ccn))
|
||||
except:
|
||||
return False
|
||||
# Generate Barnes & Noble EPUB user key from name and credit card number.
|
||||
userkeys.append(generate_keyfile(name, ccn))
|
||||
return userkeys
|
||||
pass
|
||||
return userkeys
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import zlib
|
||||
|
|
Binary file not shown.
|
@ -1,35 +1,37 @@
|
|||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
__license__ = 'GPL v3'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
|
||||
# ineptepub_plugin.py
|
||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||
# later. <http://www.gnu.org/licenses/>
|
||||
#
|
||||
# Requires Calibre version 0.7.55 or higher.
|
||||
#
|
||||
# All credit given to I <3 Cabbages for the original standalone scripts.
|
||||
# I had the much easier job of converting them to a Calibre plugin.
|
||||
# All credit given to i♥cabbages for the original standalone scripts.
|
||||
# I had the much easier job of converting them to a calibre plugin.
|
||||
#
|
||||
# This plugin is meant to decrypt Adobe Digital Edition Epubs that are protected
|
||||
# with Adobe's Adept encryption. It is meant to function without having to install
|
||||
# any dependencies... other than having Calibre installed, of course. It will still
|
||||
# any dependencies... other than having calibre installed, of course. It will still
|
||||
# work if you have Python and PyCrypto already installed, but they aren't necessary.
|
||||
#
|
||||
# Configuration:
|
||||
# When first run, the plugin will attempt to find your Adobe Digital Editions installation
|
||||
# (on Windows and Mac OS's). If successful, it will create an 'adeptkey.der' file and
|
||||
# save it in Calibre's configuration directory. It will use that file on subsequent runs.
|
||||
# If there are already '*.der' files in the directory, the plugin won't attempt to
|
||||
# find the ADE installation. So if you have ADE installed on the same machine as Calibre...
|
||||
# you are ready to go.
|
||||
# (on Windows and Mac OS's). If successful, it will create one or more
|
||||
# 'calibre-adeptkey<n>.der' files and save them in calibre's configuration directory.
|
||||
# It will use those files on subsequent runs. If there is already a 'calibre-adeptkey*.der'
|
||||
# file in the directory, the plugin won't attempt to find the ADE installation.
|
||||
# So if you have ADE installed on the same machine as calibre you are ready to go.
|
||||
#
|
||||
# If you already have keyfiles generated with I <3 Cabbages' ineptkey.pyw script,
|
||||
# If you already have keyfiles generated with i♥cabbages' ineptkey.pyw script,
|
||||
# you can put those keyfiles in Calibre's configuration directory. The easiest
|
||||
# way to find the correct directory is to go to Calibre's Preferences page... click
|
||||
# on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre
|
||||
# configuration directory' button. Paste your keyfiles in there. Just make sure that
|
||||
# configuration directory' button. Copy your keyfiles in there. Just make sure that
|
||||
# they have different names and are saved with the '.der' extension (like the ineptkey
|
||||
# script produces). This directory isn't touched when upgrading Calibre, so it's quite
|
||||
# safe to leave them there.
|
||||
|
@ -55,447 +57,157 @@ from __future__ import with_statement
|
|||
# 0.1.7 - update to new calibre plugin interface
|
||||
# 0.1.8 - Fix for potential problem with PyCrypto
|
||||
# 0.1.9 - Fix for potential problem with ADE keys and fix possible output/unicode problem
|
||||
# 0.2.0 - Major code change to use unaltered ineptepub.py file 5.8 or later.
|
||||
|
||||
"""
|
||||
Decrypt Adobe ADEPT-encrypted EPUB books.
|
||||
"""
|
||||
|
||||
PLUGIN_NAME = 'Inept Epub DeDRM'
|
||||
PLUGIN_VERSION_TUPLE = (0, 1, 9)
|
||||
PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE])
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
import sys
|
||||
import os
|
||||
import zlib
|
||||
import zipfile
|
||||
import re
|
||||
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||
from contextlib import closing
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
global AES
|
||||
global RSA
|
||||
|
||||
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
|
||||
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
||||
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
||||
PLUGIN_NAME = u"Inept Epub DeDRM"
|
||||
PLUGIN_VERSION_TUPLE = (0, 2, 0)
|
||||
PLUGIN_VERSION = u'.'.join([str(x) for x in PLUGIN_VERSION_TUPLE])
|
||||
|
||||
import sys, os, re
|
||||
|
||||
class ADEPTError(Exception):
|
||||
pass
|
||||
|
||||
def _load_crypto_libcrypto():
|
||||
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
||||
Structure, c_ulong, create_string_buffer, cast
|
||||
from ctypes.util import find_library
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
libcrypto = find_library('libeay32')
|
||||
else:
|
||||
libcrypto = find_library('crypto')
|
||||
if libcrypto is None:
|
||||
raise ADEPTError('%s Plugin v%s: libcrypto not found' % (PLUGIN_NAME, PLUGIN_VERSION))
|
||||
libcrypto = CDLL(libcrypto)
|
||||
|
||||
RSA_NO_PADDING = 3
|
||||
AES_MAXNR = 14
|
||||
|
||||
c_char_pp = POINTER(c_char_p)
|
||||
c_int_p = POINTER(c_int)
|
||||
|
||||
class RSA(Structure):
|
||||
pass
|
||||
RSA_p = POINTER(RSA)
|
||||
|
||||
class AES_KEY(Structure):
|
||||
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
|
||||
('rounds', c_int)]
|
||||
AES_KEY_p = POINTER(AES_KEY)
|
||||
|
||||
def F(restype, name, argtypes):
|
||||
func = getattr(libcrypto, name)
|
||||
func.restype = restype
|
||||
func.argtypes = argtypes
|
||||
return func
|
||||
|
||||
d2i_RSAPrivateKey = F(RSA_p, 'd2i_RSAPrivateKey',
|
||||
[RSA_p, c_char_pp, c_long])
|
||||
RSA_size = F(c_int, 'RSA_size', [RSA_p])
|
||||
RSA_private_decrypt = F(c_int, 'RSA_private_decrypt',
|
||||
[c_int, c_char_p, c_char_p, RSA_p, c_int])
|
||||
RSA_free = F(None, 'RSA_free', [RSA_p])
|
||||
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
|
||||
[c_char_p, c_int, AES_KEY_p])
|
||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
||||
c_int])
|
||||
|
||||
class RSA(object):
|
||||
def __init__(self, der):
|
||||
buf = create_string_buffer(der)
|
||||
pp = c_char_pp(cast(buf, c_char_p))
|
||||
rsa = self._rsa = d2i_RSAPrivateKey(None, pp, len(der))
|
||||
if rsa is None:
|
||||
raise ADEPTError('Error parsing ADEPT user key DER')
|
||||
|
||||
def decrypt(self, from_):
|
||||
rsa = self._rsa
|
||||
to = create_string_buffer(RSA_size(rsa))
|
||||
dlen = RSA_private_decrypt(len(from_), from_, to, rsa,
|
||||
RSA_NO_PADDING)
|
||||
if dlen < 0:
|
||||
raise ADEPTError('RSA decryption failed')
|
||||
return to[:dlen]
|
||||
|
||||
def __del__(self):
|
||||
if self._rsa is not None:
|
||||
RSA_free(self._rsa)
|
||||
self._rsa = None
|
||||
|
||||
class AES(object):
|
||||
def __init__(self, userkey):
|
||||
self._blocksize = len(userkey)
|
||||
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
||||
raise ADEPTError('AES improper key used')
|
||||
return
|
||||
key = self._key = AES_KEY()
|
||||
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
|
||||
if rv < 0:
|
||||
raise ADEPTError('Failed to initialize AES key')
|
||||
|
||||
def decrypt(self, data):
|
||||
out = create_string_buffer(len(data))
|
||||
iv = ("\x00" * self._blocksize)
|
||||
rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
|
||||
if rv == 0:
|
||||
raise ADEPTError('AES decryption failed')
|
||||
return out.raw
|
||||
print 'IneptEpub: Using libcrypto.'
|
||||
return (AES, RSA)
|
||||
|
||||
def _load_crypto_pycrypto():
|
||||
from Crypto.Cipher import AES as _AES
|
||||
from Crypto.PublicKey import RSA as _RSA
|
||||
|
||||
# ASN.1 parsing code from tlslite
|
||||
class ASN1Error(Exception):
|
||||
pass
|
||||
|
||||
class ASN1Parser(object):
|
||||
class Parser(object):
|
||||
def __init__(self, bytes):
|
||||
self.bytes = bytes
|
||||
self.index = 0
|
||||
|
||||
def get(self, length):
|
||||
if self.index + length > len(self.bytes):
|
||||
raise ASN1Error("Error decoding ASN.1")
|
||||
x = 0
|
||||
for count in range(length):
|
||||
x <<= 8
|
||||
x |= self.bytes[self.index]
|
||||
self.index += 1
|
||||
return x
|
||||
|
||||
def getFixBytes(self, lengthBytes):
|
||||
bytes = self.bytes[self.index : self.index+lengthBytes]
|
||||
self.index += lengthBytes
|
||||
return bytes
|
||||
|
||||
def getVarBytes(self, lengthLength):
|
||||
lengthBytes = self.get(lengthLength)
|
||||
return self.getFixBytes(lengthBytes)
|
||||
|
||||
def getFixList(self, length, lengthList):
|
||||
l = [0] * lengthList
|
||||
for x in range(lengthList):
|
||||
l[x] = self.get(length)
|
||||
return l
|
||||
|
||||
def getVarList(self, length, lengthLength):
|
||||
lengthList = self.get(lengthLength)
|
||||
if lengthList % length != 0:
|
||||
raise ASN1Error("Error decoding ASN.1")
|
||||
lengthList = int(lengthList/length)
|
||||
l = [0] * lengthList
|
||||
for x in range(lengthList):
|
||||
l[x] = self.get(length)
|
||||
return l
|
||||
|
||||
def startLengthCheck(self, lengthLength):
|
||||
self.lengthCheck = self.get(lengthLength)
|
||||
self.indexCheck = self.index
|
||||
|
||||
def setLengthCheck(self, length):
|
||||
self.lengthCheck = length
|
||||
self.indexCheck = self.index
|
||||
|
||||
def stopLengthCheck(self):
|
||||
if (self.index - self.indexCheck) != self.lengthCheck:
|
||||
raise ASN1Error("Error decoding ASN.1")
|
||||
|
||||
def atLengthCheck(self):
|
||||
if (self.index - self.indexCheck) < self.lengthCheck:
|
||||
return False
|
||||
elif (self.index - self.indexCheck) == self.lengthCheck:
|
||||
return True
|
||||
else:
|
||||
raise ASN1Error("Error decoding ASN.1")
|
||||
|
||||
def __init__(self, bytes):
|
||||
p = self.Parser(bytes)
|
||||
p.get(1)
|
||||
self.length = self._getASN1Length(p)
|
||||
self.value = p.getFixBytes(self.length)
|
||||
|
||||
def getChild(self, which):
|
||||
p = self.Parser(self.value)
|
||||
for x in range(which+1):
|
||||
markIndex = p.index
|
||||
p.get(1)
|
||||
length = self._getASN1Length(p)
|
||||
p.getFixBytes(length)
|
||||
return ASN1Parser(p.bytes[markIndex:p.index])
|
||||
|
||||
def _getASN1Length(self, p):
|
||||
firstLength = p.get(1)
|
||||
if firstLength<=127:
|
||||
return firstLength
|
||||
else:
|
||||
lengthLength = firstLength & 0x7F
|
||||
return p.get(lengthLength)
|
||||
|
||||
class AES(object):
|
||||
def __init__(self, key):
|
||||
self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16)
|
||||
|
||||
def decrypt(self, data):
|
||||
return self._aes.decrypt(data)
|
||||
|
||||
class RSA(object):
|
||||
def __init__(self, der):
|
||||
key = ASN1Parser([ord(x) for x in der])
|
||||
key = [key.getChild(x).value for x in xrange(1, 4)]
|
||||
key = [self.bytesToNumber(v) for v in key]
|
||||
self._rsa = _RSA.construct(key)
|
||||
|
||||
def bytesToNumber(self, bytes):
|
||||
total = 0L
|
||||
for byte in bytes:
|
||||
total = (total << 8) + byte
|
||||
return total
|
||||
|
||||
def decrypt(self, data):
|
||||
return self._rsa.decrypt(data)
|
||||
print 'IneptEpub: Using pycrypto.'
|
||||
return (AES, RSA)
|
||||
|
||||
def _load_crypto():
|
||||
_aes = _rsa = None
|
||||
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
||||
if sys.platform.startswith('win'):
|
||||
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
||||
for loader in cryptolist:
|
||||
try:
|
||||
_aes, _rsa = loader()
|
||||
break
|
||||
except (ImportError, ADEPTError):
|
||||
pass
|
||||
return (_aes, _rsa)
|
||||
|
||||
class ZipInfo(zipfile.ZipInfo):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'compress_type' in kwargs:
|
||||
compress_type = kwargs.pop('compress_type')
|
||||
super(ZipInfo, self).__init__(*args, **kwargs)
|
||||
self.compress_type = compress_type
|
||||
|
||||
class Decryptor(object):
|
||||
def __init__(self, bookkey, encryption):
|
||||
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
|
||||
self._aes = AES(bookkey)
|
||||
encryption = etree.fromstring(encryption)
|
||||
self._encrypted = encrypted = set()
|
||||
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
|
||||
enc('CipherReference'))
|
||||
for elem in encryption.findall(expr):
|
||||
path = elem.get('URI', None)
|
||||
path = path.encode('utf-8')
|
||||
if path is not None:
|
||||
encrypted.add(path)
|
||||
|
||||
def decompress(self, bytes):
|
||||
dc = zlib.decompressobj(-15)
|
||||
bytes = dc.decompress(bytes)
|
||||
ex = dc.decompress('Z') + dc.flush()
|
||||
if ex:
|
||||
bytes = bytes + ex
|
||||
return bytes
|
||||
|
||||
def decrypt(self, path, data):
|
||||
if path in self._encrypted:
|
||||
data = self._aes.decrypt(data)[16:]
|
||||
data = data[:-ord(data[-1])]
|
||||
data = self.decompress(data)
|
||||
return data
|
||||
|
||||
def plugin_main(userkey, inpath, outpath):
|
||||
rsa = RSA(userkey)
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
return 1
|
||||
for name in META_NAMES:
|
||||
namelist.remove(name)
|
||||
try:
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
bookkey = rsa.decrypt(bookkey.decode('base64'))
|
||||
# Padded as per RSAES-PKCS1-v1_5
|
||||
if bookkey[-17] != '\x00':
|
||||
raise ADEPTError('problem decrypting session key')
|
||||
encryption = inf.read('META-INF/encryption.xml')
|
||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
||||
outf.writestr(zi, inf.read('mimetype'))
|
||||
for path in namelist:
|
||||
data = inf.read(path)
|
||||
outf.writestr(path, decryptor.decrypt(path, data))
|
||||
except:
|
||||
return 2
|
||||
return 0
|
||||
|
||||
from calibre.customize import FileTypePlugin
|
||||
from calibre.constants import iswindows, isosx
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
|
||||
class IneptDeDRM(FileTypePlugin):
|
||||
name = PLUGIN_NAME
|
||||
description = 'Removes DRM from secure Adobe epub files. \
|
||||
Credit given to I <3 Cabbages for the original stand-alone scripts.'
|
||||
description = u"Removes DRM from secure Adobe epub files. Credit given to i♥cabbages for the original stand-alone scripts."
|
||||
supported_platforms = ['linux', 'osx', 'windows']
|
||||
author = 'DiapDealer'
|
||||
author = u"DiapDealer, Apprentice Alf and i♥cabbages"
|
||||
version = PLUGIN_VERSION_TUPLE
|
||||
minimum_calibre_version = (0, 7, 55) # Compiled python libraries cannot be imported in earlier versions.
|
||||
file_types = set(['epub'])
|
||||
on_import = True
|
||||
priority = 100
|
||||
|
||||
def run(self, path_to_ebook):
|
||||
from calibre_plugins.ineptepub import outputfix
|
||||
|
||||
if sys.stdout.encoding == None:
|
||||
sys.stdout = outputfix.getwriter('utf-8')(sys.stdout)
|
||||
else:
|
||||
sys.stdout = outputfix.getwriter(sys.stdout.encoding)(sys.stdout)
|
||||
if sys.stderr.encoding == None:
|
||||
sys.stderr = outputfix.getwriter('utf-8')(sys.stderr)
|
||||
else:
|
||||
sys.stderr = outputfix.getwriter(sys.stderr.encoding)(sys.stderr)
|
||||
|
||||
global AES
|
||||
global RSA
|
||||
|
||||
AES, RSA = _load_crypto()
|
||||
|
||||
if AES == None or RSA == None:
|
||||
# Failed to load libcrypto or PyCrypto... Adobe Epubs can\'t be decrypted.'
|
||||
raise ADEPTError('IneptEpub: Failed to load crypto libs... Adobe Epubs can\'t be decrypted.')
|
||||
def run(self, path_to_ebook):
|
||||
|
||||
# make sure any unicode output gets converted safely with 'replace'
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
|
||||
print u"{0} v{1}: Trying to decrypt {2}.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))
|
||||
|
||||
# Create a TemporaryPersistent file to work with.
|
||||
# Check original epub archive for zip errors.
|
||||
from calibre_plugins.ineptepub import zipfix
|
||||
inf = self.temporary_file(u".epub")
|
||||
try:
|
||||
print u"{0} v{1}: Verifying zip archive integrity.".format(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
fr = zipfix.fixZip(path_to_ebook, inf.name)
|
||||
fr.fix()
|
||||
except Exception, e:
|
||||
print u"{0} v{1}: Error when checking zip archive.".format(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
raise Exception(e)
|
||||
return
|
||||
|
||||
|
||||
#check the book
|
||||
from calibre_plugins.ineptepub import ineptepub
|
||||
if not ineptepub.adeptBook(inf.name):
|
||||
print u"{0} v{1}: {2} is not a secure Adobe Adept ePub.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))
|
||||
# return the original file, so that no error message is generated in the GUI
|
||||
return path_to_ebook
|
||||
|
||||
# Load any keyfiles (*.der) included Calibre's config directory.
|
||||
userkeys = []
|
||||
|
||||
# Find Calibre's configuration directory.
|
||||
# self.plugin_path is passed in unicode because we defined our name in unicode
|
||||
confpath = os.path.split(os.path.split(self.plugin_path)[0])[0]
|
||||
print 'IneptEpub: Calibre configuration directory = %s' % confpath
|
||||
print u"{0} v{1}: Calibre configuration directory = {2}".format(PLUGIN_NAME, PLUGIN_VERSION, confpath)
|
||||
files = os.listdir(confpath)
|
||||
filefilter = re.compile("\.der$", re.IGNORECASE)
|
||||
filefilter = re.compile(u"\.der$", re.IGNORECASE)
|
||||
files = filter(filefilter.search, files)
|
||||
foundDefault = False
|
||||
|
||||
if files:
|
||||
try:
|
||||
for filename in files:
|
||||
if filename[:16] == 'calibre-adeptkey':
|
||||
if filename[:16] == u"calibre-adeptkey":
|
||||
foundDefault = True
|
||||
fpath = os.path.join(confpath, filename)
|
||||
with open(fpath, 'rb') as f:
|
||||
userkeys.append(f.read())
|
||||
print 'IneptEpub: Keyfile %s found in config folder.' % filename
|
||||
userkeys.append([f.read(), filename])
|
||||
print u"{0} v{1}: Keyfile {2} found in config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, filename)
|
||||
except IOError:
|
||||
print 'IneptEpub: Error reading keyfiles from config directory.'
|
||||
print u"{0} v{1}: Error reading keyfiles from config directory.".format(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
pass
|
||||
|
||||
|
||||
if not foundDefault:
|
||||
# Try to find key from ADE install and save the key in
|
||||
# Calibre's configuration directory for future use.
|
||||
if iswindows or isosx:
|
||||
#ignore annoying future warning from key generation
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore', category=FutureWarning)
|
||||
|
||||
# ADE key retrieval script included in respective OS folder.
|
||||
from calibre_plugins.ineptepub.ineptkey import retrieve_keys
|
||||
try:
|
||||
keys = retrieve_keys()
|
||||
for i,key in enumerate(keys):
|
||||
userkeys.append(key)
|
||||
keypath = os.path.join(confpath, 'calibre-adeptkey{0:d}.der'.format(i))
|
||||
keyname = u"calibre-adeptkey{0:d}.der".format(i)
|
||||
userkeys.append([key,keyname])
|
||||
keypath = os.path.join(confpath, keyname)
|
||||
open(keypath, 'wb').write(key)
|
||||
print 'IneptEpub: Created keyfile %s from ADE install.' % keypath
|
||||
print u"{0} v{1}: Created keyfile {2} from ADE install.".format(PLUGIN_NAME, PLUGIN_VERSION, keyname)
|
||||
except:
|
||||
print 'IneptEpub: Couldn\'t Retrieve key from ADE install.'
|
||||
print u"{0} v{1}: Couldn\'t Retrieve key from ADE install.".format(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
pass
|
||||
|
||||
if not userkeys:
|
||||
# No user keys found... bail out.
|
||||
raise ADEPTError('IneptEpub - No keys found. Check keyfile(s)/ADE install')
|
||||
raise ADEPTError(u"{0} v{1}: No keys found. Check keyfile(s)/ADE install".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
return
|
||||
|
||||
|
||||
# Attempt to decrypt epub with each encryption key found.
|
||||
for userkey in userkeys:
|
||||
# Create a TemporaryPersistent file to work with.
|
||||
# Check original epub archive for zip errors.
|
||||
from calibre_plugins.ineptepub import zipfix
|
||||
inf = self.temporary_file('.epub')
|
||||
try:
|
||||
print '%s Plugin: Verifying zip archive integrity.' % PLUGIN_NAME
|
||||
fr = zipfix.fixZip(path_to_ebook, inf.name)
|
||||
fr.fix()
|
||||
except Exception, e:
|
||||
print '%s Plugin: unforeseen zip archive issue.' % PLUGIN_NAME
|
||||
raise Exception(e)
|
||||
return
|
||||
of = self.temporary_file('.epub')
|
||||
|
||||
# Give the user key, ebook and TemporaryPersistent file to the plugin_main function.
|
||||
result = plugin_main(userkey, inf.name, of.name)
|
||||
|
||||
for userkeyinfo in userkeys:
|
||||
print u"{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, userkeyinfo[1])
|
||||
of = self.temporary_file(u".epub")
|
||||
|
||||
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
|
||||
result = ineptepub.decryptBook(userkeyinfo[0], inf.name, of.name)
|
||||
|
||||
# Ebook is not an Adobe Adept epub... do nothing and pass it on.
|
||||
# This allows a non-encrypted epub to be imported without error messages.
|
||||
if result == 1:
|
||||
print 'IneptEpub: Not an Adobe Adept Epub... punting.'
|
||||
print u"{0} v{1}: {2} is not a secure Adobe Adept ePub.".format(PLUGIN_NAME, PLUGIN_VERSION,os.path.basename(path_to_ebook))
|
||||
of.close()
|
||||
return path_to_ebook
|
||||
break
|
||||
|
||||
|
||||
# Decryption was successful return the modified PersistentTemporary
|
||||
# file to Calibre's import process.
|
||||
if result == 0:
|
||||
print 'IneptEpub: Encryption successfully removed.'
|
||||
of.close
|
||||
print u"{0} v{1}: Encryption successfully removed.".format(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
of.close()
|
||||
return of.name
|
||||
break
|
||||
|
||||
print 'IneptEpub: Encryption key invalid... trying others.'
|
||||
of.close()
|
||||
|
||||
|
||||
print u"{0} v{1}: Encryption key incorrect.".format(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
of.close
|
||||
|
||||
# Something went wrong with decryption.
|
||||
# Import the original unmolested epub.
|
||||
of.close
|
||||
raise ADEPTError('IneptEpub - Ultimately failed to decrypt')
|
||||
raise ADEPTError(u"{0} v{1}: Ultimately failed to decrypt".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
return
|
||||
|
||||
|
|
|
@ -3,11 +3,13 @@
|
|||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ineptepub.pyw, version 5.7
|
||||
# Copyright © 2009-2010 i♥cabbages
|
||||
# ineptepub.pyw, version 5.8
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||
# later. <http://www.gnu.org/licenses/>
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# from <http://www.python.org/download/> and PyCrypto from
|
||||
|
@ -31,24 +33,83 @@ from __future__ import with_statement
|
|||
# 5.5 - On Windows try PyCrypto first, OpenSSL next
|
||||
# 5.6 - Modify interface to allow use with import
|
||||
# 5.7 - Fix for potential problem with PyCrypto
|
||||
# 5.8 - Revised to allow use in calibre plugins to eliminate need for duplicate code
|
||||
|
||||
"""
|
||||
Decrypt Adobe ADEPT-encrypted EPUB books.
|
||||
Decrypt Adobe Digital Editions encrypted ePub books.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "5.8"
|
||||
|
||||
import sys
|
||||
import os
|
||||
import traceback
|
||||
import zlib
|
||||
import zipfile
|
||||
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||
from contextlib import closing
|
||||
import xml.etree.ElementTree as etree
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
try:
|
||||
from calibre.constants import iswindows, isosx
|
||||
except:
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
return [u"ineptepub.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
class ADEPTError(Exception):
|
||||
pass
|
||||
|
@ -58,7 +119,7 @@ def _load_crypto_libcrypto():
|
|||
Structure, c_ulong, create_string_buffer, cast
|
||||
from ctypes.util import find_library
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
if iswindows:
|
||||
libcrypto = find_library('libeay32')
|
||||
else:
|
||||
libcrypto = find_library('crypto')
|
||||
|
@ -272,6 +333,7 @@ def _load_crypto():
|
|||
except (ImportError, ADEPTError):
|
||||
pass
|
||||
return (AES, RSA)
|
||||
|
||||
AES, RSA = _load_crypto()
|
||||
|
||||
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
|
||||
|
@ -314,158 +376,181 @@ class Decryptor(object):
|
|||
data = self.decompress(data)
|
||||
return data
|
||||
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = Tkinter.Label(self, text='Select files for decryption')
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
Tkinter.Label(body, text='Key file').grid(row=0)
|
||||
self.keypath = Tkinter.Entry(body, width=30)
|
||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||
if os.path.exists('adeptkey.der'):
|
||||
self.keypath.insert(0, 'adeptkey.der')
|
||||
button = Tkinter.Button(body, text="...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
Tkinter.Label(body, text='Input file').grid(row=1)
|
||||
self.inpath = Tkinter.Entry(body, width=30)
|
||||
self.inpath.grid(row=1, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
Tkinter.Label(body, text='Output file').grid(row=2)
|
||||
self.outpath = Tkinter.Entry(body, width=30)
|
||||
self.outpath.grid(row=2, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_outpath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = Tkinter.Button(
|
||||
buttons, text="Decrypt", width=10, command=self.decrypt)
|
||||
botton.pack(side=Tkconstants.LEFT)
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
button = Tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quit)
|
||||
button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkFileDialog.askopenfilename(
|
||||
parent=None, title='Select ADEPT key file',
|
||||
defaultextension='.der', filetypes=[('DER-encoded files', '.der'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(keypath)
|
||||
self.keypath.delete(0, Tkconstants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
def get_inpath(self):
|
||||
inpath = tkFileDialog.askopenfilename(
|
||||
parent=None, title='Select ADEPT-encrypted EPUB file to decrypt',
|
||||
defaultextension='.epub', filetypes=[('EPUB files', '.epub'),
|
||||
('All files', '.*')])
|
||||
if inpath:
|
||||
inpath = os.path.normpath(inpath)
|
||||
self.inpath.delete(0, Tkconstants.END)
|
||||
self.inpath.insert(0, inpath)
|
||||
return
|
||||
|
||||
def get_outpath(self):
|
||||
outpath = tkFileDialog.asksaveasfilename(
|
||||
parent=None, title='Select unencrypted EPUB file to produce',
|
||||
defaultextension='.epub', filetypes=[('EPUB files', '.epub'),
|
||||
('All files', '.*')])
|
||||
if outpath:
|
||||
outpath = os.path.normpath(outpath)
|
||||
self.outpath.delete(0, Tkconstants.END)
|
||||
self.outpath.insert(0, outpath)
|
||||
return
|
||||
|
||||
def decrypt(self):
|
||||
keypath = self.keypath.get()
|
||||
inpath = self.inpath.get()
|
||||
outpath = self.outpath.get()
|
||||
if not keypath or not os.path.exists(keypath):
|
||||
self.status['text'] = 'Specified key file does not exist'
|
||||
return
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = 'Specified input file does not exist'
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = 'Output file not specified'
|
||||
return
|
||||
if inpath == outpath:
|
||||
self.status['text'] = 'Must have different input and output files'
|
||||
return
|
||||
argv = [sys.argv[0], keypath, inpath, outpath]
|
||||
self.status['text'] = 'Decrypting...'
|
||||
try:
|
||||
cli_main(argv)
|
||||
except Exception, e:
|
||||
self.status['text'] = 'Error: ' + str(e)
|
||||
return
|
||||
self.status['text'] = 'File successfully decrypted'
|
||||
|
||||
|
||||
def decryptBook(keypath, inpath, outpath):
|
||||
with open(keypath, 'rb') as f:
|
||||
keyder = f.read()
|
||||
rsa = RSA(keyder)
|
||||
# check file to make check whether it's probably an Adobe Adept encrypted ePub
|
||||
def adeptBook(inpath):
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
raise ADEPTError('%s: not an ADEPT EPUB' % (inpath,))
|
||||
return False
|
||||
try:
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) == 172:
|
||||
return True
|
||||
except:
|
||||
# if we couldn't check, assume it is
|
||||
return True
|
||||
return False
|
||||
|
||||
def decryptBook(userkey, inpath, outpath):
|
||||
if AES is None:
|
||||
raise ADEPTError(u"PyCrypto or OpenSSL must be installed.")
|
||||
rsa = RSA(userkey)
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
print u"{0:s} is DRM-free.".format(os.path.basename(inpath))
|
||||
return 1
|
||||
for name in META_NAMES:
|
||||
namelist.remove(name)
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
bookkey = rsa.decrypt(bookkey.decode('base64'))
|
||||
# Padded as per RSAES-PKCS1-v1_5
|
||||
if bookkey[-17] != '\x00':
|
||||
raise ADEPTError('problem decrypting session key')
|
||||
encryption = inf.read('META-INF/encryption.xml')
|
||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
||||
outf.writestr(zi, inf.read('mimetype'))
|
||||
for path in namelist:
|
||||
data = inf.read(path)
|
||||
outf.writestr(path, decryptor.decrypt(path, data))
|
||||
try:
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) != 172:
|
||||
print u"{0:s} is not a secure Adobe Adept ePub.".format(os.path.basename(inpath))
|
||||
return 1
|
||||
bookkey = rsa.decrypt(bookkey.decode('base64'))
|
||||
# Padded as per RSAES-PKCS1-v1_5
|
||||
if bookkey[-17] != '\x00':
|
||||
print u"Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath))
|
||||
return 2
|
||||
encryption = inf.read('META-INF/encryption.xml')
|
||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
||||
outf.writestr(zi, inf.read('mimetype'))
|
||||
for path in namelist:
|
||||
data = inf.read(path)
|
||||
outf.writestr(path, decryptor.decrypt(path, data))
|
||||
except:
|
||||
print u"Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc())
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
if AES is None:
|
||||
print "%s: This script requires OpenSSL or PyCrypto, which must be" \
|
||||
" installed separately. Read the top-of-script comment for" \
|
||||
" details." % (progname,)
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,)
|
||||
print u"usage: {0} <keyfile.der> <inbook.epub> <outbook.epub>".format(progname)
|
||||
return 1
|
||||
keypath, inpath, outpath = argv[1:]
|
||||
return decryptBook(keypath, inpath, outpath)
|
||||
|
||||
userkey = open(keypath,'rb').read()
|
||||
result = decryptBook(userkey, inpath, outpath)
|
||||
if result == 0:
|
||||
print u"Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath))
|
||||
return result
|
||||
|
||||
def gui_main():
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import traceback
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = Tkinter.Label(self, text=u"Select files for decryption")
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
Tkinter.Label(body, text=u"Key file").grid(row=0)
|
||||
self.keypath = Tkinter.Entry(body, width=30)
|
||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||
if os.path.exists(u"adeptkey.der"):
|
||||
self.keypath.insert(0, u"adeptkey.der")
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
Tkinter.Label(body, text=u"Input file").grid(row=1)
|
||||
self.inpath = Tkinter.Entry(body, width=30)
|
||||
self.inpath.grid(row=1, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
Tkinter.Label(body, text=u"Output file").grid(row=2)
|
||||
self.outpath = Tkinter.Entry(body, width=30)
|
||||
self.outpath.grid(row=2, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_outpath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = Tkinter.Button(
|
||||
buttons, text=u"Decrypt", width=10, command=self.decrypt)
|
||||
botton.pack(side=Tkconstants.LEFT)
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
button = Tkinter.Button(
|
||||
buttons, text=u"Quit", width=10, command=self.quit)
|
||||
button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkFileDialog.askopenfilename(
|
||||
parent=None, title=u"Select Adobe Adept \'.der\' key file",
|
||||
defaultextension=u".der",
|
||||
filetypes=[('Adobe Adept DER-encoded files', '.der'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(keypath)
|
||||
self.keypath.delete(0, Tkconstants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
def get_inpath(self):
|
||||
inpath = tkFileDialog.askopenfilename(
|
||||
parent=None, title=u"Select ADEPT-encrypted ePub file to decrypt",
|
||||
defaultextension=u".epub", filetypes=[('ePub files', '.epub')])
|
||||
if inpath:
|
||||
inpath = os.path.normpath(inpath)
|
||||
self.inpath.delete(0, Tkconstants.END)
|
||||
self.inpath.insert(0, inpath)
|
||||
return
|
||||
|
||||
def get_outpath(self):
|
||||
outpath = tkFileDialog.asksaveasfilename(
|
||||
parent=None, title=u"Select unencrypted ePub file to produce",
|
||||
defaultextension=u".epub", filetypes=[('ePub files', '.epub')])
|
||||
if outpath:
|
||||
outpath = os.path.normpath(outpath)
|
||||
self.outpath.delete(0, Tkconstants.END)
|
||||
self.outpath.insert(0, outpath)
|
||||
return
|
||||
|
||||
def decrypt(self):
|
||||
keypath = self.keypath.get()
|
||||
inpath = self.inpath.get()
|
||||
outpath = self.outpath.get()
|
||||
if not keypath or not os.path.exists(keypath):
|
||||
self.status['text'] = u"Specified key file does not exist"
|
||||
return
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = u"Specified input file does not exist"
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = u"Output file not specified"
|
||||
return
|
||||
if inpath == outpath:
|
||||
self.status['text'] = u"Must have different input and output files"
|
||||
return
|
||||
userkey = open(keypath,'rb').read()
|
||||
self.status['text'] = u"Decrypting..."
|
||||
try:
|
||||
decrypt_status = decryptBook(userkey, inpath, outpath)
|
||||
except Exception, e:
|
||||
self.status['text'] = u"Error; {0}".format(e)
|
||||
return
|
||||
if decrypt_status == 0:
|
||||
self.status['text'] = u"File successfully decrypted"
|
||||
else:
|
||||
self.status['text'] = u"The was an error decrypting the file."
|
||||
|
||||
root = Tkinter.Tk()
|
||||
if AES is None:
|
||||
root.withdraw()
|
||||
tkMessageBox.showerror(
|
||||
"INEPT EPUB Decrypter",
|
||||
"This script requires OpenSSL or PyCrypto, which must be"
|
||||
" installed separately. Read the top-of-script comment for"
|
||||
" details.")
|
||||
return 1
|
||||
root.title('INEPT EPUB Decrypter')
|
||||
root.title(u"Adobe Adept ePub Decrypter v.{0}".format(__version__))
|
||||
root.resizable(True, False)
|
||||
root.minsize(300, 0)
|
||||
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||
|
@ -474,5 +559,7 @@ def gui_main():
|
|||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
|
@ -6,8 +6,8 @@ from __future__ import with_statement
|
|||
# ineptkey.pyw, version 5.6
|
||||
# Copyright © 2009-2010 i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||
# later. <http://www.gnu.org/licenses/>
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# from <http://www.python.org/download/> and PyCrypto from
|
||||
|
@ -37,7 +37,7 @@ from __future__ import with_statement
|
|||
# 5.3 - On Windows try PyCrypto first, OpenSSL next
|
||||
# 5.4 - Modify interface to allow use of import
|
||||
# 5.5 - Fix for potential problem with PyCrypto
|
||||
# 5.6 - Revise to allow use in Plugins to eliminate need for duplicate code
|
||||
# 5.6 - Revised to allow use in Plugins to eliminate need for duplicate code
|
||||
|
||||
"""
|
||||
Retrieve Adobe ADEPT user key.
|
||||
|
@ -49,12 +49,65 @@ import sys
|
|||
import os
|
||||
import struct
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
try:
|
||||
from calibre.constants import iswindows, isosx
|
||||
except:
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
return [u"ineptkey.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
class ADEPTError(Exception):
|
||||
pass
|
||||
|
||||
|
@ -80,13 +133,13 @@ if iswindows:
|
|||
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
|
||||
('rounds', c_int)]
|
||||
AES_KEY_p = POINTER(AES_KEY)
|
||||
|
||||
|
||||
def F(restype, name, argtypes):
|
||||
func = getattr(libcrypto, name)
|
||||
func.restype = restype
|
||||
func.argtypes = argtypes
|
||||
return func
|
||||
|
||||
|
||||
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
|
||||
[c_char_p, c_int, AES_KEY_p])
|
||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||
|
@ -308,9 +361,9 @@ if iswindows:
|
|||
cuser = winreg.HKEY_CURRENT_USER
|
||||
try:
|
||||
regkey = winreg.OpenKey(cuser, DEVICE_KEY_PATH)
|
||||
device = winreg.QueryValueEx(regkey, 'key')[0]
|
||||
except WindowsError:
|
||||
raise ADEPTError("Adobe Digital Editions not activated")
|
||||
device = winreg.QueryValueEx(regkey, 'key')[0]
|
||||
keykey = CryptUnprotectData(device, entropy)
|
||||
userkey = None
|
||||
keys = []
|
||||
|
@ -343,7 +396,7 @@ if iswindows:
|
|||
if len(keys) == 0:
|
||||
raise ADEPTError('Could not locate privateLicenseKey')
|
||||
return keys
|
||||
|
||||
|
||||
|
||||
elif isosx:
|
||||
import xml.etree.ElementTree as etree
|
||||
|
@ -386,7 +439,7 @@ else:
|
|||
def retrieve_keys(keypath):
|
||||
raise ADEPTError("This script only supports Windows and Mac OS X.")
|
||||
return []
|
||||
|
||||
|
||||
def retrieve_key(keypath):
|
||||
keys = retrieve_keys()
|
||||
with open(keypath, 'wb') as f:
|
||||
|
@ -397,22 +450,22 @@ def extractKeyfile(keypath):
|
|||
try:
|
||||
success = retrieve_key(keypath)
|
||||
except ADEPTError, e:
|
||||
print "Key generation Error: " + str(e)
|
||||
print u"Key generation Error: {0}".format(e.args[0])
|
||||
return 1
|
||||
except Exception, e:
|
||||
print "General Error: " + str(e)
|
||||
print "General Error: {0}".format(e.args[0])
|
||||
return 1
|
||||
if not success:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
def cli_main(argv=unicode_argv()):
|
||||
keypath = argv[1]
|
||||
return extractKeyfile(keypath)
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
def gui_main(argv=unicode_argv()):
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkMessageBox
|
||||
|
@ -421,24 +474,24 @@ def main(argv=sys.argv):
|
|||
class ExceptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root, text):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
label = Tkinter.Label(self, text="Unexpected error:",
|
||||
label = Tkinter.Label(self, text=u"Unexpected error:",
|
||||
anchor=Tkconstants.W, justify=Tkconstants.LEFT)
|
||||
label.pack(fill=Tkconstants.X, expand=0)
|
||||
self.text = Tkinter.Text(self)
|
||||
self.text.pack(fill=Tkconstants.BOTH, expand=1)
|
||||
|
||||
|
||||
self.text.insert(Tkconstants.END, text)
|
||||
|
||||
|
||||
root = Tkinter.Tk()
|
||||
root.withdraw()
|
||||
progname = os.path.basename(argv[0])
|
||||
keypath = os.path.abspath("adeptkey.der")
|
||||
keypath, progname = os.path.split(argv[0])
|
||||
keypath = os.path.join(keypath, u"adeptkey.der")
|
||||
success = False
|
||||
try:
|
||||
success = retrieve_key(keypath)
|
||||
except ADEPTError, e:
|
||||
tkMessageBox.showerror("ADEPT Key", "Error: " + str(e))
|
||||
tkMessageBox.showerror(u"ADEPT Key", "Error: {0}".format(e.args[0]))
|
||||
except Exception:
|
||||
root.wm_state('normal')
|
||||
root.title('ADEPT Key')
|
||||
|
@ -448,10 +501,12 @@ def main(argv=sys.argv):
|
|||
if not success:
|
||||
return 1
|
||||
tkMessageBox.showinfo(
|
||||
"ADEPT Key", "Key successfully retrieved to %s" % (keypath))
|
||||
u"ADEPT Key", u"Key successfully retrieved to {0}".format(keypath))
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
sys.exit(main())
|
||||
sys.exit(gui_main())
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Adapted and simplified from the kitchen project
|
||||
#
|
||||
# Kitchen Project Copyright (c) 2012 Red Hat, Inc.
|
||||
#
|
||||
# kitchen is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# kitchen is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with kitchen; if not, see <http://www.gnu.org/licenses/>
|
||||
#
|
||||
# Authors:
|
||||
# Toshio Kuratomi <toshio@fedoraproject.org>
|
||||
# Seth Vidal
|
||||
#
|
||||
# Portions of code taken from yum/i18n.py and
|
||||
# python-fedora: fedora/textutils.py
|
||||
|
||||
import codecs
|
||||
|
||||
# returns a char string unchanged
|
||||
# returns a unicode string converted to a char string of the passed encoding
|
||||
# return the empty string for anything else
|
||||
def getwriter(encoding):
|
||||
class _StreamWriter(codecs.StreamWriter):
|
||||
def __init__(self, stream):
|
||||
codecs.StreamWriter.__init__(self, stream, 'replace')
|
||||
|
||||
def encode(self, msg, errors='replace'):
|
||||
if isinstance(msg, basestring):
|
||||
if isinstance(msg, str):
|
||||
return (msg, len(msg))
|
||||
return (msg.encode(self.encoding, 'replace'), len(msg))
|
||||
return ('',0)
|
||||
|
||||
_StreamWriter.encoding = encoding
|
||||
return _StreamWriter
|
|
@ -1,4 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import zlib
|
||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load diff
|
@ -6,8 +6,8 @@ from __future__ import with_statement
|
|||
# ineptkey.pyw, version 5.6
|
||||
# Copyright © 2009-2010 i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||
# later. <http://www.gnu.org/licenses/>
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# from <http://www.python.org/download/> and PyCrypto from
|
||||
|
@ -37,7 +37,7 @@ from __future__ import with_statement
|
|||
# 5.3 - On Windows try PyCrypto first, OpenSSL next
|
||||
# 5.4 - Modify interface to allow use of import
|
||||
# 5.5 - Fix for potential problem with PyCrypto
|
||||
# 5.6 - Revise to allow use in Plugins to eliminate need for duplicate code
|
||||
# 5.6 - Revised to allow use in Plugins to eliminate need for duplicate code
|
||||
|
||||
"""
|
||||
Retrieve Adobe ADEPT user key.
|
||||
|
@ -49,12 +49,65 @@ import sys
|
|||
import os
|
||||
import struct
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
try:
|
||||
from calibre.constants import iswindows, isosx
|
||||
except:
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
return [u"ineptkey.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
class ADEPTError(Exception):
|
||||
pass
|
||||
|
||||
|
@ -80,13 +133,13 @@ if iswindows:
|
|||
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
|
||||
('rounds', c_int)]
|
||||
AES_KEY_p = POINTER(AES_KEY)
|
||||
|
||||
|
||||
def F(restype, name, argtypes):
|
||||
func = getattr(libcrypto, name)
|
||||
func.restype = restype
|
||||
func.argtypes = argtypes
|
||||
return func
|
||||
|
||||
|
||||
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
|
||||
[c_char_p, c_int, AES_KEY_p])
|
||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||
|
@ -308,9 +361,9 @@ if iswindows:
|
|||
cuser = winreg.HKEY_CURRENT_USER
|
||||
try:
|
||||
regkey = winreg.OpenKey(cuser, DEVICE_KEY_PATH)
|
||||
device = winreg.QueryValueEx(regkey, 'key')[0]
|
||||
except WindowsError:
|
||||
raise ADEPTError("Adobe Digital Editions not activated")
|
||||
device = winreg.QueryValueEx(regkey, 'key')[0]
|
||||
keykey = CryptUnprotectData(device, entropy)
|
||||
userkey = None
|
||||
keys = []
|
||||
|
@ -343,7 +396,7 @@ if iswindows:
|
|||
if len(keys) == 0:
|
||||
raise ADEPTError('Could not locate privateLicenseKey')
|
||||
return keys
|
||||
|
||||
|
||||
|
||||
elif isosx:
|
||||
import xml.etree.ElementTree as etree
|
||||
|
@ -386,7 +439,7 @@ else:
|
|||
def retrieve_keys(keypath):
|
||||
raise ADEPTError("This script only supports Windows and Mac OS X.")
|
||||
return []
|
||||
|
||||
|
||||
def retrieve_key(keypath):
|
||||
keys = retrieve_keys()
|
||||
with open(keypath, 'wb') as f:
|
||||
|
@ -397,22 +450,22 @@ def extractKeyfile(keypath):
|
|||
try:
|
||||
success = retrieve_key(keypath)
|
||||
except ADEPTError, e:
|
||||
print "Key generation Error: " + str(e)
|
||||
print u"Key generation Error: {0}".format(e.args[0])
|
||||
return 1
|
||||
except Exception, e:
|
||||
print "General Error: " + str(e)
|
||||
print "General Error: {0}".format(e.args[0])
|
||||
return 1
|
||||
if not success:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
def cli_main(argv=unicode_argv()):
|
||||
keypath = argv[1]
|
||||
return extractKeyfile(keypath)
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
def gui_main(argv=unicode_argv()):
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkMessageBox
|
||||
|
@ -421,24 +474,24 @@ def main(argv=sys.argv):
|
|||
class ExceptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root, text):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
label = Tkinter.Label(self, text="Unexpected error:",
|
||||
label = Tkinter.Label(self, text=u"Unexpected error:",
|
||||
anchor=Tkconstants.W, justify=Tkconstants.LEFT)
|
||||
label.pack(fill=Tkconstants.X, expand=0)
|
||||
self.text = Tkinter.Text(self)
|
||||
self.text.pack(fill=Tkconstants.BOTH, expand=1)
|
||||
|
||||
|
||||
self.text.insert(Tkconstants.END, text)
|
||||
|
||||
|
||||
root = Tkinter.Tk()
|
||||
root.withdraw()
|
||||
progname = os.path.basename(argv[0])
|
||||
keypath = os.path.abspath("adeptkey.der")
|
||||
keypath, progname = os.path.split(argv[0])
|
||||
keypath = os.path.join(keypath, u"adeptkey.der")
|
||||
success = False
|
||||
try:
|
||||
success = retrieve_key(keypath)
|
||||
except ADEPTError, e:
|
||||
tkMessageBox.showerror("ADEPT Key", "Error: " + str(e))
|
||||
tkMessageBox.showerror(u"ADEPT Key", "Error: {0}".format(e.args[0]))
|
||||
except Exception:
|
||||
root.wm_state('normal')
|
||||
root.title('ADEPT Key')
|
||||
|
@ -448,10 +501,12 @@ def main(argv=sys.argv):
|
|||
if not success:
|
||||
return 1
|
||||
tkMessageBox.showinfo(
|
||||
"ADEPT Key", "Key successfully retrieved to %s" % (keypath))
|
||||
u"ADEPT Key", u"Key successfully retrieved to {0}".format(keypath))
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
sys.exit(main())
|
||||
sys.exit(gui_main())
|
||||
|
|
|
@ -1,13 +1,25 @@
|
|||
#! /usr/bin/env python
|
||||
# ineptpdf.pyw, version 7.11
|
||||
#! /usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# To run this program install Python 2.6 from http://www.python.org/download/
|
||||
# and OpenSSL (already installed on Mac OS X and Linux) OR
|
||||
# PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
|
||||
# (make sure to install the version for Python 2.6). Save this script file as
|
||||
# ineptpdf.pyw and double-click on it to run it.
|
||||
# ineptpdf.pyw, version 7.11
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# from <http://www.python.org/download/> and PyCrypto from
|
||||
# <http://www.voidspace.org.uk/python/modules.shtml#pycrypto> (make sure to
|
||||
# install the version for Python 2.6). Save this script file as
|
||||
# ineptepub.pyw and double-click on it to run it.
|
||||
#
|
||||
# Mac OS X users: Save this script file as ineptepub.pyw. You can run this
|
||||
# program from the command line (pythonw ineptepub.pyw) or by double-clicking
|
||||
# it when it has been associated with PythonLauncher.
|
||||
|
||||
# Revision history:
|
||||
# 1 - Initial release
|
||||
|
@ -36,12 +48,14 @@ from __future__ import with_statement
|
|||
# 7.9 - Bug fix for some session key errors when len(bookkey) > length required
|
||||
# 7.10 - Various tweaks to fix minor problems.
|
||||
# 7.11 - More tweaks to fix minor problems.
|
||||
# 7.12 - Revised to allow use in calibre plugins to eliminate need for duplicate code
|
||||
|
||||
"""
|
||||
Decrypts Adobe ADEPT-encrypted PDF files.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "7.12"
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
@ -51,10 +65,63 @@ import struct
|
|||
import hashlib
|
||||
from itertools import chain, islice
|
||||
import xml.etree.ElementTree as etree
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
return [u"ineptepub.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
class ADEPTError(Exception):
|
||||
pass
|
||||
|
@ -1520,9 +1587,7 @@ class PDFDocument(object):
|
|||
|
||||
def initialize_ebx(self, password, docid, param):
|
||||
self.is_printable = self.is_modifiable = self.is_extractable = True
|
||||
with open(password, 'rb') as f:
|
||||
keyder = f.read()
|
||||
rsa = RSA(keyder)
|
||||
rsa = RSA(password)
|
||||
length = int_value(param.get('Length', 0)) / 8
|
||||
rights = str_value(param.get('ADEPT_LICENSE')).decode('base64')
|
||||
rights = zlib.decompress(rights, -15)
|
||||
|
@ -1907,14 +1972,14 @@ class PDFObjStrmParser(PDFParser):
|
|||
### My own code, for which there is none else to blame
|
||||
|
||||
class PDFSerializer(object):
|
||||
def __init__(self, inf, keypath):
|
||||
def __init__(self, inf, userkey):
|
||||
global GEN_XREF_STM, gen_xref_stm
|
||||
gen_xref_stm = GEN_XREF_STM > 1
|
||||
self.version = inf.read(8)
|
||||
inf.seek(0)
|
||||
self.doc = doc = PDFDocument()
|
||||
parser = PDFParser(doc, inf)
|
||||
doc.initialize(keypath)
|
||||
doc.initialize(userkey)
|
||||
self.objids = objids = set()
|
||||
for xref in reversed(doc.xrefs):
|
||||
trailer = xref.trailer
|
||||
|
@ -2097,142 +2162,144 @@ class PDFSerializer(object):
|
|||
self.write('endobj\n')
|
||||
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
ltext='Select file for decryption\n'
|
||||
self.status = Tkinter.Label(self, text=ltext)
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
Tkinter.Label(body, text='Key file').grid(row=0)
|
||||
self.keypath = Tkinter.Entry(body, width=30)
|
||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||
if os.path.exists('adeptkey.der'):
|
||||
self.keypath.insert(0, 'adeptkey.der')
|
||||
button = Tkinter.Button(body, text="...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
Tkinter.Label(body, text='Input file').grid(row=1)
|
||||
self.inpath = Tkinter.Entry(body, width=30)
|
||||
self.inpath.grid(row=1, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
Tkinter.Label(body, text='Output file').grid(row=2)
|
||||
self.outpath = Tkinter.Entry(body, width=30)
|
||||
self.outpath.grid(row=2, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_outpath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
|
||||
|
||||
botton = Tkinter.Button(
|
||||
buttons, text="Decrypt", width=10, command=self.decrypt)
|
||||
botton.pack(side=Tkconstants.LEFT)
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
button = Tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quit)
|
||||
button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkFileDialog.askopenfilename(
|
||||
parent=None, title='Select ADEPT key file',
|
||||
defaultextension='.der', filetypes=[('DER-encoded files', '.der'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(os.path.realpath(keypath))
|
||||
self.keypath.delete(0, Tkconstants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
def get_inpath(self):
|
||||
inpath = tkFileDialog.askopenfilename(
|
||||
parent=None, title='Select ADEPT encrypted PDF file to decrypt',
|
||||
defaultextension='.pdf', filetypes=[('PDF files', '.pdf'),
|
||||
('All files', '.*')])
|
||||
if inpath:
|
||||
inpath = os.path.normpath(os.path.realpath(inpath))
|
||||
self.inpath.delete(0, Tkconstants.END)
|
||||
self.inpath.insert(0, inpath)
|
||||
return
|
||||
|
||||
def get_outpath(self):
|
||||
outpath = tkFileDialog.asksaveasfilename(
|
||||
parent=None, title='Select unencrypted PDF file to produce',
|
||||
defaultextension='.pdf', filetypes=[('PDF files', '.pdf'),
|
||||
('All files', '.*')])
|
||||
if outpath:
|
||||
outpath = os.path.normpath(os.path.realpath(outpath))
|
||||
self.outpath.delete(0, Tkconstants.END)
|
||||
self.outpath.insert(0, outpath)
|
||||
return
|
||||
|
||||
def decrypt(self):
|
||||
keypath = self.keypath.get()
|
||||
inpath = self.inpath.get()
|
||||
outpath = self.outpath.get()
|
||||
if not keypath or not os.path.exists(keypath):
|
||||
# keyfile doesn't exist
|
||||
self.status['text'] = 'Specified Adept key file does not exist'
|
||||
return
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = 'Specified input file does not exist'
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = 'Output file not specified'
|
||||
return
|
||||
if inpath == outpath:
|
||||
self.status['text'] = 'Must have different input and output files'
|
||||
return
|
||||
# patch for non-ascii characters
|
||||
argv = [sys.argv[0], keypath, inpath, outpath]
|
||||
self.status['text'] = 'Processing ...'
|
||||
try:
|
||||
cli_main(argv)
|
||||
except Exception, a:
|
||||
self.status['text'] = 'Error: ' + str(a)
|
||||
return
|
||||
self.status['text'] = 'File successfully decrypted.\n'+\
|
||||
'Close this window or decrypt another pdf file.'
|
||||
return
|
||||
|
||||
|
||||
def decryptBook(keypath, inpath, outpath):
|
||||
def decryptBook(userkey, inpath, outpath):
|
||||
if RSA is None:
|
||||
raise ADEPTError(u"PyCrypto or OpenSSL must be installed.")
|
||||
with open(inpath, 'rb') as inf:
|
||||
try:
|
||||
serializer = PDFSerializer(inf, keypath)
|
||||
serializer = PDFSerializer(inf, userkey)
|
||||
except:
|
||||
print "Error serializing pdf. Probably wrong key."
|
||||
return 1
|
||||
print u"Error serializing pdf {0}. Probably wrong key.".format(os.path.basename(inpath))
|
||||
return 2
|
||||
# hope this will fix the 'bad file descriptor' problem
|
||||
with open(outpath, 'wb') as outf:
|
||||
# help construct to make sure the method runs to the end
|
||||
# help construct to make sure the method runs to the end
|
||||
try:
|
||||
serializer.dump(outf)
|
||||
except:
|
||||
print "error writing pdf."
|
||||
return 1
|
||||
except Exception, e:
|
||||
print u"error writing pdf: {0}".format(e.args[0])
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
if RSA is None:
|
||||
print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \
|
||||
"separately. Read the top-of-script comment for details." % \
|
||||
(progname,)
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,)
|
||||
print u"usage: {0} <keyfile.der> <inbook.pdf> <outbook.pdf>".format(progname)
|
||||
return 1
|
||||
keypath, inpath, outpath = argv[1:]
|
||||
return decryptBook(keypath, inpath, outpath)
|
||||
userkey = open(keypath,'rb').read()
|
||||
result = decryptBook(userkey, inpath, outpath)
|
||||
if result == 0:
|
||||
print u"Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath))
|
||||
return result
|
||||
|
||||
|
||||
def gui_main():
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = Tkinter.Label(self, text=u"Select files for decryption")
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
Tkinter.Label(body, text=u"Key file").grid(row=0)
|
||||
self.keypath = Tkinter.Entry(body, width=30)
|
||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||
if os.path.exists(u"adeptkey.der"):
|
||||
self.keypath.insert(0, u"adeptkey.der")
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
Tkinter.Label(body, text=u"Input file").grid(row=1)
|
||||
self.inpath = Tkinter.Entry(body, width=30)
|
||||
self.inpath.grid(row=1, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
Tkinter.Label(body, text=u"Output file").grid(row=2)
|
||||
self.outpath = Tkinter.Entry(body, width=30)
|
||||
self.outpath.grid(row=2, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_outpath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = Tkinter.Button(
|
||||
buttons, text=u"Decrypt", width=10, command=self.decrypt)
|
||||
botton.pack(side=Tkconstants.LEFT)
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
button = Tkinter.Button(
|
||||
buttons, text=u"Quit", width=10, command=self.quit)
|
||||
button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkFileDialog.askopenfilename(
|
||||
parent=None, title=u"Select Adobe Adept \'.der\' key file",
|
||||
defaultextension=u".der",
|
||||
filetypes=[('Adobe Adept DER-encoded files', '.der'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(keypath)
|
||||
self.keypath.delete(0, Tkconstants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
def get_inpath(self):
|
||||
inpath = tkFileDialog.askopenfilename(
|
||||
parent=None, title=u"Select ADEPT-encrypted PDF file to decrypt",
|
||||
defaultextension=u".pdf", filetypes=[('PDF files', '.pdf')])
|
||||
if inpath:
|
||||
inpath = os.path.normpath(inpath)
|
||||
self.inpath.delete(0, Tkconstants.END)
|
||||
self.inpath.insert(0, inpath)
|
||||
return
|
||||
|
||||
def get_outpath(self):
|
||||
outpath = tkFileDialog.asksaveasfilename(
|
||||
parent=None, title=u"Select unencrypted PDF file to produce",
|
||||
defaultextension=u".pdf", filetypes=[('PDF files', '.pdf')])
|
||||
if outpath:
|
||||
outpath = os.path.normpath(outpath)
|
||||
self.outpath.delete(0, Tkconstants.END)
|
||||
self.outpath.insert(0, outpath)
|
||||
return
|
||||
|
||||
def decrypt(self):
|
||||
keypath = self.keypath.get()
|
||||
inpath = self.inpath.get()
|
||||
outpath = self.outpath.get()
|
||||
if not keypath or not os.path.exists(keypath):
|
||||
self.status['text'] = u"Specified key file does not exist"
|
||||
return
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = u"Specified input file does not exist"
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = u"Output file not specified"
|
||||
return
|
||||
if inpath == outpath:
|
||||
self.status['text'] = u"Must have different input and output files"
|
||||
return
|
||||
userkey = open(keypath,'rb').read()
|
||||
self.status['text'] = u"Decrypting..."
|
||||
try:
|
||||
decrypt_status = decryptBook(userkey, inpath, outpath)
|
||||
except Exception, e:
|
||||
self.status['text'] = u"Error; {0}".format(e.args[0])
|
||||
return
|
||||
if decrypt_status == 0:
|
||||
self.status['text'] = u"File successfully decrypted"
|
||||
else:
|
||||
self.status['text'] = u"The was an error decrypting the file."
|
||||
|
||||
|
||||
root = Tkinter.Tk()
|
||||
if RSA is None:
|
||||
root.withdraw()
|
||||
|
@ -2241,7 +2308,7 @@ def gui_main():
|
|||
"This script requires OpenSSL or PyCrypto, which must be installed "
|
||||
"separately. Read the top-of-script comment for details.")
|
||||
return 1
|
||||
root.title('INEPT PDF Decrypter')
|
||||
root.title(u"Adobe Adept PDF Decrypter v.{0}".format(__version__))
|
||||
root.resizable(True, False)
|
||||
root.minsize(370, 0)
|
||||
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||
|
@ -2251,5 +2318,7 @@ def gui_main():
|
|||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
|
@ -1,45 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Adapted and simplified from the kitchen project
|
||||
#
|
||||
# Kitchen Project Copyright (c) 2012 Red Hat, Inc.
|
||||
#
|
||||
# kitchen is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# kitchen is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with kitchen; if not, see <http://www.gnu.org/licenses/>
|
||||
#
|
||||
# Authors:
|
||||
# Toshio Kuratomi <toshio@fedoraproject.org>
|
||||
# Seth Vidal
|
||||
#
|
||||
# Portions of code taken from yum/i18n.py and
|
||||
# python-fedora: fedora/textutils.py
|
||||
|
||||
import codecs
|
||||
|
||||
# returns a char string unchanged
|
||||
# returns a unicode string converted to a char string of the passed encoding
|
||||
# return the empty string for anything else
|
||||
def getwriter(encoding):
|
||||
class _StreamWriter(codecs.StreamWriter):
|
||||
def __init__(self, stream):
|
||||
codecs.StreamWriter.__init__(self, stream, 'replace')
|
||||
|
||||
def encode(self, msg, errors='replace'):
|
||||
if isinstance(msg, basestring):
|
||||
if isinstance(msg, str):
|
||||
return (msg, len(msg))
|
||||
return (msg.encode(self.encoding, 'replace'), len(msg))
|
||||
return ('',0)
|
||||
|
||||
_StreamWriter.encoding = encoding
|
||||
return _StreamWriter
|
Binary file not shown.
|
@ -1,3 +1,6 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# standlone set of Mac OSX specific routines needed for KindleBooks
|
||||
|
||||
from __future__ import with_statement
|
||||
|
@ -22,7 +25,7 @@ def _load_crypto_libcrypto():
|
|||
|
||||
libcrypto = find_library('crypto')
|
||||
if libcrypto is None:
|
||||
raise DrmException('libcrypto not found')
|
||||
raise DrmException(u"libcrypto not found")
|
||||
libcrypto = CDLL(libcrypto)
|
||||
|
||||
# From OpenSSL's crypto aes header
|
||||
|
@ -80,14 +83,14 @@ def _load_crypto_libcrypto():
|
|||
def set_decrypt_key(self, userkey, iv):
|
||||
self._blocksize = len(userkey)
|
||||
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
||||
raise DrmException('AES improper key used')
|
||||
raise DrmException(u"AES improper key used")
|
||||
return
|
||||
keyctx = self._keyctx = AES_KEY()
|
||||
self._iv = iv
|
||||
self._userkey = userkey
|
||||
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
|
||||
if rv < 0:
|
||||
raise DrmException('Failed to initialize AES key')
|
||||
raise DrmException(u"Failed to initialize AES key")
|
||||
|
||||
def decrypt(self, data):
|
||||
out = create_string_buffer(len(data))
|
||||
|
@ -95,7 +98,7 @@ def _load_crypto_libcrypto():
|
|||
keyctx = self._keyctx
|
||||
rv = AES_cbc_encrypt(data, out, len(data), keyctx, mutable_iv, 0)
|
||||
if rv == 0:
|
||||
raise DrmException('AES decryption failed')
|
||||
raise DrmException(u"AES decryption failed")
|
||||
return out.raw
|
||||
|
||||
def keyivgen(self, passwd, salt, iter, keylen):
|
||||
|
@ -139,20 +142,20 @@ def SHA256(message):
|
|||
return ctx.digest()
|
||||
|
||||
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
|
||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||
charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM"
|
||||
charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M'
|
||||
charMap2 = 'ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM'
|
||||
|
||||
# For kinf approach of K4Mac 1.6.X or later
|
||||
# On K4PC charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE"
|
||||
# On K4PC charMap5 = 'AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE'
|
||||
# For Mac they seem to re-use charMap2 here
|
||||
charMap5 = charMap2
|
||||
|
||||
# new in K4M 1.9.X
|
||||
testMap8 = "YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD"
|
||||
testMap8 = 'YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD'
|
||||
|
||||
|
||||
def encode(data, map):
|
||||
result = ""
|
||||
result = ''
|
||||
for char in data:
|
||||
value = ord(char)
|
||||
Q = (value ^ 0x80) // len(map)
|
||||
|
@ -167,14 +170,14 @@ def encodeHash(data,map):
|
|||
|
||||
# Decode the string in data with the characters in map. Returns the decoded bytes
|
||||
def decode(data,map):
|
||||
result = ""
|
||||
result = ''
|
||||
for i in range (0,len(data)-1,2):
|
||||
high = map.find(data[i])
|
||||
low = map.find(data[i+1])
|
||||
if (high == -1) or (low == -1) :
|
||||
break
|
||||
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||
result += pack("B",value)
|
||||
result += pack('B',value)
|
||||
return result
|
||||
|
||||
# For K4M 1.6.X and later
|
||||
|
@ -200,7 +203,7 @@ def primes(n):
|
|||
|
||||
|
||||
# uses a sub process to get the Hard Drive Serial Number using ioreg
|
||||
# returns with the serial number of drive whose BSD Name is "disk0"
|
||||
# returns with the serial number of drive whose BSD Name is 'disk0'
|
||||
def GetVolumeSerialNumber():
|
||||
sernum = os.getenv('MYSERIALNUMBER')
|
||||
if sernum != None:
|
||||
|
@ -216,11 +219,11 @@ def GetVolumeSerialNumber():
|
|||
foundIt = False
|
||||
for j in xrange(cnt):
|
||||
resline = reslst[j]
|
||||
pp = resline.find('"Serial Number" = "')
|
||||
pp = resline.find('\"Serial Number\" = \"')
|
||||
if pp >= 0:
|
||||
sernum = resline[pp+19:-1]
|
||||
sernum = sernum.strip()
|
||||
bb = resline.find('"BSD Name" = "')
|
||||
bb = resline.find('\"BSD Name\" = \"')
|
||||
if bb >= 0:
|
||||
bsdname = resline[bb+14:-1]
|
||||
bsdname = bsdname.strip()
|
||||
|
@ -277,7 +280,7 @@ def GetDiskPartitionUUID(diskpart):
|
|||
nest += 1
|
||||
if resline.find('}') >= 0:
|
||||
nest -= 1
|
||||
pp = resline.find('"UUID" = "')
|
||||
pp = resline.find('\"UUID\" = \"')
|
||||
if pp >= 0:
|
||||
uuidnum = resline[pp+10:-1]
|
||||
uuidnum = uuidnum.strip()
|
||||
|
@ -285,7 +288,7 @@ def GetDiskPartitionUUID(diskpart):
|
|||
if partnest == uuidnest and uuidnest > 0:
|
||||
foundIt = True
|
||||
break
|
||||
bb = resline.find('"BSD Name" = "')
|
||||
bb = resline.find('\"BSD Name\" = \"')
|
||||
if bb >= 0:
|
||||
bsdname = resline[bb+14:-1]
|
||||
bsdname = bsdname.strip()
|
||||
|
@ -323,7 +326,7 @@ def GetMACAddressMunged():
|
|||
if pp >= 0:
|
||||
macnum = resline[pp+6:-1]
|
||||
macnum = macnum.strip()
|
||||
# print "original mac", macnum
|
||||
# print 'original mac', macnum
|
||||
# now munge it up the way Kindle app does
|
||||
# by xoring it with 0xa5 and swapping elements 3 and 4
|
||||
maclst = macnum.split(':')
|
||||
|
@ -340,7 +343,7 @@ def GetMACAddressMunged():
|
|||
mlst[2] = maclst[2] ^ 0xa5
|
||||
mlst[1] = maclst[1] ^ 0xa5
|
||||
mlst[0] = maclst[0] ^ 0xa5
|
||||
macnum = "%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x" % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5])
|
||||
macnum = '%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x' % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5])
|
||||
foundIt = True
|
||||
break
|
||||
if not foundIt:
|
||||
|
@ -367,6 +370,19 @@ def isNewInstall():
|
|||
return False
|
||||
|
||||
|
||||
class Memoize:
|
||||
"""Memoize(fn) - an instance which acts like fn but memoizes its arguments
|
||||
Will only work on functions with non-mutable arguments
|
||||
"""
|
||||
def __init__(self, fn):
|
||||
self.fn = fn
|
||||
self.memo = {}
|
||||
def __call__(self, *args):
|
||||
if not self.memo.has_key(args):
|
||||
self.memo[args] = self.fn(*args)
|
||||
return self.memo[args]
|
||||
|
||||
@Memoize
|
||||
def GetIDString():
|
||||
# K4Mac now has an extensive set of ids strings it uses
|
||||
# in encoding pids and in creating unique passwords
|
||||
|
@ -530,7 +546,8 @@ def getKindleInfoFiles():
|
|||
# determine type of kindle info provided and return a
|
||||
# database of keynames and values
|
||||
def getDBfromFile(kInfoFile):
|
||||
names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber", "max_date", "SIGVERIF"]
|
||||
|
||||
names = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber', 'max_date', 'SIGVERIF']
|
||||
DB = {}
|
||||
cnt = 0
|
||||
infoReader = open(kInfoFile, 'r')
|
||||
|
@ -545,12 +562,12 @@ def getDBfromFile(kInfoFile):
|
|||
for item in items:
|
||||
if item != '':
|
||||
keyhash, rawdata = item.split(':')
|
||||
keyname = "unknown"
|
||||
keyname = 'unknown'
|
||||
for name in names:
|
||||
if encodeHash(name,charMap2) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
if keyname == 'unknown':
|
||||
keyname = keyhash
|
||||
encryptedValue = decode(rawdata,charMap2)
|
||||
cleartext = cud.decrypt(encryptedValue)
|
||||
|
@ -563,8 +580,8 @@ def getDBfromFile(kInfoFile):
|
|||
if hdr == '/':
|
||||
|
||||
# else newer style .kinf file used by K4Mac >= 1.6.0
|
||||
# the .kinf file uses "/" to separate it into records
|
||||
# so remove the trailing "/" to make it easy to use split
|
||||
# the .kinf file uses '/' to separate it into records
|
||||
# so remove the trailing '/' to make it easy to use split
|
||||
data = data[:-1]
|
||||
items = data.split('/')
|
||||
cud = CryptUnprotectDataV2()
|
||||
|
@ -578,11 +595,11 @@ def getDBfromFile(kInfoFile):
|
|||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
keyname = "unknown"
|
||||
keyname = 'unknown'
|
||||
|
||||
# the raw keyhash string is also used to create entropy for the actual
|
||||
# CryptProtectData Blob that represents that keys contents
|
||||
# "entropy" not used for K4Mac only K4PC
|
||||
# 'entropy' not used for K4Mac only K4PC
|
||||
# entropy = SHA1(keyhash)
|
||||
|
||||
# the remainder of the first record when decoded with charMap5
|
||||
|
@ -599,12 +616,12 @@ def getDBfromFile(kInfoFile):
|
|||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
keyname = "unknown"
|
||||
keyname = 'unknown'
|
||||
for name in names:
|
||||
if encodeHash(name,charMap5) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
if keyname == 'unknown':
|
||||
keyname = keyhash
|
||||
|
||||
# the charMap5 encoded contents data has had a length
|
||||
|
@ -615,10 +632,10 @@ def getDBfromFile(kInfoFile):
|
|||
|
||||
# The offset into the charMap5 encoded contents seems to be:
|
||||
# len(contents) - largest prime number less than or equal to int(len(content)/3)
|
||||
# (in other words split "about" 2/3rds of the way through)
|
||||
# (in other words split 'about' 2/3rds of the way through)
|
||||
|
||||
# move first offsets chars to end to align for decode by charMap5
|
||||
encdata = "".join(edlst)
|
||||
encdata = ''.join(edlst)
|
||||
contlen = len(encdata)
|
||||
|
||||
# now properly split and recombine
|
||||
|
@ -667,7 +684,7 @@ def getDBfromFile(kInfoFile):
|
|||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
keyname = "unknown"
|
||||
keyname = 'unknown'
|
||||
|
||||
# unlike K4PC the keyhash is not used in generating entropy
|
||||
# entropy = SHA1(keyhash) + added_entropy
|
||||
|
@ -687,12 +704,12 @@ def getDBfromFile(kInfoFile):
|
|||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
keyname = "unknown"
|
||||
keyname = 'unknown'
|
||||
for name in names:
|
||||
if encodeHash(name,testMap8) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
if keyname == 'unknown':
|
||||
keyname = keyhash
|
||||
|
||||
# the testMap8 encoded contents data has had a length
|
||||
|
@ -703,10 +720,10 @@ def getDBfromFile(kInfoFile):
|
|||
|
||||
# The offset into the testMap8 encoded contents seems to be:
|
||||
# len(contents) - largest prime number less than or equal to int(len(content)/3)
|
||||
# (in other words split "about" 2/3rds of the way through)
|
||||
# (in other words split 'about' 2/3rds of the way through)
|
||||
|
||||
# move first offsets chars to end to align for decode by testMap8
|
||||
encdata = "".join(edlst)
|
||||
encdata = ''.join(edlst)
|
||||
contlen = len(encdata)
|
||||
|
||||
# now properly split and recombine
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# K4PC Windows specific routines
|
||||
|
||||
from __future__ import with_statement
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
#!/usr/bin/python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# mobidedrm.py, version 0.38
|
||||
# Copyright © 2008 The Dark Reverser
|
||||
#
|
||||
# Modified 2008–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
|
||||
# This is a python script. You need a Python interpreter to run it.
|
||||
# For example, ActiveState Python, which exists for windows.
|
||||
#
|
||||
|
@ -59,26 +65,78 @@
|
|||
# 0.35 - add interface to get mobi_version
|
||||
# 0.36 - fixed problem with TEXtREAd and getBookTitle interface
|
||||
# 0.37 - Fixed double announcement for stand-alone operation
|
||||
# 0.38 - Unicode used wherever possible, cope with absent alfcrypto
|
||||
|
||||
|
||||
__version__ = '0.37'
|
||||
__version__ = u"0.38"
|
||||
|
||||
import sys
|
||||
import os
|
||||
import struct
|
||||
import binascii
|
||||
try:
|
||||
from alfcrypto import Pukall_Cipher
|
||||
except:
|
||||
print u"AlfCrypto not found. Using python PC1 implementation."
|
||||
|
||||
class Unbuffered:
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
|
||||
import os
|
||||
import struct
|
||||
import binascii
|
||||
from alfcrypto import Pukall_Cipher
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return [u"mobidedrm.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = 'utf-8'
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
@ -90,40 +148,45 @@ class DrmException(Exception):
|
|||
|
||||
# Implementation of Pukall Cipher 1
|
||||
def PC1(key, src, decryption=True):
|
||||
return Pukall_Cipher().PC1(key,src,decryption)
|
||||
# sum1 = 0;
|
||||
# sum2 = 0;
|
||||
# keyXorVal = 0;
|
||||
# if len(key)!=16:
|
||||
# print "Bad key length!"
|
||||
# return None
|
||||
# wkey = []
|
||||
# for i in xrange(8):
|
||||
# wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
|
||||
# dst = ""
|
||||
# for i in xrange(len(src)):
|
||||
# temp1 = 0;
|
||||
# byteXorVal = 0;
|
||||
# for j in xrange(8):
|
||||
# temp1 ^= wkey[j]
|
||||
# sum2 = (sum2+j)*20021 + sum1
|
||||
# sum1 = (temp1*346)&0xFFFF
|
||||
# sum2 = (sum2+sum1)&0xFFFF
|
||||
# temp1 = (temp1*20021+1)&0xFFFF
|
||||
# byteXorVal ^= temp1 ^ sum2
|
||||
# curByte = ord(src[i])
|
||||
# if not decryption:
|
||||
# keyXorVal = curByte * 257;
|
||||
# curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
|
||||
# if decryption:
|
||||
# keyXorVal = curByte * 257;
|
||||
# for j in xrange(8):
|
||||
# wkey[j] ^= keyXorVal;
|
||||
# dst+=chr(curByte)
|
||||
# return dst
|
||||
# if we can get it from alfcrypto, use that
|
||||
try:
|
||||
return Pukall_Cipher().PC1(key,src,decryption)
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
# use slow python version, since Pukall_Cipher didn't load
|
||||
sum1 = 0;
|
||||
sum2 = 0;
|
||||
keyXorVal = 0;
|
||||
if len(key)!=16:
|
||||
DrmException (u"PC1: Bad key length")
|
||||
wkey = []
|
||||
for i in xrange(8):
|
||||
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
|
||||
dst = ""
|
||||
for i in xrange(len(src)):
|
||||
temp1 = 0;
|
||||
byteXorVal = 0;
|
||||
for j in xrange(8):
|
||||
temp1 ^= wkey[j]
|
||||
sum2 = (sum2+j)*20021 + sum1
|
||||
sum1 = (temp1*346)&0xFFFF
|
||||
sum2 = (sum2+sum1)&0xFFFF
|
||||
temp1 = (temp1*20021+1)&0xFFFF
|
||||
byteXorVal ^= temp1 ^ sum2
|
||||
curByte = ord(src[i])
|
||||
if not decryption:
|
||||
keyXorVal = curByte * 257;
|
||||
curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
|
||||
if decryption:
|
||||
keyXorVal = curByte * 257;
|
||||
for j in xrange(8):
|
||||
wkey[j] ^= keyXorVal;
|
||||
dst+=chr(curByte)
|
||||
return dst
|
||||
|
||||
def checksumPid(s):
|
||||
letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
|
||||
crc = (~binascii.crc32(s,-1))&0xFFFFFFFF
|
||||
crc = crc ^ (crc >> 16)
|
||||
res = s
|
||||
|
@ -171,17 +234,24 @@ class MobiBook:
|
|||
off = self.sections[section][0]
|
||||
return self.data_file[off:endoff]
|
||||
|
||||
def __init__(self, infile, announce = True):
|
||||
if announce:
|
||||
print ('MobiDeDrm v%(__version__)s. '
|
||||
'Copyright 2008-2012 The Dark Reverser et al.' % globals())
|
||||
def cleanup(self):
|
||||
# to match function in Topaz book
|
||||
pass
|
||||
|
||||
def __init__(self, infile):
|
||||
print u"MobiDeDrm v{0:s}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__)
|
||||
|
||||
try:
|
||||
from alfcrypto import Pukall_Cipher
|
||||
except:
|
||||
print u"AlfCrypto not found. Using python PC1 implementation."
|
||||
|
||||
# initial sanity check on file
|
||||
self.data_file = file(infile, 'rb').read()
|
||||
self.mobi_data = ''
|
||||
self.header = self.data_file[0:78]
|
||||
if self.header[0x3C:0x3C+8] != 'BOOKMOBI' and self.header[0x3C:0x3C+8] != 'TEXtREAd':
|
||||
raise DrmException("invalid file format")
|
||||
raise DrmException(u"Invalid file format")
|
||||
self.magic = self.header[0x3C:0x3C+8]
|
||||
self.crypto_type = -1
|
||||
|
||||
|
@ -199,7 +269,7 @@ class MobiBook:
|
|||
self.compression, = struct.unpack('>H', self.sect[0x0:0x0+2])
|
||||
|
||||
if self.magic == 'TEXtREAd':
|
||||
print "Book has format: ", self.magic
|
||||
print u"PalmDoc format book detected."
|
||||
self.extra_data_flags = 0
|
||||
self.mobi_length = 0
|
||||
self.mobi_codepage = 1252
|
||||
|
@ -209,11 +279,11 @@ class MobiBook:
|
|||
self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18])
|
||||
self.mobi_codepage, = struct.unpack('>L',self.sect[0x1c:0x20])
|
||||
self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C])
|
||||
print "MOBI header version = %d, length = %d" %(self.mobi_version, self.mobi_length)
|
||||
print u"MOBI header version {0:d}, header length {1:d}".format(self.mobi_version, self.mobi_length)
|
||||
self.extra_data_flags = 0
|
||||
if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
|
||||
self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4])
|
||||
print "Extra Data Flags = %d" % self.extra_data_flags
|
||||
print u"Extra Data Flags: {0:d}".format(self.extra_data_flags)
|
||||
if (self.compression != 17480):
|
||||
# multibyte utf8 data is included in the encryption for PalmDoc compression
|
||||
# so clear that byte so that we leave it to be decrypted.
|
||||
|
@ -223,10 +293,10 @@ class MobiBook:
|
|||
self.meta_array = {}
|
||||
try:
|
||||
exth_flag, = struct.unpack('>L', self.sect[0x80:0x84])
|
||||
exth = 'NONE'
|
||||
exth = ''
|
||||
if exth_flag & 0x40:
|
||||
exth = self.sect[16 + self.mobi_length:]
|
||||
if (len(exth) >= 4) and (exth[:4] == 'EXTH'):
|
||||
if (len(exth) >= 12) and (exth[:4] == 'EXTH'):
|
||||
nitems, = struct.unpack('>I', exth[8:12])
|
||||
pos = 12
|
||||
for i in xrange(nitems):
|
||||
|
@ -236,10 +306,10 @@ class MobiBook:
|
|||
# reset the text to speech flag and clipping limit, if present
|
||||
if type == 401 and size == 9:
|
||||
# set clipping limit to 100%
|
||||
self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8)
|
||||
self.patchSection(0, '\144', 16 + self.mobi_length + pos + 8)
|
||||
elif type == 404 and size == 9:
|
||||
# make sure text to speech is enabled
|
||||
self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8)
|
||||
self.patchSection(0, '\0', 16 + self.mobi_length + pos + 8)
|
||||
# print type, size, content, content.encode('hex')
|
||||
pos += size
|
||||
except:
|
||||
|
@ -265,8 +335,8 @@ class MobiBook:
|
|||
codec = codec_map[self.mobi_codepage]
|
||||
if title == '':
|
||||
title = self.header[:32]
|
||||
title = title.split("\0")[0]
|
||||
return unicode(title, codec).encode('utf-8')
|
||||
title = title.split('\0')[0]
|
||||
return unicode(title, codec)
|
||||
|
||||
def getPIDMetaInfo(self):
|
||||
rec209 = ''
|
||||
|
@ -297,7 +367,7 @@ class MobiBook:
|
|||
|
||||
def parseDRM(self, data, count, pidlist):
|
||||
found_key = None
|
||||
keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96"
|
||||
keyvec1 = '\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96'
|
||||
for pid in pidlist:
|
||||
bigpid = pid.ljust(16,'\0')
|
||||
temp_key = PC1(keyvec1, bigpid, False)
|
||||
|
@ -315,7 +385,7 @@ class MobiBook:
|
|||
break
|
||||
if not found_key:
|
||||
# Then try the default encoding that doesn't require a PID
|
||||
pid = "00000000"
|
||||
pid = '00000000'
|
||||
temp_key = keyvec1
|
||||
temp_key_sum = sum(map(ord,temp_key)) & 0xff
|
||||
for i in xrange(count):
|
||||
|
@ -328,82 +398,90 @@ class MobiBook:
|
|||
break
|
||||
return [found_key,pid]
|
||||
|
||||
def getMobiFile(self, outpath):
|
||||
def getFile(self, outpath):
|
||||
file(outpath,'wb').write(self.mobi_data)
|
||||
|
||||
def getMobiVersion(self):
|
||||
return self.mobi_version
|
||||
def getBookType(self):
|
||||
if self.print_replica:
|
||||
return u"Print Replica"
|
||||
if self.mobi_version >= 8:
|
||||
return u"Kindle Format 8"
|
||||
return u"Mobipocket"
|
||||
|
||||
def getPrintReplica(self):
|
||||
return self.print_replica
|
||||
def getBookExtension(self):
|
||||
if self.print_replica:
|
||||
return u".azw4"
|
||||
if self.mobi_version >= 8:
|
||||
return u".azw3"
|
||||
return u".mobi"
|
||||
|
||||
def processBook(self, pidlist):
|
||||
crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2])
|
||||
print 'Crypto Type is: ', crypto_type
|
||||
print u"Crypto Type is: {0:d}".format(crypto_type)
|
||||
self.crypto_type = crypto_type
|
||||
if crypto_type == 0:
|
||||
print "This book is not encrypted."
|
||||
print u"This book is not encrypted."
|
||||
# we must still check for Print Replica
|
||||
self.print_replica = (self.loadSection(1)[0:4] == '%MOP')
|
||||
self.mobi_data = self.data_file
|
||||
return
|
||||
if crypto_type != 2 and crypto_type != 1:
|
||||
raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type)
|
||||
raise DrmException(u"Cannot decode unknown Mobipocket encryption type {0:d}".format(crypto_type))
|
||||
if 406 in self.meta_array:
|
||||
data406 = self.meta_array[406]
|
||||
val406, = struct.unpack('>Q',data406)
|
||||
if val406 != 0:
|
||||
raise DrmException("Cannot decode library or rented ebooks.")
|
||||
raise DrmException(u"Cannot decode library or rented ebooks.")
|
||||
|
||||
goodpids = []
|
||||
for pid in pidlist:
|
||||
if len(pid)==10:
|
||||
if checksumPid(pid[0:-2]) != pid:
|
||||
print "Warning: PID " + pid + " has incorrect checksum, should have been "+checksumPid(pid[0:-2])
|
||||
print u"Warning: PID {0} has incorrect checksum, should have been {1}".format(pid,checksumPid(pid[0:-2]))
|
||||
goodpids.append(pid[0:-2])
|
||||
elif len(pid)==8:
|
||||
goodpids.append(pid)
|
||||
|
||||
if self.crypto_type == 1:
|
||||
t1_keyvec = "QDCVEPMU675RUBSZ"
|
||||
t1_keyvec = 'QDCVEPMU675RUBSZ'
|
||||
if self.magic == 'TEXtREAd':
|
||||
bookkey_data = self.sect[0x0E:0x0E+16]
|
||||
elif self.mobi_version < 0:
|
||||
bookkey_data = self.sect[0x90:0x90+16]
|
||||
else:
|
||||
bookkey_data = self.sect[self.mobi_length+16:self.mobi_length+32]
|
||||
pid = "00000000"
|
||||
pid = '00000000'
|
||||
found_key = PC1(t1_keyvec, bookkey_data)
|
||||
else :
|
||||
# calculate the keys
|
||||
drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16])
|
||||
if drm_count == 0:
|
||||
raise DrmException("Not yet initialised with PID. Must be opened with Mobipocket Reader first.")
|
||||
raise DrmException(u"Encryption not initialised. Must be opened with Mobipocket Reader first.")
|
||||
found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids)
|
||||
if not found_key:
|
||||
raise DrmException("No key found in " + str(len(goodpids)) + " keys tried. Read the FAQs at Alf's blog. Only if none apply, report this failure for help.")
|
||||
raise DrmException(u"No key found in {0:d} keys tried. Read the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(len(goodpids)))
|
||||
# kill the drm keys
|
||||
self.patchSection(0, "\0" * drm_size, drm_ptr)
|
||||
self.patchSection(0, '\0' * drm_size, drm_ptr)
|
||||
# kill the drm pointers
|
||||
self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8)
|
||||
self.patchSection(0, '\xff' * 4 + '\0' * 12, 0xA8)
|
||||
|
||||
if pid=="00000000":
|
||||
print "File has default encryption, no specific PID."
|
||||
if pid=='00000000':
|
||||
print u"File has default encryption, no specific key needed."
|
||||
else:
|
||||
print "File is encoded with PID "+checksumPid(pid)+"."
|
||||
print u"File is encoded with PID {0}.".format(checksumPid(pid))
|
||||
|
||||
# clear the crypto type
|
||||
self.patchSection(0, "\0" * 2, 0xC)
|
||||
|
||||
# decrypt sections
|
||||
print "Decrypting. Please wait . . .",
|
||||
print u"Decrypting. Please wait . . .",
|
||||
mobidataList = []
|
||||
mobidataList.append(self.data_file[:self.sections[1][0]])
|
||||
for i in xrange(1, self.records+1):
|
||||
data = self.loadSection(i)
|
||||
extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags)
|
||||
if i%100 == 0:
|
||||
print ".",
|
||||
print u".",
|
||||
# print "record %d, extra_size %d" %(i,extra_size)
|
||||
decoded_data = PC1(found_key, data[0:len(data) - extra_size])
|
||||
if i==1:
|
||||
|
@ -414,31 +492,24 @@ class MobiBook:
|
|||
if self.num_sections > self.records+1:
|
||||
mobidataList.append(self.data_file[self.sections[self.records+1][0]:])
|
||||
self.mobi_data = "".join(mobidataList)
|
||||
print "done"
|
||||
print u"done"
|
||||
return
|
||||
|
||||
def getUnencryptedBook(infile,pid,announce=True):
|
||||
def getUnencryptedBook(infile,pidlist):
|
||||
if not os.path.isfile(infile):
|
||||
raise DrmException('Input File Not Found')
|
||||
book = MobiBook(infile,announce)
|
||||
book.processBook([pid])
|
||||
return book.mobi_data
|
||||
|
||||
def getUnencryptedBookWithList(infile,pidlist,announce=True):
|
||||
if not os.path.isfile(infile):
|
||||
raise DrmException('Input File Not Found')
|
||||
book = MobiBook(infile, announce)
|
||||
raise DrmException(u"Input File Not Found.")
|
||||
book = MobiBook(infile)
|
||||
book.processBook(pidlist)
|
||||
return book.mobi_data
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
print ('MobiDeDrm v%(__version__)s. '
|
||||
'Copyright 2008-2012 The Dark Reverser et al.' % globals())
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
if len(argv)<3 or len(argv)>4:
|
||||
print "Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks"
|
||||
print "Usage:"
|
||||
print " %s <infile> <outfile> [<Comma separated list of PIDs to try>]" % sys.argv[0]
|
||||
print u"MobiDeDrm v{0}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__)
|
||||
print u"Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks"
|
||||
print u"Usage:"
|
||||
print u" {0} <infile> <outfile> [<Comma separated list of PIDs to try>]".format(os.path.basename(sys.argv[0]))
|
||||
return 1
|
||||
else:
|
||||
infile = argv[1]
|
||||
|
@ -446,15 +517,17 @@ def main(argv=sys.argv):
|
|||
if len(argv) is 4:
|
||||
pidlist = argv[3].split(',')
|
||||
else:
|
||||
pidlist = {}
|
||||
pidlist = []
|
||||
try:
|
||||
stripped_file = getUnencryptedBookWithList(infile, pidlist, False)
|
||||
stripped_file = getUnencryptedBook(infile, pidlist)
|
||||
file(outfile, 'wb').write(stripped_file)
|
||||
except DrmException, e:
|
||||
print "Error: %s" % e
|
||||
print u"MobiDeDRM v{0} Error: {0:s}".format(__version__,e.args[0])
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
if __name__ == '__main__':
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
|
|
|
@ -41,7 +41,7 @@ Mac OS X 10.5 and above: You do
|
|||
\i not
|
||||
\i0 need to install Python.\
|
||||
\
|
||||
Drag the DeDRM application from from tools_v5.4.1\\DeDRM_Applications\\Macintosh (the location of this ReadMe) to your Applications folder, or anywhere else you find convenient.\
|
||||
Drag the DeDRM application from from tools_v5.5\\DeDRM_Applications\\Macintosh (the location of this ReadMe) to your Applications folder, or anywhere else you find convenient.\
|
||||
\
|
||||
\
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ global BNKeyGenTool
|
|||
global BNePubTool
|
||||
global AdobeKeyGenTool
|
||||
global AdobeePubTool
|
||||
global ePubTestTool
|
||||
global AdobePDFTool
|
||||
global ZipFixTool
|
||||
global ProgressApp
|
||||
|
@ -42,7 +43,7 @@ on writetolog(logstring)
|
|||
try
|
||||
set fileRef to open for access logFilePath with write permission
|
||||
write logstring & "
|
||||
" to fileRef starting at eof
|
||||
" to fileRef starting at eof as Çclass utf8È
|
||||
close access fileRef
|
||||
end try
|
||||
end writetolog
|
||||
|
@ -68,11 +69,7 @@ on readtemp()
|
|||
try
|
||||
set fileRef to open for access tempfilepath
|
||||
if (get eof fileRef) > 0 then
|
||||
set tempContentsLines to (read fileRef from 1 using delimiter (character id 10))
|
||||
set oldTIDs to AppleScript's text item delimiters
|
||||
set AppleScript's text item delimiters to (character id 13)
|
||||
set tempContents to tempContentsLines as string
|
||||
set AppleScript's text item delimiters to oldTIDs
|
||||
set tempContents to read fileRef from 1 as Çclass utf8È
|
||||
end if
|
||||
close access fileRef
|
||||
end try
|
||||
|
@ -155,6 +152,7 @@ on GetTools()
|
|||
set BNePubTool to POSIX path of (path to resource "ignobleepub.py")
|
||||
set AdobeKeyGenTool to POSIX path of (path to resource "ineptkey.py")
|
||||
set AdobeePubTool to POSIX path of (path to resource "ineptepub.py")
|
||||
set ePubTestTool to POSIX path of (path to resource "epubtest.py")
|
||||
set AdobePDFTool to POSIX path of (path to resource "ineptpdf.py")
|
||||
set ZipFixTool to POSIX path of (path to resource "zipfix.py")
|
||||
set ProgressApp to POSIX path of (path to resource "DeDRM Progress.app")
|
||||
|
@ -208,6 +206,12 @@ on GetTools()
|
|||
return false
|
||||
end if
|
||||
end if
|
||||
if not fileexists(ePubTestTool) then
|
||||
set dialogresult to (display dialog "The ePub encryption test script (epubtesttool.py) is missing from this package. Get a fresh copy." buttons {"Quit", "Continue Anyway"} default button 1 with title "DeDRM" with icon stop)
|
||||
if button returned of dialogresult is "Quit" then
|
||||
return false
|
||||
end if
|
||||
end if
|
||||
if not folderexists(ProgressApp) then
|
||||
set dialogresult to (display dialog "The Progress dialog application (DeDRM Progress.app) is missing from this package. Get a fresh copy." buttons {"Quit", "Continue Anyway"} default button 1 with title "DeDRM" with icon stop)
|
||||
if button returned of dialogresult is "Quit" then
|
||||
|
@ -278,7 +282,8 @@ on unlockmobifile(encryptedFile)
|
|||
writetolog("shellresult: " & shellresult & " " & ErrorText)
|
||||
try
|
||||
repeat
|
||||
if (not DecodingError) or (totalebooks > 1) or (offset of "No key found" in ErrorText) is 0 then
|
||||
if (totalebooks > 1) or (offset of "No key found" in shellresult) is 0 then
|
||||
--display dialog (totalebooks as text) & shellresult
|
||||
exit repeat
|
||||
end if
|
||||
-- ask for another PID as we're only doing one ebook
|
||||
|
@ -311,8 +316,7 @@ on unlockmobifile(encryptedFile)
|
|||
end repeat
|
||||
if DecodingError then
|
||||
set ErrorCount to ErrorCount + 1
|
||||
set ErrorList to ErrorList & fileName & fileExtension & " couldn't be decoded:
|
||||
" & (ErrorText as text) & "
|
||||
set ErrorList to ErrorList & fileName & fileExtension & " couldn't be decrypted.
|
||||
"
|
||||
else if (offset of "not encrypted" in shellresult) > 0 then
|
||||
set WarningCount to WarningCount + 1
|
||||
|
@ -512,34 +516,66 @@ on unlockepubfile(encryptedFile)
|
|||
set shellresult to "no keys"
|
||||
set ErrorText to ""
|
||||
|
||||
-- first we'll try the Barnes & Noble keys
|
||||
repeat with BNKey in bnKeys
|
||||
|
||||
set keyfilepath to third item of BNKey
|
||||
if length of keyfilepath > 0 then
|
||||
set shellcommand to python & (quoted form of BNePubTool) & " " & (quoted form of keyfilepath) & " " & (quoted form of fixedFilePath) & " " & (quoted form of unlockedFilePath)
|
||||
set shellcommand to shellcommand & " > " & quotedtemppath()
|
||||
--display dialog "shellcommand: " & shellcommand buttons {"OK"} default button 1 giving up after 10
|
||||
writetolog("shellcommand: " & shellcommand)
|
||||
cleartemp()
|
||||
set DecodingError to false
|
||||
set ErrorText to ""
|
||||
try
|
||||
do shell script shellcommand
|
||||
on error ErrorText
|
||||
set DecodingError to true
|
||||
end try
|
||||
set shellresult to readtemp()
|
||||
writetolog("shellresult: " & shellresult & " " & ErrorText)
|
||||
--display dialog shellresult
|
||||
if not DecodingError then
|
||||
set decoded to "YES"
|
||||
exit repeat
|
||||
end if
|
||||
-- get encryption type
|
||||
set TryBandNePub to true
|
||||
set TryAdobeePub to true
|
||||
set shellcommand to python & (quoted form of ePubTestTool) & " " & (quoted form of fixedFilePath)
|
||||
set shellcommand to shellcommand & " > " & quotedtemppath()
|
||||
--display dialog "shellcommand: " & shellcommand buttons {"OK"} default button 1 giving up after 10
|
||||
writetolog("shellcommand: " & shellcommand)
|
||||
cleartemp()
|
||||
set TestError to false
|
||||
set ErrorText to ""
|
||||
try
|
||||
do shell script shellcommand
|
||||
on error ErrorText
|
||||
set TestError to true
|
||||
end try
|
||||
set shellresult to readtemp()
|
||||
writetolog("shellresult: " & shellresult & " " & ErrorText)
|
||||
--display dialog shellresult
|
||||
if not TestError then
|
||||
if (offset of "B&N" in shellresult) > 0 then
|
||||
set TryAdobeePub to false
|
||||
else if (offset of "Adobe" in shellresult) > 0 then
|
||||
set TryBandNePub to false
|
||||
else if (offset of "Unencrypted" in shellresult) > 0 then
|
||||
set TryAdobeePub to false
|
||||
set TryBandNePub to false
|
||||
end if
|
||||
end repeat
|
||||
end if
|
||||
|
||||
if decoded is "NO" then
|
||||
|
||||
-- first we'll try the Barnes & Noble keys
|
||||
if TryBandNePub then
|
||||
repeat with BNKey in bnKeys
|
||||
|
||||
set keyfilepath to third item of BNKey
|
||||
if length of keyfilepath > 0 then
|
||||
set shellcommand to python & (quoted form of BNePubTool) & " " & (quoted form of keyfilepath) & " " & (quoted form of fixedFilePath) & " " & (quoted form of unlockedFilePath)
|
||||
set shellcommand to shellcommand & " > " & quotedtemppath()
|
||||
--display dialog "shellcommand: " & shellcommand buttons {"OK"} default button 1 giving up after 10
|
||||
writetolog("shellcommand: " & shellcommand)
|
||||
cleartemp()
|
||||
set DecodingError to false
|
||||
set ErrorText to ""
|
||||
try
|
||||
do shell script shellcommand
|
||||
on error ErrorText
|
||||
set DecodingError to true
|
||||
end try
|
||||
set shellresult to readtemp()
|
||||
writetolog("shellresult: " & shellresult & " " & ErrorText)
|
||||
--display dialog shellresult
|
||||
if not DecodingError then
|
||||
set decoded to "YES"
|
||||
exit repeat
|
||||
end if
|
||||
end if
|
||||
end repeat
|
||||
end if
|
||||
|
||||
if decoded is "NO" and TryAdobeePub then
|
||||
-- now try Adobe ePub
|
||||
repeat with AdeptKey in AdeptKeyList
|
||||
set shellcommand to python & (quoted form of AdobeePubTool) & " " & (quoted form of AdeptKey) & " " & (quoted form of fixedFilePath) & " " & (quoted form of unlockedFilePath)
|
||||
|
@ -567,13 +603,13 @@ on unlockepubfile(encryptedFile)
|
|||
if decoded is "YES" then
|
||||
set CompletedCount to CompletedCount + 1
|
||||
set CompletedList to CompletedList & fileName & fileExtension & paraend
|
||||
else if not TryAdobeePub and not TryBandNePub then
|
||||
set WarningCount to WarningCount + 1
|
||||
set WarningList to (WarningList & fileName & " doesn't seem to be encrypted.
|
||||
")
|
||||
else if shellresult is "no keys" then
|
||||
set ErrorCount to ErrorCount + 1
|
||||
set ErrorList to (ErrorList & fileName & fileExtension & " couldn't be decoded: no keys.
|
||||
")
|
||||
else if (offset of "not an ADEPT EPUB" in shellresult) is not 0 then
|
||||
set WarningCount to WarningCount + 1
|
||||
set WarningList to (WarningList & fileName & " doesn't seem to be encrypted.
|
||||
")
|
||||
else
|
||||
set ErrorCount to ErrorCount + 1
|
||||
|
@ -942,7 +978,7 @@ Enter any additional Kindle Serial Numbers one at a time:"
|
|||
if button returned of dialogresult is "Add" then
|
||||
set Serial to text returned of dialogresult
|
||||
set Seriallength to length of Serial
|
||||
if Seriallength is 16 and (first character of Serial) is "B" then
|
||||
if Seriallength is 16 and ((first character of Serial) is "B" or (first character of Serial) is "9") then
|
||||
set KindleSerialList to KindleSerialList & Serial
|
||||
set Serial to ""
|
||||
else
|
||||
|
@ -1528,7 +1564,7 @@ For full information about the licence, please see http://unlicense.org/
|
|||
The application icon is adapted from the Authors Against DRM logo at
|
||||
http://readersbillofrights.info/AAD and is under the Creative Commons Attribution-ShareAlike licence.
|
||||
The included Python scripts are all free to use, but have a variety of licences. See the individual files for details.
|
||||
" with title "DeDRM 5.2 by Apprentice Alf" buttons {"Close", "Select EbookÉ ", "ConfigureÉ "} default button 1 with icon note
|
||||
" with title "DeDRM by Apprentice Alf" buttons {"Close", "Select EbookÉ ", "ConfigureÉ "} default button 1 with icon note
|
||||
ReadPrefs()
|
||||
clearlog()
|
||||
GetAdeptKey(false)
|
||||
|
|
|
@ -24,17 +24,17 @@
|
|||
<key>CFBundleExecutable</key>
|
||||
<string>droplet</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>DeDRM 5.4.1. AppleScript written 2010–2012 by Apprentice Alf and others.</string>
|
||||
<string>DeDRM 5.5. AppleScript written 2010–2012 by Apprentice Alf and others.</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>DeDRM</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>DeDRM 5.4.1</string>
|
||||
<string>DeDRM 5.5</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>5.4.1</string>
|
||||
<string>5.5</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>dplt</string>
|
||||
<key>LSRequiresCarbon</key>
|
||||
|
@ -50,7 +50,7 @@
|
|||
<key>positionOfDivider</key>
|
||||
<real>0</real>
|
||||
<key>savedFrame</key>
|
||||
<string>287 405 800 473 0 0 1440 878 </string>
|
||||
<string>1846 -16 800 473 1440 -180 1920 1080 </string>
|
||||
<key>selectedTabView</key>
|
||||
<string>event log</string>
|
||||
</dict>
|
||||
|
|
Binary file not shown.
|
@ -1,11 +1,18 @@
|
|||
#! /usr/bin/env python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# crypto library mainly by some_updates
|
||||
|
||||
# pbkdf2.py pbkdf2 code taken from pbkdf2.py
|
||||
# pbkdf2.py Copyright © 2004 Matt Johnston <matt @ ucc asn au>
|
||||
# pbkdf2.py Copyright © 2009 Daniel Holth <dholth@fastmail.fm>
|
||||
# pbkdf2.py This code may be freely used and modified for any purpose.
|
||||
|
||||
import sys, os
|
||||
import hmac
|
||||
from struct import pack
|
||||
import hashlib
|
||||
|
||||
|
||||
# interface to needed routines libalfcrypto
|
||||
def _load_libalfcrypto():
|
||||
import ctypes
|
||||
|
@ -26,8 +33,8 @@ def _load_libalfcrypto():
|
|||
name_of_lib = 'libalfcrypto32.so'
|
||||
else:
|
||||
name_of_lib = 'libalfcrypto64.so'
|
||||
|
||||
libalfcrypto = sys.path[0] + os.sep + name_of_lib
|
||||
|
||||
libalfcrypto = os.path.join(sys.path[0],name_of_lib)
|
||||
|
||||
if not os.path.isfile(libalfcrypto):
|
||||
raise Exception('libalfcrypto not found')
|
||||
|
@ -55,7 +62,7 @@ def _load_libalfcrypto():
|
|||
#
|
||||
# int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key);
|
||||
#
|
||||
#
|
||||
#
|
||||
# void AES_cbc_encrypt(const unsigned char *in, unsigned char *out,
|
||||
# const unsigned long length, const AES_KEY *key,
|
||||
# unsigned char *ivec, const int enc);
|
||||
|
@ -147,7 +154,7 @@ def _load_libalfcrypto():
|
|||
topazCryptoDecrypt(ctx, data, out, len(data))
|
||||
return out.raw
|
||||
|
||||
print "Using Library AlfCrypto DLL/DYLIB/SO"
|
||||
print u"Using Library AlfCrypto DLL/DYLIB/SO"
|
||||
return (AES_CBC, Pukall_Cipher, Topaz_Cipher)
|
||||
|
||||
|
||||
|
@ -164,8 +171,7 @@ def _load_python_alfcrypto():
|
|||
sum2 = 0;
|
||||
keyXorVal = 0;
|
||||
if len(key)!=16:
|
||||
print "Bad key length!"
|
||||
return None
|
||||
raise Exception('Pukall_Cipher: Bad key length.')
|
||||
wkey = []
|
||||
for i in xrange(8):
|
||||
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
|
||||
|
@ -234,6 +240,7 @@ def _load_python_alfcrypto():
|
|||
cleartext = self.aes.decrypt(iv + data)
|
||||
return cleartext
|
||||
|
||||
print u"Using Library AlfCrypto Python"
|
||||
return (AES_CBC, Pukall_Cipher, Topaz_Cipher)
|
||||
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from PyQt4.Qt import QWidget, QVBoxLayout, QLabel, QLineEdit
|
||||
|
||||
from calibre.utils.config import JSONConfig
|
||||
|
|
|
@ -230,6 +230,7 @@ class PageParser(object):
|
|||
'empty' : (1, 'snippets', 1, 0),
|
||||
|
||||
'page' : (1, 'snippets', 1, 0),
|
||||
'page.class' : (1, 'scalar_text', 0, 0),
|
||||
'page.pageid' : (1, 'scalar_text', 0, 0),
|
||||
'page.pagelabel' : (1, 'scalar_text', 0, 0),
|
||||
'page.type' : (1, 'scalar_text', 0, 0),
|
||||
|
@ -238,11 +239,13 @@ class PageParser(object):
|
|||
'page.startID' : (1, 'scalar_number', 0, 0),
|
||||
|
||||
'group' : (1, 'snippets', 1, 0),
|
||||
'group.class' : (1, 'scalar_text', 0, 0),
|
||||
'group.type' : (1, 'scalar_text', 0, 0),
|
||||
'group._tag' : (1, 'scalar_text', 0, 0),
|
||||
'group.orientation': (1, 'scalar_text', 0, 0),
|
||||
|
||||
'region' : (1, 'snippets', 1, 0),
|
||||
'region.class' : (1, 'scalar_text', 0, 0),
|
||||
'region.type' : (1, 'scalar_text', 0, 0),
|
||||
'region.x' : (1, 'scalar_number', 0, 0),
|
||||
'region.y' : (1, 'scalar_number', 0, 0),
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
#!/usr/bin/python
|
||||
#
|
||||
# This is a python script. You need a Python interpreter to run it.
|
||||
# For example, ActiveState Python, which exists for windows.
|
||||
#
|
||||
# Changelog drmcheck
|
||||
# 1.00 - Initial version, with code from various other scripts
|
||||
# 1.01 - Moved authorship announcement to usage section.
|
||||
#
|
||||
# Changelog drmcheck
|
||||
# 1.00 - Cut to drmtest.py, testing ePub files only by Apprentice Alf
|
||||
#
|
||||
# Written in 2011 by Paul Durrant
|
||||
# Released with unlicense. See http://unlicense.org/
|
||||
#
|
||||
#############################################################################
|
||||
#
|
||||
# This is free and unencumbered software released into the public domain.
|
||||
#
|
||||
# Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
# distribute this software, either in source code form or as a compiled
|
||||
# binary, for any purpose, commercial or non-commercial, and by any
|
||||
# means.
|
||||
#
|
||||
# In jurisdictions that recognize copyright laws, the author or authors
|
||||
# of this software dedicate any and all copyright interest in the
|
||||
# software to the public domain. We make this dedication for the benefit
|
||||
# of the public at large and to the detriment of our heirs and
|
||||
# successors. We intend this dedication to be an overt act of
|
||||
# relinquishment in perpetuity of all present and future rights to this
|
||||
# software under copyright law.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
#############################################################################
|
||||
#
|
||||
# It's still polite to give attribution if you do reuse this code.
|
||||
#
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
__version__ = '1.00'
|
||||
|
||||
import sys, struct, os
|
||||
import zlib
|
||||
import zipfile
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
||||
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
def unicode_argv():
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
_FILENAME_LEN_OFFSET = 26
|
||||
_EXTRA_LEN_OFFSET = 28
|
||||
_FILENAME_OFFSET = 30
|
||||
_MAX_SIZE = 64 * 1024
|
||||
|
||||
|
||||
def uncompress(cmpdata):
|
||||
dc = zlib.decompressobj(-15)
|
||||
data = ''
|
||||
while len(cmpdata) > 0:
|
||||
if len(cmpdata) > _MAX_SIZE :
|
||||
newdata = cmpdata[0:_MAX_SIZE]
|
||||
cmpdata = cmpdata[_MAX_SIZE:]
|
||||
else:
|
||||
newdata = cmpdata
|
||||
cmpdata = ''
|
||||
newdata = dc.decompress(newdata)
|
||||
unprocessed = dc.unconsumed_tail
|
||||
if len(unprocessed) == 0:
|
||||
newdata += dc.flush()
|
||||
data += newdata
|
||||
cmpdata += unprocessed
|
||||
unprocessed = ''
|
||||
return data
|
||||
|
||||
def getfiledata(file, zi):
|
||||
# get file name length and exta data length to find start of file data
|
||||
local_header_offset = zi.header_offset
|
||||
|
||||
file.seek(local_header_offset + _FILENAME_LEN_OFFSET)
|
||||
leninfo = file.read(2)
|
||||
local_name_length, = struct.unpack('<H', leninfo)
|
||||
|
||||
file.seek(local_header_offset + _EXTRA_LEN_OFFSET)
|
||||
exinfo = file.read(2)
|
||||
extra_field_length, = struct.unpack('<H', exinfo)
|
||||
|
||||
file.seek(local_header_offset + _FILENAME_OFFSET + local_name_length + extra_field_length)
|
||||
data = None
|
||||
|
||||
# if not compressed we are good to go
|
||||
if zi.compress_type == zipfile.ZIP_STORED:
|
||||
data = file.read(zi.file_size)
|
||||
|
||||
# if compressed we must decompress it using zlib
|
||||
if zi.compress_type == zipfile.ZIP_DEFLATED:
|
||||
cmpdata = file.read(zi.compress_size)
|
||||
data = uncompress(cmpdata)
|
||||
|
||||
return data
|
||||
|
||||
def main(argv=unicode_argv()):
|
||||
infile = argv[1]
|
||||
kind = "Unknown"
|
||||
encryption = "Unknown"
|
||||
with file(infile,'rb') as infileobject:
|
||||
bookdata = infileobject.read(58)
|
||||
# Check for Mobipocket/Kindle
|
||||
if bookdata[0:0+2] == "PK":
|
||||
if bookdata[30:30+28] == 'mimetypeapplication/epub+zip':
|
||||
kind = "ePub"
|
||||
else:
|
||||
kind = "ZIP"
|
||||
encryption = "Unencrypted"
|
||||
foundrights = False
|
||||
foundencryption = False
|
||||
inzip = zipfile.ZipFile(infile,'r')
|
||||
namelist = set(inzip.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or 'META-INF/encryption.xml' not in namelist:
|
||||
encryption = "Unencrypted"
|
||||
else:
|
||||
rights = etree.fromstring(inzip.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:
|
||||
encryption = "Adobe"
|
||||
elif len(bookkey) == 64:
|
||||
encryption = "B&N"
|
||||
else:
|
||||
encryption = "Unknown"
|
||||
|
||||
print u"{0} {1}".format(encryption, kind)
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(main())
|
|
@ -1,8 +1,11 @@
|
|||
#!/usr/bin/env python
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# erdr2pml.py
|
||||
# Copyright © 2008 The Dark Reverser
|
||||
#
|
||||
# Modified 2008–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
|
||||
# This is a python script. You need a Python interpreter to run it.
|
||||
# For example, ActiveState Python, which exists for windows.
|
||||
# Changelog
|
||||
|
@ -62,19 +65,11 @@
|
|||
# 0.21 - Support eReader (drm) version 11.
|
||||
# - Don't reject dictionary format.
|
||||
# - Ignore sidebars for dictionaries (different format?)
|
||||
# 0.22 - Unicode and plugin support, different image folders for PMLZ and source
|
||||
|
||||
__version__='0.21'
|
||||
__version__='0.22'
|
||||
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
def write(self, data):
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
import sys, re
|
||||
import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
|
@ -82,8 +77,66 @@ if 'calibre' in sys.modules:
|
|||
else:
|
||||
inCalibre = False
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return [u"mobidedrm.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
Des = None
|
||||
if sys.platform.startswith('win'):
|
||||
if iswindows:
|
||||
# first try with pycrypto
|
||||
if inCalibre:
|
||||
from calibre_plugins.erdrpdb2pml import pycrypto_des
|
||||
|
@ -168,17 +221,30 @@ class Sectionizer(object):
|
|||
off = self.sections[section][0]
|
||||
return self.contents[off:end_off]
|
||||
|
||||
def sanitizeFileName(s):
|
||||
r = ''
|
||||
for c in s:
|
||||
if c in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-":
|
||||
r += c
|
||||
return r
|
||||
# cleanup unicode filenames
|
||||
# borrowed from calibre from calibre/src/calibre/__init__.py
|
||||
# added in removal of control (<32) chars
|
||||
# and removal of . at start and end
|
||||
# and with some (heavily edited) code from Paul Durrant's kindlenamer.py
|
||||
def sanitizeFileName(name):
|
||||
# substitute filename unfriendly characters
|
||||
name = name.replace(u"<",u"[").replace(u">",u"]").replace(u" : ",u" – ").replace(u": ",u" – ").replace(u":",u"—").replace(u"/",u"_").replace(u"\\",u"_").replace(u"|",u"_").replace(u"\"",u"\'")
|
||||
# delete control characters
|
||||
name = u"".join(char for char in name if ord(char)>=32)
|
||||
# white space to single space, delete leading and trailing while space
|
||||
name = re.sub(ur"\s", u" ", name).strip()
|
||||
# remove leading dots
|
||||
while len(name)>0 and name[0] == u".":
|
||||
name = name[1:]
|
||||
# remove trailing dots (Windows doesn't like them)
|
||||
if name.endswith(u'.'):
|
||||
name = name[:-1]
|
||||
return name
|
||||
|
||||
def fixKey(key):
|
||||
def fixByte(b):
|
||||
return b ^ ((b ^ (b<<1) ^ (b<<2) ^ (b<<3) ^ (b<<4) ^ (b<<5) ^ (b<<6) ^ (b<<7) ^ 0x80) & 0x80)
|
||||
return "".join([chr(fixByte(ord(a))) for a in key])
|
||||
return "".join([chr(fixByte(ord(a))) for a in key])
|
||||
|
||||
def deXOR(text, sp, table):
|
||||
r=''
|
||||
|
@ -191,7 +257,7 @@ def deXOR(text, sp, table):
|
|||
return r
|
||||
|
||||
class EreaderProcessor(object):
|
||||
def __init__(self, sect, username, creditcard):
|
||||
def __init__(self, sect, user_key):
|
||||
self.section_reader = sect.loadSection
|
||||
data = self.section_reader(0)
|
||||
version, = struct.unpack('>H', data[0:2])
|
||||
|
@ -212,18 +278,10 @@ class EreaderProcessor(object):
|
|||
for i in xrange(len(data)):
|
||||
j = (j + shuf) % len(data)
|
||||
r[j] = data[i]
|
||||
assert len("".join(r)) == len(data)
|
||||
assert len("".join(r)) == len(data)
|
||||
return "".join(r)
|
||||
r = unshuff(input[0:-8], cookie_shuf)
|
||||
|
||||
def fixUsername(s):
|
||||
r = ''
|
||||
for c in s.lower():
|
||||
if (c >= 'a' and c <= 'z' or c >= '0' and c <= '9'):
|
||||
r += c
|
||||
return r
|
||||
|
||||
user_key = struct.pack('>LL', binascii.crc32(fixUsername(username)) & 0xffffffff, binascii.crc32(creditcard[-8:])& 0xffffffff)
|
||||
drm_sub_version = struct.unpack('>H', r[0:2])[0]
|
||||
self.num_text_pages = struct.unpack('>H', r[2:4])[0] - 1
|
||||
self.num_image_pages = struct.unpack('>H', r[26:26+2])[0]
|
||||
|
@ -302,7 +360,7 @@ class EreaderProcessor(object):
|
|||
sect = self.section_reader(self.first_image_page + i)
|
||||
name = sect[4:4+32].strip('\0')
|
||||
data = sect[62:]
|
||||
return sanitizeFileName(name), data
|
||||
return sanitizeFileName(unicode(name,'windows-1252')), data
|
||||
|
||||
|
||||
# def getChapterNamePMLOffsetData(self):
|
||||
|
@ -399,60 +457,53 @@ class EreaderProcessor(object):
|
|||
return r
|
||||
|
||||
def cleanPML(pml):
|
||||
# Convert special characters to proper PML code. High ASCII start at (\x80, \a128) and go up to (\xff, \a255)
|
||||
# Convert special characters to proper PML code. High ASCII start at (\x80, \a128) and go up to (\xff, \a255)
|
||||
pml2 = pml
|
||||
for k in xrange(128,256):
|
||||
badChar = chr(k)
|
||||
pml2 = pml2.replace(badChar, '\\a%03d' % k)
|
||||
return pml2
|
||||
|
||||
def convertEreaderToPml(infile, name, cc, outdir):
|
||||
if not os.path.exists(outdir):
|
||||
os.makedirs(outdir)
|
||||
def decryptBook(infile, outpath, make_pmlz, user_key):
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
print " Decoding File"
|
||||
sect = Sectionizer(infile, 'PNRdPPrs')
|
||||
er = EreaderProcessor(sect, name, cc)
|
||||
|
||||
if er.getNumImages() > 0:
|
||||
print " Extracting images"
|
||||
imagedir = bookname + '_img/'
|
||||
imagedirpath = os.path.join(outdir,imagedir)
|
||||
if not os.path.exists(imagedirpath):
|
||||
os.makedirs(imagedirpath)
|
||||
for i in xrange(er.getNumImages()):
|
||||
name, contents = er.getImage(i)
|
||||
file(os.path.join(imagedirpath, name), 'wb').write(contents)
|
||||
|
||||
print " Extracting pml"
|
||||
pml_string = er.getText()
|
||||
pmlfilename = bookname + ".pml"
|
||||
file(os.path.join(outdir, pmlfilename),'wb').write(cleanPML(pml_string))
|
||||
|
||||
# bkinfo = er.getBookInfo()
|
||||
# if bkinfo != '':
|
||||
# print " Extracting book meta information"
|
||||
# file(os.path.join(outdir, 'bookinfo.txt'),'wb').write(bkinfo)
|
||||
|
||||
|
||||
|
||||
def decryptBook(infile, outdir, name, cc, make_pmlz):
|
||||
if make_pmlz :
|
||||
# ignore specified outdir, use tempdir instead
|
||||
if make_pmlz:
|
||||
# outpath is actually pmlz name
|
||||
pmlzname = outpath
|
||||
outdir = tempfile.mkdtemp()
|
||||
imagedirpath = os.path.join(outdir,u"images")
|
||||
else:
|
||||
pmlzname = None
|
||||
outdir = outpath
|
||||
imagedirpath = os.path.join(outdir,bookname + u"_img")
|
||||
|
||||
try:
|
||||
print "Processing..."
|
||||
convertEreaderToPml(infile, name, cc, outdir)
|
||||
if make_pmlz :
|
||||
if not os.path.exists(outdir):
|
||||
os.makedirs(outdir)
|
||||
print u"Decoding File"
|
||||
sect = Sectionizer(infile, 'PNRdPPrs')
|
||||
er = EreaderProcessor(sect, user_key)
|
||||
|
||||
if er.getNumImages() > 0:
|
||||
print u"Extracting images"
|
||||
if not os.path.exists(imagedirpath):
|
||||
os.makedirs(imagedirpath)
|
||||
for i in xrange(er.getNumImages()):
|
||||
name, contents = er.getImage(i)
|
||||
file(os.path.join(imagedirpath, name), 'wb').write(contents)
|
||||
|
||||
print u"Extracting pml"
|
||||
pml_string = er.getText()
|
||||
pmlfilename = bookname + ".pml"
|
||||
file(os.path.join(outdir, pmlfilename),'wb').write(cleanPML(pml_string))
|
||||
if pmlzname is not None:
|
||||
import zipfile
|
||||
import shutil
|
||||
print " Creating PMLZ file"
|
||||
zipname = infile[:-4] + '.pmlz'
|
||||
myZipFile = zipfile.ZipFile(zipname,'w',zipfile.ZIP_STORED, False)
|
||||
print u"Creating PMLZ file {0}".format(os.path.basename(pmlzname))
|
||||
myZipFile = zipfile.ZipFile(pmlzname,'w',zipfile.ZIP_STORED, False)
|
||||
list = os.listdir(outdir)
|
||||
for file in list:
|
||||
localname = file
|
||||
filePath = os.path.join(outdir,file)
|
||||
for filename in list:
|
||||
localname = filename
|
||||
filePath = os.path.join(outdir,filename)
|
||||
if os.path.isfile(filePath):
|
||||
myZipFile.write(filePath, localname)
|
||||
elif os.path.isdir(filePath):
|
||||
|
@ -466,36 +517,46 @@ def decryptBook(infile, outdir, name, cc, make_pmlz):
|
|||
myZipFile.close()
|
||||
# remove temporary directory
|
||||
shutil.rmtree(outdir, True)
|
||||
print 'output is %s' % zipname
|
||||
print u"Output is {0}".format(pmlzname)
|
||||
else :
|
||||
print 'output in %s' % outdir
|
||||
print u"Output is in {0}".format(outdir)
|
||||
print "done"
|
||||
except ValueError, e:
|
||||
print "Error: %s" % e
|
||||
print u"Error: {0}".format(e.args[0])
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def usage():
|
||||
print "Converts DRMed eReader books to PML Source"
|
||||
print "Usage:"
|
||||
print " erdr2pml [options] infile.pdb [outdir] \"your name\" credit_card_number "
|
||||
print " "
|
||||
print "Options: "
|
||||
print " -h prints this message"
|
||||
print " --make-pmlz create PMLZ instead of using output directory"
|
||||
print " "
|
||||
print "Note:"
|
||||
print " if ommitted, outdir defaults based on 'infile.pdb'"
|
||||
print " It's enough to enter the last 8 digits of the credit card number"
|
||||
print u"Converts DRMed eReader books to PML Source"
|
||||
print u"Usage:"
|
||||
print u" erdr2pml [options] infile.pdb [outpath] \"your name\" credit_card_number"
|
||||
print u" "
|
||||
print u"Options: "
|
||||
print u" -h prints this message"
|
||||
print u" -p create PMLZ instead of source folder"
|
||||
print u" --make-pmlz create PMLZ instead of source folder"
|
||||
print u" "
|
||||
print u"Note:"
|
||||
print u" if outpath is ommitted, creates source in 'infile_Source' folder"
|
||||
print u" if outpath is ommitted and pmlz option, creates PMLZ 'infile.pmlz'"
|
||||
print u" if source folder created, images are in infile_img folder"
|
||||
print u" if pmlz file created, images are in images folder"
|
||||
print u" It's enough to enter the last 8 digits of the credit card number"
|
||||
return
|
||||
|
||||
def getuser_key(name,cc):
|
||||
newname = "".join(c for c in name.lower() if c >= 'a' and c <= 'z' or c >= '0' and c <= '9')
|
||||
cc = cc.replace(" ","")
|
||||
return struct.pack('>LL', binascii.crc32(newname) & 0xffffffff,binascii.crc32(cc[-8:])& 0xffffffff)
|
||||
|
||||
def cli_main(argv=unicode_argv()):
|
||||
print u"eRdr2Pml v{0}. Copyright © 2009–2012 The Dark Reverser et al.".format(__version__)
|
||||
|
||||
def main(argv=None):
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], "h", ["make-pmlz"])
|
||||
opts, args = getopt.getopt(argv[1:], "hp", ["make-pmlz"])
|
||||
except getopt.GetoptError, err:
|
||||
print str(err)
|
||||
print err.args[0]
|
||||
usage()
|
||||
return 1
|
||||
make_pmlz = False
|
||||
|
@ -503,24 +564,31 @@ def main(argv=None):
|
|||
if o == "-h":
|
||||
usage()
|
||||
return 0
|
||||
elif o == "-p":
|
||||
make_pmlz = True
|
||||
elif o == "--make-pmlz":
|
||||
make_pmlz = True
|
||||
|
||||
print "eRdr2Pml v%s. Copyright (c) 2009 The Dark Reverser" % __version__
|
||||
|
||||
if len(args)!=3 and len(args)!=4:
|
||||
usage()
|
||||
return 1
|
||||
|
||||
if len(args)==3:
|
||||
infile, name, cc = args[0], args[1], args[2]
|
||||
outdir = infile[:-4] + '_Source'
|
||||
infile, name, cc = args
|
||||
if make_pmlz:
|
||||
outpath = os.path.splitext(infile)[0] + u".pmlz"
|
||||
else:
|
||||
outpath = os.path.splitext(infile)[0] + u"_Source"
|
||||
elif len(args)==4:
|
||||
infile, outdir, name, cc = args[0], args[1], args[2], args[3]
|
||||
infile, outpath, name, cc = args
|
||||
|
||||
return decryptBook(infile, outdir, name, cc, make_pmlz)
|
||||
print getuser_key(name,cc).encode('hex')
|
||||
|
||||
return decryptBook(infile, outpath, make_pmlz, getuser_key(name,cc))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
sys.exit(main())
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
|
||||
|
|
|
@ -1,13 +1,25 @@
|
|||
#! /usr/bin/python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ignobleepub.pyw, version 3.5
|
||||
# ignobleepub.pyw, version 3.6
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# To run this program install Python 2.6 from <http://www.python.org/download/>
|
||||
# and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
|
||||
# (make sure to install the version for Python 2.6). Save this script file as
|
||||
# ignobleepub.pyw and double-click on it to run it.
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# from <http://www.python.org/download/> and PyCrypto from
|
||||
# <http://www.voidspace.org.uk/python/modules.shtml#pycrypto> (make sure to
|
||||
# install the version for Python 2.6). Save this script file as
|
||||
# ineptepub.pyw and double-click on it to run it.
|
||||
#
|
||||
# Mac OS X users: Save this script file as ineptepub.pyw. You can run this
|
||||
# program from the command line (pythonw ineptepub.pyw) or by double-clicking
|
||||
# it when it has been associated with PythonLauncher.
|
||||
|
||||
# Revision history:
|
||||
# 1 - Initial release
|
||||
|
@ -18,21 +30,83 @@ from __future__ import with_statement
|
|||
# 3.3 - On Windows try PyCrypto first and OpenSSL next
|
||||
# 3.4 - Modify interace to allow use with import
|
||||
# 3.5 - Fix for potential problem with PyCrypto
|
||||
# 3.6 - Revised to allow use in calibre plugins to eliminate need for duplicate code
|
||||
|
||||
"""
|
||||
Decrypt Barnes & Noble encrypted ePub books.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "3.6"
|
||||
|
||||
import sys
|
||||
import os
|
||||
import traceback
|
||||
import zlib
|
||||
import zipfile
|
||||
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||
from contextlib import closing
|
||||
import xml.etree.ElementTree as etree
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
try:
|
||||
from calibre.constants import iswindows, isosx
|
||||
except:
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
return [u"ineptepub.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
class IGNOBLEError(Exception):
|
||||
pass
|
||||
|
@ -42,10 +116,11 @@ def _load_crypto_libcrypto():
|
|||
Structure, c_ulong, create_string_buffer, cast
|
||||
from ctypes.util import find_library
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
if iswindows:
|
||||
libcrypto = find_library('libeay32')
|
||||
else:
|
||||
libcrypto = find_library('crypto')
|
||||
|
||||
if libcrypto is None:
|
||||
raise IGNOBLEError('libcrypto not found')
|
||||
libcrypto = CDLL(libcrypto)
|
||||
|
@ -66,9 +141,6 @@ def _load_crypto_libcrypto():
|
|||
func.argtypes = argtypes
|
||||
return func
|
||||
|
||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
||||
c_int])
|
||||
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
|
||||
[c_char_p, c_int, AES_KEY_p])
|
||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||
|
@ -123,13 +195,6 @@ def _load_crypto():
|
|||
|
||||
AES = _load_crypto()
|
||||
|
||||
|
||||
|
||||
"""
|
||||
Decrypt Barnes & Noble ADEPT encrypted EPUB books.
|
||||
"""
|
||||
|
||||
|
||||
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
|
||||
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
||||
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
||||
|
@ -144,7 +209,6 @@ class ZipInfo(zipfile.ZipInfo):
|
|||
class Decryptor(object):
|
||||
def __init__(self, bookkey, encryption):
|
||||
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
|
||||
# self._aes = AES.new(bookkey, AES.MODE_CBC, '\x00'*16)
|
||||
self._aes = AES(bookkey)
|
||||
encryption = etree.fromstring(encryption)
|
||||
self._encrypted = encrypted = set()
|
||||
|
@ -152,8 +216,8 @@ class Decryptor(object):
|
|||
enc('CipherReference'))
|
||||
for elem in encryption.findall(expr):
|
||||
path = elem.get('URI', None)
|
||||
path = path.encode('utf-8')
|
||||
if path is not None:
|
||||
path = path.encode('utf-8')
|
||||
encrypted.add(path)
|
||||
|
||||
def decompress(self, bytes):
|
||||
|
@ -171,167 +235,186 @@ class Decryptor(object):
|
|||
data = self.decompress(data)
|
||||
return data
|
||||
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = Tkinter.Label(self, text='Select files for decryption')
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
Tkinter.Label(body, text='Key file').grid(row=0)
|
||||
self.keypath = Tkinter.Entry(body, width=30)
|
||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||
if os.path.exists('bnepubkey.b64'):
|
||||
self.keypath.insert(0, 'bnepubkey.b64')
|
||||
button = Tkinter.Button(body, text="...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
Tkinter.Label(body, text='Input file').grid(row=1)
|
||||
self.inpath = Tkinter.Entry(body, width=30)
|
||||
self.inpath.grid(row=1, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
Tkinter.Label(body, text='Output file').grid(row=2)
|
||||
self.outpath = Tkinter.Entry(body, width=30)
|
||||
self.outpath.grid(row=2, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_outpath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = Tkinter.Button(
|
||||
buttons, text="Decrypt", width=10, command=self.decrypt)
|
||||
botton.pack(side=Tkconstants.LEFT)
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
button = Tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quit)
|
||||
button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkFileDialog.askopenfilename(
|
||||
parent=None, title='Select B&N EPUB key file',
|
||||
defaultextension='.b64',
|
||||
filetypes=[('base64-encoded files', '.b64'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(keypath)
|
||||
self.keypath.delete(0, Tkconstants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
def get_inpath(self):
|
||||
inpath = tkFileDialog.askopenfilename(
|
||||
parent=None, title='Select B&N-encrypted EPUB file to decrypt',
|
||||
defaultextension='.epub', filetypes=[('EPUB files', '.epub'),
|
||||
('All files', '.*')])
|
||||
if inpath:
|
||||
inpath = os.path.normpath(inpath)
|
||||
self.inpath.delete(0, Tkconstants.END)
|
||||
self.inpath.insert(0, inpath)
|
||||
return
|
||||
|
||||
def get_outpath(self):
|
||||
outpath = tkFileDialog.asksaveasfilename(
|
||||
parent=None, title='Select unencrypted EPUB file to produce',
|
||||
defaultextension='.epub', filetypes=[('EPUB files', '.epub'),
|
||||
('All files', '.*')])
|
||||
if outpath:
|
||||
outpath = os.path.normpath(outpath)
|
||||
self.outpath.delete(0, Tkconstants.END)
|
||||
self.outpath.insert(0, outpath)
|
||||
return
|
||||
|
||||
def decrypt(self):
|
||||
keypath = self.keypath.get()
|
||||
inpath = self.inpath.get()
|
||||
outpath = self.outpath.get()
|
||||
if not keypath or not os.path.exists(keypath):
|
||||
self.status['text'] = 'Specified key file does not exist'
|
||||
return
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = 'Specified input file does not exist'
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = 'Output file not specified'
|
||||
return
|
||||
if inpath == outpath:
|
||||
self.status['text'] = 'Must have different input and output files'
|
||||
return
|
||||
argv = [sys.argv[0], keypath, inpath, outpath]
|
||||
self.status['text'] = 'Decrypting...'
|
||||
try:
|
||||
cli_main(argv)
|
||||
except Exception, e:
|
||||
self.status['text'] = 'Error: ' + str(e)
|
||||
return
|
||||
self.status['text'] = 'File successfully decrypted'
|
||||
|
||||
|
||||
def decryptBook(keypath, inpath, outpath):
|
||||
with open(keypath, 'rb') as f:
|
||||
keyb64 = f.read()
|
||||
key = keyb64.decode('base64')[:16]
|
||||
# aes = AES.new(key, AES.MODE_CBC, '\x00'*16)
|
||||
aes = AES(key)
|
||||
|
||||
# check file to make check whether it's probably an Adobe Adept encrypted ePub
|
||||
def ignobleBook(inpath):
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
raise IGNOBLEError('%s: not an B&N ADEPT EPUB' % (inpath,))
|
||||
return False
|
||||
try:
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) == 64:
|
||||
return True
|
||||
except:
|
||||
# if we couldn't check, assume it is
|
||||
return True
|
||||
return False
|
||||
|
||||
# return error code and error message duple
|
||||
def decryptBook(keyb64, inpath, outpath):
|
||||
if AES is None:
|
||||
# 1 means don't try again
|
||||
return (1, u"PyCrypto or OpenSSL must be installed.")
|
||||
key = keyb64.decode('base64')[:16]
|
||||
aes = AES(key)
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
return (1, u"Not a secure Barnes & Noble ePub.")
|
||||
for name in META_NAMES:
|
||||
namelist.remove(name)
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
bookkey = aes.decrypt(bookkey.decode('base64'))
|
||||
bookkey = bookkey[:-ord(bookkey[-1])]
|
||||
encryption = inf.read('META-INF/encryption.xml')
|
||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
||||
outf.writestr(zi, inf.read('mimetype'))
|
||||
for path in namelist:
|
||||
data = inf.read(path)
|
||||
outf.writestr(path, decryptor.decrypt(path, data))
|
||||
return 0
|
||||
try:
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) != 64:
|
||||
return (1, u"Not a secure Barnes & Noble ePub.")
|
||||
bookkey = aes.decrypt(bookkey.decode('base64'))
|
||||
bookkey = bookkey[:-ord(bookkey[-1])]
|
||||
encryption = inf.read('META-INF/encryption.xml')
|
||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
||||
outf.writestr(zi, inf.read('mimetype'))
|
||||
for path in namelist:
|
||||
data = inf.read(path)
|
||||
outf.writestr(path, decryptor.decrypt(path, data))
|
||||
except Exception, e:
|
||||
return (2, u"{0}.".format(e.args[0]))
|
||||
return (0, u"Success")
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
if AES is None:
|
||||
print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \
|
||||
"separately. Read the top-of-script comment for details." % \
|
||||
(progname,)
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,)
|
||||
print u"usage: {0} <keyfile.der> <inbook.epub> <outbook.epub>".format(progname)
|
||||
return 1
|
||||
keypath, inpath, outpath = argv[1:]
|
||||
return decryptBook(keypath, inpath, outpath)
|
||||
|
||||
userkey = open(keypath,'rb').read()
|
||||
result = decryptBook(userkey, inpath, outpath)
|
||||
print result[1]
|
||||
return result[0]
|
||||
|
||||
def gui_main():
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import traceback
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = Tkinter.Label(self, text=u"Select files for decryption")
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
Tkinter.Label(body, text=u"Key file").grid(row=0)
|
||||
self.keypath = Tkinter.Entry(body, width=30)
|
||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||
if os.path.exists(u"bnepubkey.b64"):
|
||||
self.keypath.insert(0, u"bnepubkey.b64")
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
Tkinter.Label(body, text=u"Input file").grid(row=1)
|
||||
self.inpath = Tkinter.Entry(body, width=30)
|
||||
self.inpath.grid(row=1, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
Tkinter.Label(body, text=u"Output file").grid(row=2)
|
||||
self.outpath = Tkinter.Entry(body, width=30)
|
||||
self.outpath.grid(row=2, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_outpath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = Tkinter.Button(
|
||||
buttons, text=u"Decrypt", width=10, command=self.decrypt)
|
||||
botton.pack(side=Tkconstants.LEFT)
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
button = Tkinter.Button(
|
||||
buttons, text=u"Quit", width=10, command=self.quit)
|
||||
button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkFileDialog.askopenfilename(
|
||||
parent=None, title=u"Select Barnes & Noble \'.b64\' key file",
|
||||
defaultextension=u".b64",
|
||||
filetypes=[('base64-encoded files', '.b64'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(keypath)
|
||||
self.keypath.delete(0, Tkconstants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
def get_inpath(self):
|
||||
inpath = tkFileDialog.askopenfilename(
|
||||
parent=None, title=u"Select B&N-encrypted ePub file to decrypt",
|
||||
defaultextension=u".epub", filetypes=[('ePub files', '.epub')])
|
||||
if inpath:
|
||||
inpath = os.path.normpath(inpath)
|
||||
self.inpath.delete(0, Tkconstants.END)
|
||||
self.inpath.insert(0, inpath)
|
||||
return
|
||||
|
||||
def get_outpath(self):
|
||||
outpath = tkFileDialog.asksaveasfilename(
|
||||
parent=None, title=u"Select unencrypted ePub file to produce",
|
||||
defaultextension=u".epub", filetypes=[('ePub files', '.epub')])
|
||||
if outpath:
|
||||
outpath = os.path.normpath(outpath)
|
||||
self.outpath.delete(0, Tkconstants.END)
|
||||
self.outpath.insert(0, outpath)
|
||||
return
|
||||
|
||||
def decrypt(self):
|
||||
keypath = self.keypath.get()
|
||||
inpath = self.inpath.get()
|
||||
outpath = self.outpath.get()
|
||||
if not keypath or not os.path.exists(keypath):
|
||||
self.status['text'] = u"Specified key file does not exist"
|
||||
return
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = u"Specified input file does not exist"
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = u"Output file not specified"
|
||||
return
|
||||
if inpath == outpath:
|
||||
self.status['text'] = u"Must have different input and output files"
|
||||
return
|
||||
userkey = open(keypath,'rb').read()
|
||||
self.status['text'] = u"Decrypting..."
|
||||
try:
|
||||
decrypt_status = decryptBook(userkey, inpath, outpath)
|
||||
except Exception, e:
|
||||
self.status['text'] = u"Error: {0}".format(e.args[0])
|
||||
return
|
||||
if decrypt_status[0] == 0:
|
||||
self.status['text'] = u"File successfully decrypted"
|
||||
else:
|
||||
self.status['text'] = decrypt_status[1]
|
||||
|
||||
root = Tkinter.Tk()
|
||||
if AES is None:
|
||||
root.withdraw()
|
||||
tkMessageBox.showerror(
|
||||
"Ignoble EPUB Decrypter",
|
||||
"This script requires OpenSSL or PyCrypto, which must be installed "
|
||||
"separately. Read the top-of-script comment for details.")
|
||||
return 1
|
||||
root.title('Ignoble EPUB Decrypter')
|
||||
root.title(u"Barnes & Noble ePub Decrypter v.{0}".format(__version__))
|
||||
root.resizable(True, False)
|
||||
root.minsize(300, 0)
|
||||
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
||||
|
|
|
@ -1,13 +1,25 @@
|
|||
#! /usr/bin/python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ignoblekeygen.pyw, version 2.4
|
||||
# ignoblekeygen.pyw, version 2.5
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# To run this program install Python 2.6 from <http://www.python.org/download/>
|
||||
# and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
|
||||
# (make sure to install the version for Python 2.6). Save this script file as
|
||||
# ignoblekeygen.pyw and double-click on it to run it.
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# from <http://www.python.org/download/> and PyCrypto from
|
||||
# <http://www.voidspace.org.uk/python/modules.shtml#pycrypto> (make sure to
|
||||
# install the version for Python 2.6). Save this script file as
|
||||
# ignoblekeygen.pyw and double-click on it to run it.
|
||||
#
|
||||
# Mac OS X users: Save this script file as ignoblekeygen.pyw. You can run this
|
||||
# program from the command line (pythonw ignoblekeygen.pyw) or by double-clicking
|
||||
# it when it has been associated with PythonLauncher.
|
||||
|
||||
# Revision history:
|
||||
# 1 - Initial release
|
||||
|
@ -16,36 +28,92 @@ from __future__ import with_statement
|
|||
# 2.2 - On Windows try PyCrypto first and then OpenSSL next
|
||||
# 2.3 - Modify interface to allow use of import
|
||||
# 2.4 - Improvements to UI and now works in plugins
|
||||
# 2.5 - Additional improvement for unicode and plugin support
|
||||
|
||||
"""
|
||||
Generate Barnes & Noble EPUB user key from name and credit card number.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "2.5"
|
||||
|
||||
import sys
|
||||
import os
|
||||
import hashlib
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return [u"ignoblekeygen.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
# use openssl's libcrypt if it exists in place of pycrypto
|
||||
# code extracted from the Adobe Adept DRM removal code also by I HeartCabbages
|
||||
class IGNOBLEError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _load_crypto_libcrypto():
|
||||
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
||||
Structure, c_ulong, create_string_buffer, cast
|
||||
from ctypes.util import find_library
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
if iswindows:
|
||||
libcrypto = find_library('libeay32')
|
||||
else:
|
||||
libcrypto = find_library('crypto')
|
||||
|
||||
if libcrypto is None:
|
||||
print 'libcrypto not found'
|
||||
raise IGNOBLEError('libcrypto not found')
|
||||
libcrypto = CDLL(libcrypto)
|
||||
|
||||
|
@ -70,6 +138,7 @@ def _load_crypto_libcrypto():
|
|||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
||||
c_int])
|
||||
|
||||
class AES(object):
|
||||
def __init__(self, userkey, iv):
|
||||
self._blocksize = len(userkey)
|
||||
|
@ -88,7 +157,6 @@ def _load_crypto_libcrypto():
|
|||
|
||||
return AES
|
||||
|
||||
|
||||
def _load_crypto_pycrypto():
|
||||
from Crypto.Cipher import AES as _AES
|
||||
|
||||
|
@ -120,25 +188,28 @@ def normalize_name(name):
|
|||
return ''.join(x for x in name.lower() if x != ' ')
|
||||
|
||||
|
||||
def generate_keyfile(name, ccn, outpath):
|
||||
def generate_key(name, ccn):
|
||||
# remove spaces and case from name and CC numbers.
|
||||
if type(name)==unicode:
|
||||
name = name.encode('utf-8')
|
||||
if type(ccn)==unicode:
|
||||
ccn = ccn.encode('utf-8')
|
||||
|
||||
name = normalize_name(name) + '\x00'
|
||||
ccn = normalize_name(ccn) + '\x00'
|
||||
|
||||
|
||||
name_sha = hashlib.sha1(name).digest()[:16]
|
||||
ccn_sha = hashlib.sha1(ccn).digest()[:16]
|
||||
both_sha = hashlib.sha1(name + ccn).digest()
|
||||
aes = AES(ccn_sha, name_sha)
|
||||
crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c))
|
||||
userkey = hashlib.sha1(crypt).digest()
|
||||
with open(outpath, 'wb') as f:
|
||||
f.write(userkey.encode('base64'))
|
||||
return userkey
|
||||
return userkey.encode('base64')
|
||||
|
||||
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
if AES is None:
|
||||
print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \
|
||||
|
@ -146,10 +217,11 @@ def cli_main(argv=sys.argv):
|
|||
(progname,)
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print "usage: %s NAME CC# OUTFILE" % (progname,)
|
||||
print u"usage: {0} <Name> <CC#> <keyfileout.b64>".format(progname)
|
||||
return 1
|
||||
name, ccn, outpath = argv[1:]
|
||||
generate_keyfile(name, ccn, outpath)
|
||||
name, ccn, keypath = argv[1:]
|
||||
userkey = generate_key(name, ccn)
|
||||
open(keypath,'wb').write(userkey)
|
||||
return 0
|
||||
|
||||
|
||||
|
@ -162,38 +234,38 @@ def gui_main():
|
|||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = Tkinter.Label(self, text='Enter parameters')
|
||||
self.status = Tkinter.Label(self, text=u"Enter parameters")
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
Tkinter.Label(body, text='Account Name').grid(row=0)
|
||||
Tkinter.Label(body, text=u"Account Name").grid(row=0)
|
||||
self.name = Tkinter.Entry(body, width=40)
|
||||
self.name.grid(row=0, column=1, sticky=sticky)
|
||||
Tkinter.Label(body, text='CC#').grid(row=1)
|
||||
Tkinter.Label(body, text=u"CC#").grid(row=1)
|
||||
self.ccn = Tkinter.Entry(body, width=40)
|
||||
self.ccn.grid(row=1, column=1, sticky=sticky)
|
||||
Tkinter.Label(body, text='Output file').grid(row=2)
|
||||
Tkinter.Label(body, text=u"Output file").grid(row=2)
|
||||
self.keypath = Tkinter.Entry(body, width=40)
|
||||
self.keypath.grid(row=2, column=1, sticky=sticky)
|
||||
self.keypath.insert(2, 'bnepubkey.b64')
|
||||
button = Tkinter.Button(body, text="...", command=self.get_keypath)
|
||||
self.keypath.insert(2, u"bnepubkey.b64")
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = Tkinter.Button(
|
||||
buttons, text="Generate", width=10, command=self.generate)
|
||||
buttons, text=u"Generate", width=10, command=self.generate)
|
||||
botton.pack(side=Tkconstants.LEFT)
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
button = Tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quit)
|
||||
buttons, text=u"Quit", width=10, command=self.quit)
|
||||
button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkFileDialog.asksaveasfilename(
|
||||
parent=None, title='Select B&N EPUB key file to produce',
|
||||
defaultextension='.b64',
|
||||
parent=None, title=u"Select B&N ePub key file to produce",
|
||||
defaultextension=u".b64",
|
||||
filetypes=[('base64-encoded files', '.b64'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
|
@ -201,27 +273,28 @@ def gui_main():
|
|||
self.keypath.delete(0, Tkconstants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
|
||||
def generate(self):
|
||||
name = self.name.get()
|
||||
ccn = self.ccn.get()
|
||||
keypath = self.keypath.get()
|
||||
if not name:
|
||||
self.status['text'] = 'Name not specified'
|
||||
self.status['text'] = u"Name not specified"
|
||||
return
|
||||
if not ccn:
|
||||
self.status['text'] = 'Credit card number not specified'
|
||||
self.status['text'] = u"Credit card number not specified"
|
||||
return
|
||||
if not keypath:
|
||||
self.status['text'] = 'Output keyfile path not specified'
|
||||
self.status['text'] = u"Output keyfile path not specified"
|
||||
return
|
||||
self.status['text'] = 'Generating...'
|
||||
self.status['text'] = u"Generating..."
|
||||
try:
|
||||
generate_keyfile(name, ccn, keypath)
|
||||
userkey = generate_key(name, ccn)
|
||||
except Exception, e:
|
||||
self.status['text'] = 'Error: ' + str(e)
|
||||
self.status['text'] = u"Error: (0}".format(e.args[0])
|
||||
return
|
||||
self.status['text'] = 'Keyfile successfully generated'
|
||||
open(keypath,'wb').write(userkey)
|
||||
self.status['text'] = u"Keyfile successfully generated"
|
||||
|
||||
root = Tkinter.Tk()
|
||||
if AES is None:
|
||||
|
@ -231,7 +304,7 @@ def gui_main():
|
|||
"This script requires OpenSSL or PyCrypto, which must be installed "
|
||||
"separately. Read the top-of-script comment for details.")
|
||||
return 1
|
||||
root.title('Ignoble EPUB Keyfile Generator')
|
||||
root.title(u"Barnes & Noble ePub Keyfile Generator v.{0}".format(__version__))
|
||||
root.resizable(True, False)
|
||||
root.minsize(300, 0)
|
||||
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||
|
@ -240,5 +313,7 @@ def gui_main():
|
|||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
||||
|
|
|
@ -3,11 +3,13 @@
|
|||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ineptepub.pyw, version 5.6
|
||||
# Copyright © 2009-2010 i♥cabbages
|
||||
# ineptepub.pyw, version 5.8
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||
# later. <http://www.gnu.org/licenses/>
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# from <http://www.python.org/download/> and PyCrypto from
|
||||
|
@ -31,24 +33,83 @@ from __future__ import with_statement
|
|||
# 5.5 - On Windows try PyCrypto first, OpenSSL next
|
||||
# 5.6 - Modify interface to allow use with import
|
||||
# 5.7 - Fix for potential problem with PyCrypto
|
||||
# 5.8 - Revised to allow use in calibre plugins to eliminate need for duplicate code
|
||||
|
||||
"""
|
||||
Decrypt Adobe ADEPT-encrypted EPUB books.
|
||||
Decrypt Adobe Digital Editions encrypted ePub books.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "5.8"
|
||||
|
||||
import sys
|
||||
import os
|
||||
import traceback
|
||||
import zlib
|
||||
import zipfile
|
||||
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||
from contextlib import closing
|
||||
import xml.etree.ElementTree as etree
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
try:
|
||||
from calibre.constants import iswindows, isosx
|
||||
except:
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
return [u"ineptepub.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
class ADEPTError(Exception):
|
||||
pass
|
||||
|
@ -58,7 +119,7 @@ def _load_crypto_libcrypto():
|
|||
Structure, c_ulong, create_string_buffer, cast
|
||||
from ctypes.util import find_library
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
if iswindows:
|
||||
libcrypto = find_library('libeay32')
|
||||
else:
|
||||
libcrypto = find_library('crypto')
|
||||
|
@ -272,6 +333,7 @@ def _load_crypto():
|
|||
except (ImportError, ADEPTError):
|
||||
pass
|
||||
return (AES, RSA)
|
||||
|
||||
AES, RSA = _load_crypto()
|
||||
|
||||
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
|
||||
|
@ -314,158 +376,181 @@ class Decryptor(object):
|
|||
data = self.decompress(data)
|
||||
return data
|
||||
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = Tkinter.Label(self, text='Select files for decryption')
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
Tkinter.Label(body, text='Key file').grid(row=0)
|
||||
self.keypath = Tkinter.Entry(body, width=30)
|
||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||
if os.path.exists('adeptkey.der'):
|
||||
self.keypath.insert(0, 'adeptkey.der')
|
||||
button = Tkinter.Button(body, text="...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
Tkinter.Label(body, text='Input file').grid(row=1)
|
||||
self.inpath = Tkinter.Entry(body, width=30)
|
||||
self.inpath.grid(row=1, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
Tkinter.Label(body, text='Output file').grid(row=2)
|
||||
self.outpath = Tkinter.Entry(body, width=30)
|
||||
self.outpath.grid(row=2, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_outpath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = Tkinter.Button(
|
||||
buttons, text="Decrypt", width=10, command=self.decrypt)
|
||||
botton.pack(side=Tkconstants.LEFT)
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
button = Tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quit)
|
||||
button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkFileDialog.askopenfilename(
|
||||
parent=None, title='Select ADEPT key file',
|
||||
defaultextension='.der', filetypes=[('DER-encoded files', '.der'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(keypath)
|
||||
self.keypath.delete(0, Tkconstants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
def get_inpath(self):
|
||||
inpath = tkFileDialog.askopenfilename(
|
||||
parent=None, title='Select ADEPT-encrypted EPUB file to decrypt',
|
||||
defaultextension='.epub', filetypes=[('EPUB files', '.epub'),
|
||||
('All files', '.*')])
|
||||
if inpath:
|
||||
inpath = os.path.normpath(inpath)
|
||||
self.inpath.delete(0, Tkconstants.END)
|
||||
self.inpath.insert(0, inpath)
|
||||
return
|
||||
|
||||
def get_outpath(self):
|
||||
outpath = tkFileDialog.asksaveasfilename(
|
||||
parent=None, title='Select unencrypted EPUB file to produce',
|
||||
defaultextension='.epub', filetypes=[('EPUB files', '.epub'),
|
||||
('All files', '.*')])
|
||||
if outpath:
|
||||
outpath = os.path.normpath(outpath)
|
||||
self.outpath.delete(0, Tkconstants.END)
|
||||
self.outpath.insert(0, outpath)
|
||||
return
|
||||
|
||||
def decrypt(self):
|
||||
keypath = self.keypath.get()
|
||||
inpath = self.inpath.get()
|
||||
outpath = self.outpath.get()
|
||||
if not keypath or not os.path.exists(keypath):
|
||||
self.status['text'] = 'Specified key file does not exist'
|
||||
return
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = 'Specified input file does not exist'
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = 'Output file not specified'
|
||||
return
|
||||
if inpath == outpath:
|
||||
self.status['text'] = 'Must have different input and output files'
|
||||
return
|
||||
argv = [sys.argv[0], keypath, inpath, outpath]
|
||||
self.status['text'] = 'Decrypting...'
|
||||
try:
|
||||
cli_main(argv)
|
||||
except Exception, e:
|
||||
self.status['text'] = 'Error: ' + str(e)
|
||||
return
|
||||
self.status['text'] = 'File successfully decrypted'
|
||||
|
||||
|
||||
def decryptBook(keypath, inpath, outpath):
|
||||
with open(keypath, 'rb') as f:
|
||||
keyder = f.read()
|
||||
rsa = RSA(keyder)
|
||||
# check file to make check whether it's probably an Adobe Adept encrypted ePub
|
||||
def adeptBook(inpath):
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
raise ADEPTError('%s: not an ADEPT EPUB' % (inpath,))
|
||||
return False
|
||||
try:
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) == 172:
|
||||
return True
|
||||
except:
|
||||
# if we couldn't check, assume it is
|
||||
return True
|
||||
return False
|
||||
|
||||
def decryptBook(userkey, inpath, outpath):
|
||||
if AES is None:
|
||||
raise ADEPTError(u"PyCrypto or OpenSSL must be installed.")
|
||||
rsa = RSA(userkey)
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
print u"{0:s} is DRM-free.".format(os.path.basename(inpath))
|
||||
return 1
|
||||
for name in META_NAMES:
|
||||
namelist.remove(name)
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
bookkey = rsa.decrypt(bookkey.decode('base64'))
|
||||
# Padded as per RSAES-PKCS1-v1_5
|
||||
if bookkey[-17] != '\x00':
|
||||
raise ADEPTError('problem decrypting session key')
|
||||
encryption = inf.read('META-INF/encryption.xml')
|
||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
||||
outf.writestr(zi, inf.read('mimetype'))
|
||||
for path in namelist:
|
||||
data = inf.read(path)
|
||||
outf.writestr(path, decryptor.decrypt(path, data))
|
||||
try:
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) != 172:
|
||||
print u"{0:s} is not a secure Adobe Adept ePub.".format(os.path.basename(inpath))
|
||||
return 1
|
||||
bookkey = rsa.decrypt(bookkey.decode('base64'))
|
||||
# Padded as per RSAES-PKCS1-v1_5
|
||||
if bookkey[-17] != '\x00':
|
||||
print u"Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath))
|
||||
return 2
|
||||
encryption = inf.read('META-INF/encryption.xml')
|
||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
||||
outf.writestr(zi, inf.read('mimetype'))
|
||||
for path in namelist:
|
||||
data = inf.read(path)
|
||||
outf.writestr(path, decryptor.decrypt(path, data))
|
||||
except:
|
||||
print u"Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc())
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
if AES is None:
|
||||
print "%s: This script requires OpenSSL or PyCrypto, which must be" \
|
||||
" installed separately. Read the top-of-script comment for" \
|
||||
" details." % (progname,)
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,)
|
||||
print u"usage: {0} <keyfile.der> <inbook.epub> <outbook.epub>".format(progname)
|
||||
return 1
|
||||
keypath, inpath, outpath = argv[1:]
|
||||
return decryptBook(keypath, inpath, outpath)
|
||||
|
||||
userkey = open(keypath,'rb').read()
|
||||
result = decryptBook(userkey, inpath, outpath)
|
||||
if result == 0:
|
||||
print u"Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath))
|
||||
return result
|
||||
|
||||
def gui_main():
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import traceback
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = Tkinter.Label(self, text=u"Select files for decryption")
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
Tkinter.Label(body, text=u"Key file").grid(row=0)
|
||||
self.keypath = Tkinter.Entry(body, width=30)
|
||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||
if os.path.exists(u"adeptkey.der"):
|
||||
self.keypath.insert(0, u"adeptkey.der")
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
Tkinter.Label(body, text=u"Input file").grid(row=1)
|
||||
self.inpath = Tkinter.Entry(body, width=30)
|
||||
self.inpath.grid(row=1, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
Tkinter.Label(body, text=u"Output file").grid(row=2)
|
||||
self.outpath = Tkinter.Entry(body, width=30)
|
||||
self.outpath.grid(row=2, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_outpath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = Tkinter.Button(
|
||||
buttons, text=u"Decrypt", width=10, command=self.decrypt)
|
||||
botton.pack(side=Tkconstants.LEFT)
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
button = Tkinter.Button(
|
||||
buttons, text=u"Quit", width=10, command=self.quit)
|
||||
button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkFileDialog.askopenfilename(
|
||||
parent=None, title=u"Select Adobe Adept \'.der\' key file",
|
||||
defaultextension=u".der",
|
||||
filetypes=[('Adobe Adept DER-encoded files', '.der'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(keypath)
|
||||
self.keypath.delete(0, Tkconstants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
def get_inpath(self):
|
||||
inpath = tkFileDialog.askopenfilename(
|
||||
parent=None, title=u"Select ADEPT-encrypted ePub file to decrypt",
|
||||
defaultextension=u".epub", filetypes=[('ePub files', '.epub')])
|
||||
if inpath:
|
||||
inpath = os.path.normpath(inpath)
|
||||
self.inpath.delete(0, Tkconstants.END)
|
||||
self.inpath.insert(0, inpath)
|
||||
return
|
||||
|
||||
def get_outpath(self):
|
||||
outpath = tkFileDialog.asksaveasfilename(
|
||||
parent=None, title=u"Select unencrypted ePub file to produce",
|
||||
defaultextension=u".epub", filetypes=[('ePub files', '.epub')])
|
||||
if outpath:
|
||||
outpath = os.path.normpath(outpath)
|
||||
self.outpath.delete(0, Tkconstants.END)
|
||||
self.outpath.insert(0, outpath)
|
||||
return
|
||||
|
||||
def decrypt(self):
|
||||
keypath = self.keypath.get()
|
||||
inpath = self.inpath.get()
|
||||
outpath = self.outpath.get()
|
||||
if not keypath or not os.path.exists(keypath):
|
||||
self.status['text'] = u"Specified key file does not exist"
|
||||
return
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = u"Specified input file does not exist"
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = u"Output file not specified"
|
||||
return
|
||||
if inpath == outpath:
|
||||
self.status['text'] = u"Must have different input and output files"
|
||||
return
|
||||
userkey = open(keypath,'rb').read()
|
||||
self.status['text'] = u"Decrypting..."
|
||||
try:
|
||||
decrypt_status = decryptBook(userkey, inpath, outpath)
|
||||
except Exception, e:
|
||||
self.status['text'] = u"Error; {0}".format(e)
|
||||
return
|
||||
if decrypt_status == 0:
|
||||
self.status['text'] = u"File successfully decrypted"
|
||||
else:
|
||||
self.status['text'] = u"The was an error decrypting the file."
|
||||
|
||||
root = Tkinter.Tk()
|
||||
if AES is None:
|
||||
root.withdraw()
|
||||
tkMessageBox.showerror(
|
||||
"INEPT EPUB Decrypter",
|
||||
"This script requires OpenSSL or PyCrypto, which must be"
|
||||
" installed separately. Read the top-of-script comment for"
|
||||
" details.")
|
||||
return 1
|
||||
root.title('INEPT EPUB Decrypter')
|
||||
root.title(u"Adobe Adept ePub Decrypter v.{0}".format(__version__))
|
||||
root.resizable(True, False)
|
||||
root.minsize(300, 0)
|
||||
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||
|
@ -474,5 +559,7 @@ def gui_main():
|
|||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
||||
|
|
|
@ -6,8 +6,8 @@ from __future__ import with_statement
|
|||
# ineptkey.pyw, version 5.6
|
||||
# Copyright © 2009-2010 i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||
# later. <http://www.gnu.org/licenses/>
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# from <http://www.python.org/download/> and PyCrypto from
|
||||
|
@ -37,7 +37,7 @@ from __future__ import with_statement
|
|||
# 5.3 - On Windows try PyCrypto first, OpenSSL next
|
||||
# 5.4 - Modify interface to allow use of import
|
||||
# 5.5 - Fix for potential problem with PyCrypto
|
||||
# 5.6 - Revise to allow use in Plugins to eliminate need for duplicate code
|
||||
# 5.6 - Revised to allow use in Plugins to eliminate need for duplicate code
|
||||
|
||||
"""
|
||||
Retrieve Adobe ADEPT user key.
|
||||
|
@ -49,12 +49,65 @@ import sys
|
|||
import os
|
||||
import struct
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
try:
|
||||
from calibre.constants import iswindows, isosx
|
||||
except:
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
return [u"ineptkey.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
class ADEPTError(Exception):
|
||||
pass
|
||||
|
||||
|
@ -80,13 +133,13 @@ if iswindows:
|
|||
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
|
||||
('rounds', c_int)]
|
||||
AES_KEY_p = POINTER(AES_KEY)
|
||||
|
||||
|
||||
def F(restype, name, argtypes):
|
||||
func = getattr(libcrypto, name)
|
||||
func.restype = restype
|
||||
func.argtypes = argtypes
|
||||
return func
|
||||
|
||||
|
||||
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
|
||||
[c_char_p, c_int, AES_KEY_p])
|
||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||
|
@ -308,9 +361,9 @@ if iswindows:
|
|||
cuser = winreg.HKEY_CURRENT_USER
|
||||
try:
|
||||
regkey = winreg.OpenKey(cuser, DEVICE_KEY_PATH)
|
||||
device = winreg.QueryValueEx(regkey, 'key')[0]
|
||||
except WindowsError:
|
||||
raise ADEPTError("Adobe Digital Editions not activated")
|
||||
device = winreg.QueryValueEx(regkey, 'key')[0]
|
||||
keykey = CryptUnprotectData(device, entropy)
|
||||
userkey = None
|
||||
keys = []
|
||||
|
@ -343,7 +396,7 @@ if iswindows:
|
|||
if len(keys) == 0:
|
||||
raise ADEPTError('Could not locate privateLicenseKey')
|
||||
return keys
|
||||
|
||||
|
||||
|
||||
elif isosx:
|
||||
import xml.etree.ElementTree as etree
|
||||
|
@ -386,7 +439,7 @@ else:
|
|||
def retrieve_keys(keypath):
|
||||
raise ADEPTError("This script only supports Windows and Mac OS X.")
|
||||
return []
|
||||
|
||||
|
||||
def retrieve_key(keypath):
|
||||
keys = retrieve_keys()
|
||||
with open(keypath, 'wb') as f:
|
||||
|
@ -397,22 +450,22 @@ def extractKeyfile(keypath):
|
|||
try:
|
||||
success = retrieve_key(keypath)
|
||||
except ADEPTError, e:
|
||||
print "Key generation Error: " + str(e)
|
||||
print u"Key generation Error: {0}".format(e.args[0])
|
||||
return 1
|
||||
except Exception, e:
|
||||
print "General Error: " + str(e)
|
||||
print "General Error: {0}".format(e.args[0])
|
||||
return 1
|
||||
if not success:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
def cli_main(argv=unicode_argv()):
|
||||
keypath = argv[1]
|
||||
return extractKeyfile(keypath)
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
def gui_main(argv=unicode_argv()):
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkMessageBox
|
||||
|
@ -421,24 +474,24 @@ def main(argv=sys.argv):
|
|||
class ExceptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root, text):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
label = Tkinter.Label(self, text="Unexpected error:",
|
||||
label = Tkinter.Label(self, text=u"Unexpected error:",
|
||||
anchor=Tkconstants.W, justify=Tkconstants.LEFT)
|
||||
label.pack(fill=Tkconstants.X, expand=0)
|
||||
self.text = Tkinter.Text(self)
|
||||
self.text.pack(fill=Tkconstants.BOTH, expand=1)
|
||||
|
||||
|
||||
self.text.insert(Tkconstants.END, text)
|
||||
|
||||
|
||||
root = Tkinter.Tk()
|
||||
root.withdraw()
|
||||
progname = os.path.basename(argv[0])
|
||||
keypath = os.path.abspath("adeptkey.der")
|
||||
keypath, progname = os.path.split(argv[0])
|
||||
keypath = os.path.join(keypath, u"adeptkey.der")
|
||||
success = False
|
||||
try:
|
||||
success = retrieve_key(keypath)
|
||||
except ADEPTError, e:
|
||||
tkMessageBox.showerror("ADEPT Key", "Error: " + str(e))
|
||||
tkMessageBox.showerror(u"ADEPT Key", "Error: {0}".format(e.args[0]))
|
||||
except Exception:
|
||||
root.wm_state('normal')
|
||||
root.title('ADEPT Key')
|
||||
|
@ -448,10 +501,12 @@ def main(argv=sys.argv):
|
|||
if not success:
|
||||
return 1
|
||||
tkMessageBox.showinfo(
|
||||
"ADEPT Key", "Key successfully retrieved to %s" % (keypath))
|
||||
u"ADEPT Key", u"Key successfully retrieved to {0}".format(keypath))
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
sys.exit(main())
|
||||
sys.exit(gui_main())
|
||||
|
|
|
@ -1,13 +1,25 @@
|
|||
#! /usr/bin/env python
|
||||
# ineptpdf.pyw, version 7.11
|
||||
#! /usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# To run this program install Python 2.6 from http://www.python.org/download/
|
||||
# and OpenSSL (already installed on Mac OS X and Linux) OR
|
||||
# PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
|
||||
# (make sure to install the version for Python 2.6). Save this script file as
|
||||
# ineptpdf.pyw and double-click on it to run it.
|
||||
# ineptpdf.pyw, version 7.11
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# from <http://www.python.org/download/> and PyCrypto from
|
||||
# <http://www.voidspace.org.uk/python/modules.shtml#pycrypto> (make sure to
|
||||
# install the version for Python 2.6). Save this script file as
|
||||
# ineptepub.pyw and double-click on it to run it.
|
||||
#
|
||||
# Mac OS X users: Save this script file as ineptepub.pyw. You can run this
|
||||
# program from the command line (pythonw ineptepub.pyw) or by double-clicking
|
||||
# it when it has been associated with PythonLauncher.
|
||||
|
||||
# Revision history:
|
||||
# 1 - Initial release
|
||||
|
@ -36,12 +48,14 @@ from __future__ import with_statement
|
|||
# 7.9 - Bug fix for some session key errors when len(bookkey) > length required
|
||||
# 7.10 - Various tweaks to fix minor problems.
|
||||
# 7.11 - More tweaks to fix minor problems.
|
||||
# 7.12 - Revised to allow use in calibre plugins to eliminate need for duplicate code
|
||||
|
||||
"""
|
||||
Decrypts Adobe ADEPT-encrypted PDF files.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "7.12"
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
@ -51,10 +65,63 @@ import struct
|
|||
import hashlib
|
||||
from itertools import chain, islice
|
||||
import xml.etree.ElementTree as etree
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
return [u"ineptepub.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
class ADEPTError(Exception):
|
||||
pass
|
||||
|
@ -1520,9 +1587,7 @@ class PDFDocument(object):
|
|||
|
||||
def initialize_ebx(self, password, docid, param):
|
||||
self.is_printable = self.is_modifiable = self.is_extractable = True
|
||||
with open(password, 'rb') as f:
|
||||
keyder = f.read()
|
||||
rsa = RSA(keyder)
|
||||
rsa = RSA(password)
|
||||
length = int_value(param.get('Length', 0)) / 8
|
||||
rights = str_value(param.get('ADEPT_LICENSE')).decode('base64')
|
||||
rights = zlib.decompress(rights, -15)
|
||||
|
@ -1907,14 +1972,14 @@ class PDFObjStrmParser(PDFParser):
|
|||
### My own code, for which there is none else to blame
|
||||
|
||||
class PDFSerializer(object):
|
||||
def __init__(self, inf, keypath):
|
||||
def __init__(self, inf, userkey):
|
||||
global GEN_XREF_STM, gen_xref_stm
|
||||
gen_xref_stm = GEN_XREF_STM > 1
|
||||
self.version = inf.read(8)
|
||||
inf.seek(0)
|
||||
self.doc = doc = PDFDocument()
|
||||
parser = PDFParser(doc, inf)
|
||||
doc.initialize(keypath)
|
||||
doc.initialize(userkey)
|
||||
self.objids = objids = set()
|
||||
for xref in reversed(doc.xrefs):
|
||||
trailer = xref.trailer
|
||||
|
@ -2097,142 +2162,144 @@ class PDFSerializer(object):
|
|||
self.write('endobj\n')
|
||||
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
ltext='Select file for decryption\n'
|
||||
self.status = Tkinter.Label(self, text=ltext)
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
Tkinter.Label(body, text='Key file').grid(row=0)
|
||||
self.keypath = Tkinter.Entry(body, width=30)
|
||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||
if os.path.exists('adeptkey.der'):
|
||||
self.keypath.insert(0, 'adeptkey.der')
|
||||
button = Tkinter.Button(body, text="...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
Tkinter.Label(body, text='Input file').grid(row=1)
|
||||
self.inpath = Tkinter.Entry(body, width=30)
|
||||
self.inpath.grid(row=1, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
Tkinter.Label(body, text='Output file').grid(row=2)
|
||||
self.outpath = Tkinter.Entry(body, width=30)
|
||||
self.outpath.grid(row=2, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_outpath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
|
||||
|
||||
botton = Tkinter.Button(
|
||||
buttons, text="Decrypt", width=10, command=self.decrypt)
|
||||
botton.pack(side=Tkconstants.LEFT)
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
button = Tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quit)
|
||||
button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkFileDialog.askopenfilename(
|
||||
parent=None, title='Select ADEPT key file',
|
||||
defaultextension='.der', filetypes=[('DER-encoded files', '.der'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(os.path.realpath(keypath))
|
||||
self.keypath.delete(0, Tkconstants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
def get_inpath(self):
|
||||
inpath = tkFileDialog.askopenfilename(
|
||||
parent=None, title='Select ADEPT encrypted PDF file to decrypt',
|
||||
defaultextension='.pdf', filetypes=[('PDF files', '.pdf'),
|
||||
('All files', '.*')])
|
||||
if inpath:
|
||||
inpath = os.path.normpath(os.path.realpath(inpath))
|
||||
self.inpath.delete(0, Tkconstants.END)
|
||||
self.inpath.insert(0, inpath)
|
||||
return
|
||||
|
||||
def get_outpath(self):
|
||||
outpath = tkFileDialog.asksaveasfilename(
|
||||
parent=None, title='Select unencrypted PDF file to produce',
|
||||
defaultextension='.pdf', filetypes=[('PDF files', '.pdf'),
|
||||
('All files', '.*')])
|
||||
if outpath:
|
||||
outpath = os.path.normpath(os.path.realpath(outpath))
|
||||
self.outpath.delete(0, Tkconstants.END)
|
||||
self.outpath.insert(0, outpath)
|
||||
return
|
||||
|
||||
def decrypt(self):
|
||||
keypath = self.keypath.get()
|
||||
inpath = self.inpath.get()
|
||||
outpath = self.outpath.get()
|
||||
if not keypath or not os.path.exists(keypath):
|
||||
# keyfile doesn't exist
|
||||
self.status['text'] = 'Specified Adept key file does not exist'
|
||||
return
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = 'Specified input file does not exist'
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = 'Output file not specified'
|
||||
return
|
||||
if inpath == outpath:
|
||||
self.status['text'] = 'Must have different input and output files'
|
||||
return
|
||||
# patch for non-ascii characters
|
||||
argv = [sys.argv[0], keypath, inpath, outpath]
|
||||
self.status['text'] = 'Processing ...'
|
||||
try:
|
||||
cli_main(argv)
|
||||
except Exception, a:
|
||||
self.status['text'] = 'Error: ' + str(a)
|
||||
return
|
||||
self.status['text'] = 'File successfully decrypted.\n'+\
|
||||
'Close this window or decrypt another pdf file.'
|
||||
return
|
||||
|
||||
|
||||
def decryptBook(keypath, inpath, outpath):
|
||||
def decryptBook(userkey, inpath, outpath):
|
||||
if RSA is None:
|
||||
raise ADEPTError(u"PyCrypto or OpenSSL must be installed.")
|
||||
with open(inpath, 'rb') as inf:
|
||||
try:
|
||||
serializer = PDFSerializer(inf, keypath)
|
||||
serializer = PDFSerializer(inf, userkey)
|
||||
except:
|
||||
print "Error serializing pdf. Probably wrong key."
|
||||
return 1
|
||||
print u"Error serializing pdf {0}. Probably wrong key.".format(os.path.basename(inpath))
|
||||
return 2
|
||||
# hope this will fix the 'bad file descriptor' problem
|
||||
with open(outpath, 'wb') as outf:
|
||||
# help construct to make sure the method runs to the end
|
||||
# help construct to make sure the method runs to the end
|
||||
try:
|
||||
serializer.dump(outf)
|
||||
except:
|
||||
print "error writing pdf."
|
||||
return 1
|
||||
except Exception, e:
|
||||
print u"error writing pdf: {0}".format(e.args[0])
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
if RSA is None:
|
||||
print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \
|
||||
"separately. Read the top-of-script comment for details." % \
|
||||
(progname,)
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,)
|
||||
print u"usage: {0} <keyfile.der> <inbook.pdf> <outbook.pdf>".format(progname)
|
||||
return 1
|
||||
keypath, inpath, outpath = argv[1:]
|
||||
return decryptBook(keypath, inpath, outpath)
|
||||
userkey = open(keypath,'rb').read()
|
||||
result = decryptBook(userkey, inpath, outpath)
|
||||
if result == 0:
|
||||
print u"Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath))
|
||||
return result
|
||||
|
||||
|
||||
def gui_main():
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = Tkinter.Label(self, text=u"Select files for decryption")
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
Tkinter.Label(body, text=u"Key file").grid(row=0)
|
||||
self.keypath = Tkinter.Entry(body, width=30)
|
||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||
if os.path.exists(u"adeptkey.der"):
|
||||
self.keypath.insert(0, u"adeptkey.der")
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
Tkinter.Label(body, text=u"Input file").grid(row=1)
|
||||
self.inpath = Tkinter.Entry(body, width=30)
|
||||
self.inpath.grid(row=1, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
Tkinter.Label(body, text=u"Output file").grid(row=2)
|
||||
self.outpath = Tkinter.Entry(body, width=30)
|
||||
self.outpath.grid(row=2, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_outpath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = Tkinter.Button(
|
||||
buttons, text=u"Decrypt", width=10, command=self.decrypt)
|
||||
botton.pack(side=Tkconstants.LEFT)
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
button = Tkinter.Button(
|
||||
buttons, text=u"Quit", width=10, command=self.quit)
|
||||
button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkFileDialog.askopenfilename(
|
||||
parent=None, title=u"Select Adobe Adept \'.der\' key file",
|
||||
defaultextension=u".der",
|
||||
filetypes=[('Adobe Adept DER-encoded files', '.der'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(keypath)
|
||||
self.keypath.delete(0, Tkconstants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
def get_inpath(self):
|
||||
inpath = tkFileDialog.askopenfilename(
|
||||
parent=None, title=u"Select ADEPT-encrypted PDF file to decrypt",
|
||||
defaultextension=u".pdf", filetypes=[('PDF files', '.pdf')])
|
||||
if inpath:
|
||||
inpath = os.path.normpath(inpath)
|
||||
self.inpath.delete(0, Tkconstants.END)
|
||||
self.inpath.insert(0, inpath)
|
||||
return
|
||||
|
||||
def get_outpath(self):
|
||||
outpath = tkFileDialog.asksaveasfilename(
|
||||
parent=None, title=u"Select unencrypted PDF file to produce",
|
||||
defaultextension=u".pdf", filetypes=[('PDF files', '.pdf')])
|
||||
if outpath:
|
||||
outpath = os.path.normpath(outpath)
|
||||
self.outpath.delete(0, Tkconstants.END)
|
||||
self.outpath.insert(0, outpath)
|
||||
return
|
||||
|
||||
def decrypt(self):
|
||||
keypath = self.keypath.get()
|
||||
inpath = self.inpath.get()
|
||||
outpath = self.outpath.get()
|
||||
if not keypath or not os.path.exists(keypath):
|
||||
self.status['text'] = u"Specified key file does not exist"
|
||||
return
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = u"Specified input file does not exist"
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = u"Output file not specified"
|
||||
return
|
||||
if inpath == outpath:
|
||||
self.status['text'] = u"Must have different input and output files"
|
||||
return
|
||||
userkey = open(keypath,'rb').read()
|
||||
self.status['text'] = u"Decrypting..."
|
||||
try:
|
||||
decrypt_status = decryptBook(userkey, inpath, outpath)
|
||||
except Exception, e:
|
||||
self.status['text'] = u"Error; {0}".format(e.args[0])
|
||||
return
|
||||
if decrypt_status == 0:
|
||||
self.status['text'] = u"File successfully decrypted"
|
||||
else:
|
||||
self.status['text'] = u"The was an error decrypting the file."
|
||||
|
||||
|
||||
root = Tkinter.Tk()
|
||||
if RSA is None:
|
||||
root.withdraw()
|
||||
|
@ -2241,7 +2308,7 @@ def gui_main():
|
|||
"This script requires OpenSSL or PyCrypto, which must be installed "
|
||||
"separately. Read the top-of-script comment for details.")
|
||||
return 1
|
||||
root.title('INEPT PDF Decrypter')
|
||||
root.title(u"Adobe Adept PDF Decrypter v.{0}".format(__version__))
|
||||
root.resizable(True, False)
|
||||
root.minsize(370, 0)
|
||||
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||
|
@ -2251,5 +2318,7 @@ def gui_main():
|
|||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ignobleepub.pyw, version 3.6
|
||||
# Copyright © 2009-2012 by DiapDealer et al.
|
||||
|
||||
# engine to remove drm from Kindle for Mac and Kindle for PC books
|
||||
# for personal use for archiving and converting your ebooks
|
||||
|
||||
|
@ -12,30 +16,51 @@ from __future__ import with_statement
|
|||
# be able to read OUR books on whatever device we want and to keep
|
||||
# readable for a long, long time
|
||||
|
||||
# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
|
||||
# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
|
||||
# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates
|
||||
# and many many others
|
||||
# Special thanks to The Dark Reverser for MobiDeDrm and CMBDTC for cmbdtc_dump
|
||||
# from which this script borrows most unashamedly.
|
||||
|
||||
|
||||
__version__ = '4.4'
|
||||
# Changelog
|
||||
# 1.0 - Name change to k4mobidedrm. Adds Mac support, Adds plugin code
|
||||
# 1.1 - Adds support for additional kindle.info files
|
||||
# 1.2 - Better error handling for older Mobipocket
|
||||
# 1.3 - Don't try to decrypt Topaz books
|
||||
# 1.7 - Add support for Topaz books and Kindle serial numbers. Split code.
|
||||
# 1.9 - Tidy up after Topaz, minor exception changes
|
||||
# 2.1 - Topaz fix and filename sanitizing
|
||||
# 2.2 - Topaz Fix and minor Mac code fix
|
||||
# 2.3 - More Topaz fixes
|
||||
# 2.4 - K4PC/Mac key generation fix
|
||||
# 2.6 - Better handling of non-K4PC/Mac ebooks
|
||||
# 2.7 - Better trailing bytes handling in mobidedrm
|
||||
# 2.8 - Moved parsing of kindle.info files to mac & pc util files.
|
||||
# 3.1 - Updated for new calibre interface. Now __init__ in plugin.
|
||||
# 3.5 - Now support Kindle for PC/Mac 1.6
|
||||
# 3.6 - Even better trailing bytes handling in mobidedrm
|
||||
# 3.7 - Add support for Amazon Print Replica ebooks.
|
||||
# 3.8 - Improved Topaz support
|
||||
# 4.1 - Improved Topaz support and faster decryption with alfcrypto
|
||||
# 4.2 - Added support for Amazon's KF8 format ebooks
|
||||
# 4.4 - Linux calls to Wine added, and improved configuration dialog
|
||||
# 4.5 - Linux works again without Wine. Some Mac key file search changes
|
||||
# 4.6 - First attempt to handle unicode properly
|
||||
# 4.7 - Added timing reports, and changed search for Mac key files
|
||||
# 4.8 - Much better unicode handling, matching the updated inept and ignoble scripts
|
||||
# - Moved back into plugin, __init__ in plugin now only contains plugin code.
|
||||
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
def write(self, data):
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
__version__ = '4.8'
|
||||
|
||||
import sys
|
||||
import os, csv, getopt
|
||||
import string
|
||||
|
||||
import sys, os, re
|
||||
import csv
|
||||
import getopt
|
||||
import re
|
||||
import traceback
|
||||
import time
|
||||
|
||||
buildXML = False
|
||||
import htmlentitydefs
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
@ -54,161 +79,203 @@ else:
|
|||
import topazextract
|
||||
import kgenpids
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
# cleanup bytestring filenames
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return [u"mobidedrm.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
# cleanup unicode filenames
|
||||
# borrowed from calibre from calibre/src/calibre/__init__.py
|
||||
# added in removal of non-printing chars
|
||||
# and removal of . at start
|
||||
# convert underscores to spaces (we're OK with spaces in file names)
|
||||
# added in removal of control (<32) chars
|
||||
# and removal of . at start and end
|
||||
# and with some (heavily edited) code from Paul Durrant's kindlenamer.py
|
||||
def cleanup_name(name):
|
||||
_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+/]')
|
||||
substitute='_'
|
||||
one = ''.join(char for char in name if char in string.printable)
|
||||
one = _filename_sanitize.sub(substitute, one)
|
||||
one = re.sub(r'\s', ' ', one).strip()
|
||||
one = re.sub(r'^\.+$', '_', one)
|
||||
one = one.replace('..', substitute)
|
||||
# Windows doesn't like path components that end with a period
|
||||
if one.endswith('.'):
|
||||
one = one[:-1]+substitute
|
||||
# Mac and Unix don't like file names that begin with a full stop
|
||||
if len(one) > 0 and one[0] == '.':
|
||||
one = substitute+one[1:]
|
||||
one = one.replace('_',' ')
|
||||
return one
|
||||
|
||||
def decryptBook(infile, outdir, k4, kInfoFiles, serials, pids):
|
||||
global buildXML
|
||||
# substitute filename unfriendly characters
|
||||
name = name.replace(u"<",u"[").replace(u">",u"]").replace(u" : ",u" – ").replace(u": ",u" – ").replace(u":",u"—").replace(u"/",u"_").replace(u"\\",u"_").replace(u"|",u"_").replace(u"\"",u"\'")
|
||||
# delete control characters
|
||||
name = u"".join(char for char in name if ord(char)>=32)
|
||||
# white space to single space, delete leading and trailing while space
|
||||
name = re.sub(ur"\s", u" ", name).strip()
|
||||
# remove leading dots
|
||||
while len(name)>0 and name[0] == u".":
|
||||
name = name[1:]
|
||||
# remove trailing dots (Windows doesn't like them)
|
||||
if name.endswith(u'.'):
|
||||
name = name[:-1]
|
||||
return name
|
||||
|
||||
# must be passed unicode
|
||||
def unescape(text):
|
||||
def fixup(m):
|
||||
text = m.group(0)
|
||||
if text[:2] == u"&#":
|
||||
# character reference
|
||||
try:
|
||||
if text[:3] == u"&#x":
|
||||
return unichr(int(text[3:-1], 16))
|
||||
else:
|
||||
return unichr(int(text[2:-1]))
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
# named entity
|
||||
try:
|
||||
text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
|
||||
except KeyError:
|
||||
pass
|
||||
return text # leave as is
|
||||
return re.sub(u"&#?\w+;", fixup, text)
|
||||
|
||||
def GetDecryptedBook(infile, kInfoFiles, serials, pids, starttime = time.time()):
|
||||
# handle the obvious cases at the beginning
|
||||
if not os.path.isfile(infile):
|
||||
print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: Input file does not exist"
|
||||
return 1
|
||||
|
||||
starttime = time.time()
|
||||
print "Starting decryptBook routine."
|
||||
|
||||
raise DRMException (u"Input file does not exist.")
|
||||
|
||||
mobi = True
|
||||
magic3 = file(infile,'rb').read(3)
|
||||
if magic3 == 'TPZ':
|
||||
mobi = False
|
||||
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
|
||||
if mobi:
|
||||
mb = mobidedrm.MobiBook(infile)
|
||||
else:
|
||||
mb = topazextract.TopazBook(infile)
|
||||
|
||||
title = mb.getBookTitle()
|
||||
print "Processing Book: ", title
|
||||
filenametitle = cleanup_name(title)
|
||||
outfilename = cleanup_name(bookname)
|
||||
bookname = unescape(mb.getBookTitle())
|
||||
print u"Decrypting {1} ebook: {0}".format(bookname, mb.getBookType())
|
||||
|
||||
# generate 'sensible' filename, that will sort with the original name,
|
||||
# but is close to the name from the file.
|
||||
outlength = len(outfilename)
|
||||
comparelength = min(8,min(outlength,len(filenametitle)))
|
||||
copylength = min(max(outfilename.find(' '),8),len(outfilename))
|
||||
if outlength==0:
|
||||
outfilename = filenametitle
|
||||
elif comparelength > 0:
|
||||
if outfilename[:comparelength] == filenametitle[:comparelength]:
|
||||
outfilename = filenametitle
|
||||
else:
|
||||
outfilename = outfilename[:copylength] + " " + filenametitle
|
||||
# extend PID list with book-specific PIDs
|
||||
md1, md2 = mb.getPIDMetaInfo()
|
||||
pids.extend(kgenpids.getPidList(md1, md2, serials, kInfoFiles))
|
||||
print u"Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(pids))
|
||||
|
||||
try:
|
||||
mb.processBook(pids)
|
||||
except:
|
||||
mb.cleanup
|
||||
raise
|
||||
|
||||
print u"Decryption succeeded after {0:.1f} seconds".format(time.time()-starttime)
|
||||
return mb
|
||||
|
||||
|
||||
# infile, outdir and kInfoFiles should be unicode strings
|
||||
def decryptBook(infile, outdir, kInfoFiles, serials, pids):
|
||||
starttime = time.time()
|
||||
print "Starting decryptBook routine."
|
||||
try:
|
||||
book = GetDecryptedBook(infile, kInfoFiles, serials, pids, starttime)
|
||||
except Exception, e:
|
||||
print u"Error decrypting book after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime)
|
||||
return 1
|
||||
|
||||
# if we're saving to the same folder as the original, use file name_
|
||||
# if to a different folder, use book name
|
||||
if os.path.normcase(os.path.normpath(outdir)) == os.path.normcase(os.path.normpath(os.path.dirname(infile))):
|
||||
outfilename = os.path.splitext(os.path.basename(infile))[0]
|
||||
else:
|
||||
outfilename = cleanup_name(book.getBookTitle())
|
||||
|
||||
# avoid excessively long file names
|
||||
if len(outfilename)>150:
|
||||
outfilename = outfilename[:150]
|
||||
|
||||
# build pid list
|
||||
md1, md2 = mb.getPIDMetaInfo()
|
||||
pids.extend(kgenpids.getPidList(md1, md2, k4, serials, kInfoFiles))
|
||||
outfilename = outfilename+u"_nodrm"
|
||||
outfile = os.path.join(outdir, outfilename + book.getBookExtension())
|
||||
|
||||
print "Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(pids))
|
||||
book.getFile(outfile)
|
||||
print u"Saved decrypted book {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename)
|
||||
|
||||
|
||||
try:
|
||||
mb.processBook(pids)
|
||||
|
||||
except mobidedrm.DrmException, e:
|
||||
print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||
print "Failed to decrypted book after {0:.1f} seconds".format(time.time()-starttime)
|
||||
return 1
|
||||
except topazextract.TpzDRMError, e:
|
||||
print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||
print "Failed to decrypted book after {0:.1f} seconds".format(time.time()-starttime)
|
||||
return 1
|
||||
except Exception, e:
|
||||
print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||
print "Failed to decrypted book after {0:.1f} seconds".format(time.time()-starttime)
|
||||
return 1
|
||||
|
||||
print "Successfully decrypted book after {0:.1f} seconds".format(time.time()-starttime)
|
||||
|
||||
if mobi:
|
||||
if mb.getPrintReplica():
|
||||
outfile = os.path.join(outdir, outfilename + '_nodrm' + '.azw4')
|
||||
elif mb.getMobiVersion() >= 8:
|
||||
outfile = os.path.join(outdir, outfilename + '_nodrm' + '.azw3')
|
||||
else:
|
||||
outfile = os.path.join(outdir, outfilename + '_nodrm' + '.mobi')
|
||||
mb.getMobiFile(outfile)
|
||||
print "Saved decrypted book {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename + '_nodrm')
|
||||
return 0
|
||||
|
||||
# topaz:
|
||||
print " Creating NoDRM HTMLZ Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_nodrm' + '.htmlz')
|
||||
mb.getHTMLZip(zipname)
|
||||
|
||||
print " Creating SVG ZIP Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_SVG' + '.zip')
|
||||
mb.getSVGZip(zipname)
|
||||
|
||||
if buildXML:
|
||||
print " Creating XML ZIP Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_XML' + '.zip')
|
||||
mb.getXMLZip(zipname)
|
||||
if book.getBookType()==u"Topaz":
|
||||
zipname = os.path.join(outdir, outfilename + u"_SVG.zip")
|
||||
book.getSVGZip(zipname)
|
||||
print u"Saved SVG ZIP Archive for {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename)
|
||||
|
||||
# remove internal temporary directory of Topaz pieces
|
||||
mb.cleanup()
|
||||
print "Saved decrypted Topaz book parts after {0:.1f} seconds".format(time.time()-starttime)
|
||||
return 0
|
||||
book.cleanup()
|
||||
|
||||
|
||||
def usage(progname):
|
||||
print "Removes DRM protection from K4PC/M, Kindle, Mobi and Topaz ebooks"
|
||||
print "Usage:"
|
||||
print " %s [-k <kindle.info>] [-p <pidnums>] [-s <kindleSerialNumbers>] <infile> <outdir> " % progname
|
||||
print u"Removes DRM protection from Mobipocket, Amazon KF8, Amazon Print Replica and Amazon Topaz ebooks"
|
||||
print u"Usage:"
|
||||
print u" {0} [-k <kindle.info>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] <infile> <outdir>".format(progname)
|
||||
|
||||
#
|
||||
# Main
|
||||
#
|
||||
def main(argv=sys.argv):
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
|
||||
k4 = False
|
||||
kInfoFiles = []
|
||||
serials = []
|
||||
pids = []
|
||||
|
||||
print ('K4MobiDeDrm v%(__version__)s '
|
||||
'provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc .' % globals())
|
||||
print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__)
|
||||
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:")
|
||||
except getopt.GetoptError, err:
|
||||
print str(err)
|
||||
print u"Error in options or arguments: {0}".format(err.args[0])
|
||||
usage(progname)
|
||||
sys.exit(2)
|
||||
if len(args)<2:
|
||||
usage(progname)
|
||||
sys.exit(2)
|
||||
|
||||
infile = args[0]
|
||||
outdir = args[1]
|
||||
kInfoFiles = []
|
||||
serials = []
|
||||
pids = []
|
||||
|
||||
for o, a in opts:
|
||||
if o == "-k":
|
||||
if a == None :
|
||||
|
@ -223,16 +290,13 @@ def main(argv=sys.argv):
|
|||
raise DrmException("Invalid parameter for -s")
|
||||
serials = a.split(',')
|
||||
|
||||
# try with built in Kindle Info files
|
||||
k4 = True
|
||||
if sys.platform.startswith('linux'):
|
||||
k4 = False
|
||||
kInfoFiles = None
|
||||
infile = args[0]
|
||||
outdir = args[1]
|
||||
return decryptBook(infile, outdir, k4, kInfoFiles, serials, pids)
|
||||
# try with built in Kindle Info files if not on Linux
|
||||
k4 = not sys.platform.startswith('linux')
|
||||
|
||||
return decryptBook(infile, outdir, kInfoFiles, serials, pids)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
sys.exit(main())
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# standlone set of Mac OSX specific routines needed for KindleBooks
|
||||
|
||||
from __future__ import with_statement
|
||||
|
@ -22,7 +25,7 @@ def _load_crypto_libcrypto():
|
|||
|
||||
libcrypto = find_library('crypto')
|
||||
if libcrypto is None:
|
||||
raise DrmException('libcrypto not found')
|
||||
raise DrmException(u"libcrypto not found")
|
||||
libcrypto = CDLL(libcrypto)
|
||||
|
||||
# From OpenSSL's crypto aes header
|
||||
|
@ -80,14 +83,14 @@ def _load_crypto_libcrypto():
|
|||
def set_decrypt_key(self, userkey, iv):
|
||||
self._blocksize = len(userkey)
|
||||
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
||||
raise DrmException('AES improper key used')
|
||||
raise DrmException(u"AES improper key used")
|
||||
return
|
||||
keyctx = self._keyctx = AES_KEY()
|
||||
self._iv = iv
|
||||
self._userkey = userkey
|
||||
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
|
||||
if rv < 0:
|
||||
raise DrmException('Failed to initialize AES key')
|
||||
raise DrmException(u"Failed to initialize AES key")
|
||||
|
||||
def decrypt(self, data):
|
||||
out = create_string_buffer(len(data))
|
||||
|
@ -95,7 +98,7 @@ def _load_crypto_libcrypto():
|
|||
keyctx = self._keyctx
|
||||
rv = AES_cbc_encrypt(data, out, len(data), keyctx, mutable_iv, 0)
|
||||
if rv == 0:
|
||||
raise DrmException('AES decryption failed')
|
||||
raise DrmException(u"AES decryption failed")
|
||||
return out.raw
|
||||
|
||||
def keyivgen(self, passwd, salt, iter, keylen):
|
||||
|
@ -139,20 +142,20 @@ def SHA256(message):
|
|||
return ctx.digest()
|
||||
|
||||
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
|
||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||
charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM"
|
||||
charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M'
|
||||
charMap2 = 'ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM'
|
||||
|
||||
# For kinf approach of K4Mac 1.6.X or later
|
||||
# On K4PC charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE"
|
||||
# On K4PC charMap5 = 'AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE'
|
||||
# For Mac they seem to re-use charMap2 here
|
||||
charMap5 = charMap2
|
||||
|
||||
# new in K4M 1.9.X
|
||||
testMap8 = "YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD"
|
||||
testMap8 = 'YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD'
|
||||
|
||||
|
||||
def encode(data, map):
|
||||
result = ""
|
||||
result = ''
|
||||
for char in data:
|
||||
value = ord(char)
|
||||
Q = (value ^ 0x80) // len(map)
|
||||
|
@ -167,14 +170,14 @@ def encodeHash(data,map):
|
|||
|
||||
# Decode the string in data with the characters in map. Returns the decoded bytes
|
||||
def decode(data,map):
|
||||
result = ""
|
||||
result = ''
|
||||
for i in range (0,len(data)-1,2):
|
||||
high = map.find(data[i])
|
||||
low = map.find(data[i+1])
|
||||
if (high == -1) or (low == -1) :
|
||||
break
|
||||
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||
result += pack("B",value)
|
||||
result += pack('B',value)
|
||||
return result
|
||||
|
||||
# For K4M 1.6.X and later
|
||||
|
@ -200,7 +203,7 @@ def primes(n):
|
|||
|
||||
|
||||
# uses a sub process to get the Hard Drive Serial Number using ioreg
|
||||
# returns with the serial number of drive whose BSD Name is "disk0"
|
||||
# returns with the serial number of drive whose BSD Name is 'disk0'
|
||||
def GetVolumeSerialNumber():
|
||||
sernum = os.getenv('MYSERIALNUMBER')
|
||||
if sernum != None:
|
||||
|
@ -216,11 +219,11 @@ def GetVolumeSerialNumber():
|
|||
foundIt = False
|
||||
for j in xrange(cnt):
|
||||
resline = reslst[j]
|
||||
pp = resline.find('"Serial Number" = "')
|
||||
pp = resline.find('\"Serial Number\" = \"')
|
||||
if pp >= 0:
|
||||
sernum = resline[pp+19:-1]
|
||||
sernum = sernum.strip()
|
||||
bb = resline.find('"BSD Name" = "')
|
||||
bb = resline.find('\"BSD Name\" = \"')
|
||||
if bb >= 0:
|
||||
bsdname = resline[bb+14:-1]
|
||||
bsdname = bsdname.strip()
|
||||
|
@ -277,7 +280,7 @@ def GetDiskPartitionUUID(diskpart):
|
|||
nest += 1
|
||||
if resline.find('}') >= 0:
|
||||
nest -= 1
|
||||
pp = resline.find('"UUID" = "')
|
||||
pp = resline.find('\"UUID\" = \"')
|
||||
if pp >= 0:
|
||||
uuidnum = resline[pp+10:-1]
|
||||
uuidnum = uuidnum.strip()
|
||||
|
@ -285,7 +288,7 @@ def GetDiskPartitionUUID(diskpart):
|
|||
if partnest == uuidnest and uuidnest > 0:
|
||||
foundIt = True
|
||||
break
|
||||
bb = resline.find('"BSD Name" = "')
|
||||
bb = resline.find('\"BSD Name\" = \"')
|
||||
if bb >= 0:
|
||||
bsdname = resline[bb+14:-1]
|
||||
bsdname = bsdname.strip()
|
||||
|
@ -323,7 +326,7 @@ def GetMACAddressMunged():
|
|||
if pp >= 0:
|
||||
macnum = resline[pp+6:-1]
|
||||
macnum = macnum.strip()
|
||||
# print "original mac", macnum
|
||||
# print 'original mac', macnum
|
||||
# now munge it up the way Kindle app does
|
||||
# by xoring it with 0xa5 and swapping elements 3 and 4
|
||||
maclst = macnum.split(':')
|
||||
|
@ -340,7 +343,7 @@ def GetMACAddressMunged():
|
|||
mlst[2] = maclst[2] ^ 0xa5
|
||||
mlst[1] = maclst[1] ^ 0xa5
|
||||
mlst[0] = maclst[0] ^ 0xa5
|
||||
macnum = "%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x" % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5])
|
||||
macnum = '%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x' % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5])
|
||||
foundIt = True
|
||||
break
|
||||
if not foundIt:
|
||||
|
@ -367,6 +370,19 @@ def isNewInstall():
|
|||
return False
|
||||
|
||||
|
||||
class Memoize:
|
||||
"""Memoize(fn) - an instance which acts like fn but memoizes its arguments
|
||||
Will only work on functions with non-mutable arguments
|
||||
"""
|
||||
def __init__(self, fn):
|
||||
self.fn = fn
|
||||
self.memo = {}
|
||||
def __call__(self, *args):
|
||||
if not self.memo.has_key(args):
|
||||
self.memo[args] = self.fn(*args)
|
||||
return self.memo[args]
|
||||
|
||||
@Memoize
|
||||
def GetIDString():
|
||||
# K4Mac now has an extensive set of ids strings it uses
|
||||
# in encoding pids and in creating unique passwords
|
||||
|
@ -530,7 +546,8 @@ def getKindleInfoFiles():
|
|||
# determine type of kindle info provided and return a
|
||||
# database of keynames and values
|
||||
def getDBfromFile(kInfoFile):
|
||||
names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber", "max_date", "SIGVERIF"]
|
||||
|
||||
names = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber', 'max_date', 'SIGVERIF']
|
||||
DB = {}
|
||||
cnt = 0
|
||||
infoReader = open(kInfoFile, 'r')
|
||||
|
@ -545,12 +562,12 @@ def getDBfromFile(kInfoFile):
|
|||
for item in items:
|
||||
if item != '':
|
||||
keyhash, rawdata = item.split(':')
|
||||
keyname = "unknown"
|
||||
keyname = 'unknown'
|
||||
for name in names:
|
||||
if encodeHash(name,charMap2) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
if keyname == 'unknown':
|
||||
keyname = keyhash
|
||||
encryptedValue = decode(rawdata,charMap2)
|
||||
cleartext = cud.decrypt(encryptedValue)
|
||||
|
@ -563,8 +580,8 @@ def getDBfromFile(kInfoFile):
|
|||
if hdr == '/':
|
||||
|
||||
# else newer style .kinf file used by K4Mac >= 1.6.0
|
||||
# the .kinf file uses "/" to separate it into records
|
||||
# so remove the trailing "/" to make it easy to use split
|
||||
# the .kinf file uses '/' to separate it into records
|
||||
# so remove the trailing '/' to make it easy to use split
|
||||
data = data[:-1]
|
||||
items = data.split('/')
|
||||
cud = CryptUnprotectDataV2()
|
||||
|
@ -578,11 +595,11 @@ def getDBfromFile(kInfoFile):
|
|||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
keyname = "unknown"
|
||||
keyname = 'unknown'
|
||||
|
||||
# the raw keyhash string is also used to create entropy for the actual
|
||||
# CryptProtectData Blob that represents that keys contents
|
||||
# "entropy" not used for K4Mac only K4PC
|
||||
# 'entropy' not used for K4Mac only K4PC
|
||||
# entropy = SHA1(keyhash)
|
||||
|
||||
# the remainder of the first record when decoded with charMap5
|
||||
|
@ -599,12 +616,12 @@ def getDBfromFile(kInfoFile):
|
|||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
keyname = "unknown"
|
||||
keyname = 'unknown'
|
||||
for name in names:
|
||||
if encodeHash(name,charMap5) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
if keyname == 'unknown':
|
||||
keyname = keyhash
|
||||
|
||||
# the charMap5 encoded contents data has had a length
|
||||
|
@ -615,10 +632,10 @@ def getDBfromFile(kInfoFile):
|
|||
|
||||
# The offset into the charMap5 encoded contents seems to be:
|
||||
# len(contents) - largest prime number less than or equal to int(len(content)/3)
|
||||
# (in other words split "about" 2/3rds of the way through)
|
||||
# (in other words split 'about' 2/3rds of the way through)
|
||||
|
||||
# move first offsets chars to end to align for decode by charMap5
|
||||
encdata = "".join(edlst)
|
||||
encdata = ''.join(edlst)
|
||||
contlen = len(encdata)
|
||||
|
||||
# now properly split and recombine
|
||||
|
@ -667,7 +684,7 @@ def getDBfromFile(kInfoFile):
|
|||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
keyname = "unknown"
|
||||
keyname = 'unknown'
|
||||
|
||||
# unlike K4PC the keyhash is not used in generating entropy
|
||||
# entropy = SHA1(keyhash) + added_entropy
|
||||
|
@ -687,12 +704,12 @@ def getDBfromFile(kInfoFile):
|
|||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
keyname = "unknown"
|
||||
keyname = 'unknown'
|
||||
for name in names:
|
||||
if encodeHash(name,testMap8) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
if keyname == 'unknown':
|
||||
keyname = keyhash
|
||||
|
||||
# the testMap8 encoded contents data has had a length
|
||||
|
@ -703,10 +720,10 @@ def getDBfromFile(kInfoFile):
|
|||
|
||||
# The offset into the testMap8 encoded contents seems to be:
|
||||
# len(contents) - largest prime number less than or equal to int(len(content)/3)
|
||||
# (in other words split "about" 2/3rds of the way through)
|
||||
# (in other words split 'about' 2/3rds of the way through)
|
||||
|
||||
# move first offsets chars to end to align for decode by testMap8
|
||||
encdata = "".join(edlst)
|
||||
encdata = ''.join(edlst)
|
||||
contlen = len(encdata)
|
||||
|
||||
# now properly split and recombine
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# K4PC Windows specific routines
|
||||
|
||||
from __future__ import with_statement
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
import sys
|
||||
|
@ -17,26 +18,24 @@ global charMap4
|
|||
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
if inCalibre:
|
||||
if sys.platform.startswith('win'):
|
||||
from calibre.constants import iswindows, isosx
|
||||
if iswindows:
|
||||
from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||
|
||||
if sys.platform.startswith('darwin'):
|
||||
if isosx:
|
||||
from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||
else:
|
||||
if sys.platform.startswith('win'):
|
||||
inCalibre = False
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
if iswindows:
|
||||
from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||
|
||||
if sys.platform.startswith('darwin'):
|
||||
if isosx:
|
||||
from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||
|
||||
|
||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M'
|
||||
charMap3 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||
charMap4 = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
|
||||
|
||||
# crypto digestroutines
|
||||
import hashlib
|
||||
|
@ -54,7 +53,7 @@ def SHA1(message):
|
|||
|
||||
# Encode the bytes in data with the characters in map
|
||||
def encode(data, map):
|
||||
result = ""
|
||||
result = ''
|
||||
for char in data:
|
||||
value = ord(char)
|
||||
Q = (value ^ 0x80) // len(map)
|
||||
|
@ -69,14 +68,14 @@ def encodeHash(data,map):
|
|||
|
||||
# Decode the string in data with the characters in map. Returns the decoded bytes
|
||||
def decode(data,map):
|
||||
result = ""
|
||||
result = ''
|
||||
for i in range (0,len(data)-1,2):
|
||||
high = map.find(data[i])
|
||||
low = map.find(data[i+1])
|
||||
if (high == -1) or (low == -1) :
|
||||
break
|
||||
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||
result += pack("B",value)
|
||||
result += pack('B',value)
|
||||
return result
|
||||
|
||||
#
|
||||
|
@ -98,7 +97,7 @@ def getSixBitsFromBitField(bitField,offset):
|
|||
# 8 bits to six bits encoding from hash to generate PID string
|
||||
def encodePID(hash):
|
||||
global charMap3
|
||||
PID = ""
|
||||
PID = ''
|
||||
for position in range (0,8):
|
||||
PID += charMap3[getSixBitsFromBitField(hash,position)]
|
||||
return PID
|
||||
|
@ -129,7 +128,7 @@ def generatePidSeed(table,dsn) :
|
|||
def generateDevicePID(table,dsn,nbRoll):
|
||||
global charMap4
|
||||
seed = generatePidSeed(table,dsn)
|
||||
pidAscii = ""
|
||||
pidAscii = ''
|
||||
pid = [(seed >>24) &0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF,(seed>>24) & 0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF]
|
||||
index = 0
|
||||
for counter in range (0,nbRoll):
|
||||
|
@ -176,28 +175,31 @@ def pidFromSerial(s, l):
|
|||
|
||||
|
||||
# Parse the EXTH header records and use the Kindle serial number to calculate the book pid.
|
||||
def getKindlePid(pidlst, rec209, token, serialnum):
|
||||
def getKindlePids(rec209, token, serialnum):
|
||||
pids=[]
|
||||
|
||||
# Compute book PID
|
||||
pidHash = SHA1(serialnum+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
pids.append(bookPID)
|
||||
|
||||
# compute fixed pid for old pre 2.5 firmware update pid as well
|
||||
bookPID = pidFromSerial(serialnum, 7) + "*"
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
kindlePID = pidFromSerial(serialnum, 7) + "*"
|
||||
kindlePID = checksumPid(kindlePID)
|
||||
pids.append(kindlePID)
|
||||
|
||||
return pidlst
|
||||
return pids
|
||||
|
||||
|
||||
# parse the Kindleinfo file to calculate the book pid.
|
||||
|
||||
keynames = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
|
||||
keynames = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber']
|
||||
|
||||
def getK4Pids(pidlst, rec209, token, kInfoFile):
|
||||
def getK4Pids(rec209, token, kInfoFile):
|
||||
global charMap1
|
||||
kindleDatabase = None
|
||||
pids = []
|
||||
try:
|
||||
kindleDatabase = getDBfromFile(kInfoFile)
|
||||
except Exception, message:
|
||||
|
@ -206,17 +208,17 @@ def getK4Pids(pidlst, rec209, token, kInfoFile):
|
|||
pass
|
||||
|
||||
if kindleDatabase == None :
|
||||
return pidlst
|
||||
return pids
|
||||
|
||||
try:
|
||||
# Get the Mazama Random number
|
||||
MazamaRandomNumber = kindleDatabase["MazamaRandomNumber"]
|
||||
MazamaRandomNumber = kindleDatabase['MazamaRandomNumber']
|
||||
|
||||
# Get the kindle account token
|
||||
kindleAccountToken = kindleDatabase["kindle.account.tokens"]
|
||||
kindleAccountToken = kindleDatabase['kindle.account.tokens']
|
||||
except KeyError:
|
||||
print "Keys not found in " + kInfoFile
|
||||
return pidlst
|
||||
print u"Keys not found in {0}".format(os.path.basename(kInfoFile))
|
||||
return pids
|
||||
|
||||
# Get the ID string used
|
||||
encodedIDString = encodeHash(GetIDString(),charMap1)
|
||||
|
@ -231,7 +233,7 @@ def getK4Pids(pidlst, rec209, token, kInfoFile):
|
|||
table = generatePidEncryptionTable()
|
||||
devicePID = generateDevicePID(table,DSN,4)
|
||||
devicePID = checksumPid(devicePID)
|
||||
pidlst.append(devicePID)
|
||||
pids.append(devicePID)
|
||||
|
||||
# Compute book PIDs
|
||||
|
||||
|
@ -239,36 +241,38 @@ def getK4Pids(pidlst, rec209, token, kInfoFile):
|
|||
pidHash = SHA1(DSN+kindleAccountToken+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
pids.append(bookPID)
|
||||
|
||||
# variant 1
|
||||
pidHash = SHA1(kindleAccountToken+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
pids.append(bookPID)
|
||||
|
||||
# variant 2
|
||||
pidHash = SHA1(DSN+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
pids.append(bookPID)
|
||||
|
||||
return pidlst
|
||||
return pids
|
||||
|
||||
def getPidList(md1, md2, k4 = True, serials=[], kInfoFiles=[]):
|
||||
def getPidList(md1, md2, serials=[], kInfoFiles=[]):
|
||||
pidlst = []
|
||||
if kInfoFiles is None:
|
||||
kInfoFiles = []
|
||||
if k4:
|
||||
if serials is None:
|
||||
serials = []
|
||||
if iswindows or isosx:
|
||||
kInfoFiles.extend(getKindleInfoFiles())
|
||||
for infoFile in kInfoFiles:
|
||||
try:
|
||||
pidlst = getK4Pids(pidlst, md1, md2, infoFile)
|
||||
except Exception, message:
|
||||
print("Error getting PIDs from " + infoFile + ": " + message)
|
||||
pidlst.extend(getK4Pids(md1, md2, infoFile))
|
||||
except Exception, e:
|
||||
print u"Error getting PIDs from {0}: {1}".format(os.path.basename(infoFile),e.args[0])
|
||||
for serialnum in serials:
|
||||
try:
|
||||
pidlst = getKindlePid(pidlst, md1, md2, serialnum)
|
||||
pidlst.extend(getKindlePids(md1, md2, serialnum))
|
||||
except Exception, message:
|
||||
print("Error getting PIDs from " + serialnum + ": " + message)
|
||||
print u"Error getting PIDs from serial number {0}: {1}".format(serialnum ,e.args[0])
|
||||
return pidlst
|
||||
|
|
|
@ -1,29 +1,80 @@
|
|||
#!/usr/bin/python
|
||||
# Mobipocket PID calculator v0.2 for Amazon Kindle.
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Mobipocket PID calculator v0.4 for Amazon Kindle.
|
||||
# Copyright (c) 2007, 2009 Igor Skochinsky <skochinsky@mail.ru>
|
||||
# History:
|
||||
# 0.1 Initial release
|
||||
# 0.2 Added support for generating PID for iPhone (thanks to mbp)
|
||||
# 0.3 changed to autoflush stdout, fixed return code usage
|
||||
class Unbuffered:
|
||||
# 0.3 updated for unicode
|
||||
|
||||
import sys
|
||||
import binascii
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
import binascii
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return [u"mobidedrm.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
if sys.hexversion >= 0x3000000:
|
||||
print "This script is incompatible with Python 3.x. Please install Python 2.6.x from python.org"
|
||||
print 'This script is incompatible with Python 3.x. Please install Python 2.7.x.'
|
||||
sys.exit(2)
|
||||
|
||||
letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
|
||||
|
||||
def crc32(s):
|
||||
return (~binascii.crc32(s,-1))&0xFFFFFFFF
|
||||
|
@ -53,39 +104,39 @@ def pidFromSerial(s, l):
|
|||
for i in xrange(l):
|
||||
arr1[i] ^= crc_bytes[i&3]
|
||||
|
||||
pid = ""
|
||||
pid = ''
|
||||
for i in xrange(l):
|
||||
b = arr1[i] & 0xff
|
||||
pid+=letters[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))]
|
||||
|
||||
return pid
|
||||
|
||||
def main(argv=sys.argv):
|
||||
print "Mobipocket PID calculator for Amazon Kindle. Copyright (c) 2007, 2009 Igor Skochinsky"
|
||||
def cli_main(argv=unicode_argv()):
|
||||
print u"Mobipocket PID calculator for Amazon Kindle. Copyright © 2007, 2009 Igor Skochinsky"
|
||||
if len(sys.argv)==2:
|
||||
serial = sys.argv[1]
|
||||
else:
|
||||
print "Usage: kindlepid.py <Kindle Serial Number>/<iPhone/iPod Touch UDID>"
|
||||
print u"Usage: kindlepid.py <Kindle Serial Number>/<iPhone/iPod Touch UDID>"
|
||||
return 1
|
||||
if len(serial)==16:
|
||||
if serial.startswith("B"):
|
||||
print "Kindle serial number detected"
|
||||
print u"Kindle serial number detected"
|
||||
else:
|
||||
print "Warning: unrecognized serial number. Please recheck input."
|
||||
print u"Warning: unrecognized serial number. Please recheck input."
|
||||
return 1
|
||||
pid = pidFromSerial(serial,7)+"*"
|
||||
print "Mobipocket PID for Kindle serial# "+serial+" is "+checksumPid(pid)
|
||||
pid = pidFromSerial(serial.encode("utf-8"),7)+'*'
|
||||
print u"Mobipocket PID for Kindle serial#{0} is {1} ".format(serial,checksumPid(pid))
|
||||
return 0
|
||||
elif len(serial)==40:
|
||||
print "iPhone serial number (UDID) detected"
|
||||
pid = pidFromSerial(serial,8)
|
||||
print "Mobipocket PID for iPhone serial# "+serial+" is "+checksumPid(pid)
|
||||
print u"iPhone serial number (UDID) detected"
|
||||
pid = pidFromSerial(serial.encode("utf-8"),8)
|
||||
print u"Mobipocket PID for iPhone serial#{0} is {1} ".format(serial,checksumPid(pid))
|
||||
return 0
|
||||
else:
|
||||
print "Warning: unrecognized serial number. Please recheck input."
|
||||
return 1
|
||||
return 0
|
||||
print u"Warning: unrecognized serial number. Please recheck input."
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
#!/usr/bin/python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# mobidedrm.py, version 0.38
|
||||
# Copyright © 2008 The Dark Reverser
|
||||
#
|
||||
# Modified 2008–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
|
||||
# This is a python script. You need a Python interpreter to run it.
|
||||
# For example, ActiveState Python, which exists for windows.
|
||||
#
|
||||
|
@ -59,26 +65,78 @@
|
|||
# 0.35 - add interface to get mobi_version
|
||||
# 0.36 - fixed problem with TEXtREAd and getBookTitle interface
|
||||
# 0.37 - Fixed double announcement for stand-alone operation
|
||||
# 0.38 - Unicode used wherever possible, cope with absent alfcrypto
|
||||
|
||||
|
||||
__version__ = '0.37'
|
||||
__version__ = u"0.38"
|
||||
|
||||
import sys
|
||||
import os
|
||||
import struct
|
||||
import binascii
|
||||
try:
|
||||
from alfcrypto import Pukall_Cipher
|
||||
except:
|
||||
print u"AlfCrypto not found. Using python PC1 implementation."
|
||||
|
||||
class Unbuffered:
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
|
||||
import os
|
||||
import struct
|
||||
import binascii
|
||||
from alfcrypto import Pukall_Cipher
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return [u"mobidedrm.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = 'utf-8'
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
@ -90,40 +148,45 @@ class DrmException(Exception):
|
|||
|
||||
# Implementation of Pukall Cipher 1
|
||||
def PC1(key, src, decryption=True):
|
||||
return Pukall_Cipher().PC1(key,src,decryption)
|
||||
# sum1 = 0;
|
||||
# sum2 = 0;
|
||||
# keyXorVal = 0;
|
||||
# if len(key)!=16:
|
||||
# print "Bad key length!"
|
||||
# return None
|
||||
# wkey = []
|
||||
# for i in xrange(8):
|
||||
# wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
|
||||
# dst = ""
|
||||
# for i in xrange(len(src)):
|
||||
# temp1 = 0;
|
||||
# byteXorVal = 0;
|
||||
# for j in xrange(8):
|
||||
# temp1 ^= wkey[j]
|
||||
# sum2 = (sum2+j)*20021 + sum1
|
||||
# sum1 = (temp1*346)&0xFFFF
|
||||
# sum2 = (sum2+sum1)&0xFFFF
|
||||
# temp1 = (temp1*20021+1)&0xFFFF
|
||||
# byteXorVal ^= temp1 ^ sum2
|
||||
# curByte = ord(src[i])
|
||||
# if not decryption:
|
||||
# keyXorVal = curByte * 257;
|
||||
# curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
|
||||
# if decryption:
|
||||
# keyXorVal = curByte * 257;
|
||||
# for j in xrange(8):
|
||||
# wkey[j] ^= keyXorVal;
|
||||
# dst+=chr(curByte)
|
||||
# return dst
|
||||
# if we can get it from alfcrypto, use that
|
||||
try:
|
||||
return Pukall_Cipher().PC1(key,src,decryption)
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
# use slow python version, since Pukall_Cipher didn't load
|
||||
sum1 = 0;
|
||||
sum2 = 0;
|
||||
keyXorVal = 0;
|
||||
if len(key)!=16:
|
||||
DrmException (u"PC1: Bad key length")
|
||||
wkey = []
|
||||
for i in xrange(8):
|
||||
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
|
||||
dst = ""
|
||||
for i in xrange(len(src)):
|
||||
temp1 = 0;
|
||||
byteXorVal = 0;
|
||||
for j in xrange(8):
|
||||
temp1 ^= wkey[j]
|
||||
sum2 = (sum2+j)*20021 + sum1
|
||||
sum1 = (temp1*346)&0xFFFF
|
||||
sum2 = (sum2+sum1)&0xFFFF
|
||||
temp1 = (temp1*20021+1)&0xFFFF
|
||||
byteXorVal ^= temp1 ^ sum2
|
||||
curByte = ord(src[i])
|
||||
if not decryption:
|
||||
keyXorVal = curByte * 257;
|
||||
curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
|
||||
if decryption:
|
||||
keyXorVal = curByte * 257;
|
||||
for j in xrange(8):
|
||||
wkey[j] ^= keyXorVal;
|
||||
dst+=chr(curByte)
|
||||
return dst
|
||||
|
||||
def checksumPid(s):
|
||||
letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
|
||||
crc = (~binascii.crc32(s,-1))&0xFFFFFFFF
|
||||
crc = crc ^ (crc >> 16)
|
||||
res = s
|
||||
|
@ -171,17 +234,24 @@ class MobiBook:
|
|||
off = self.sections[section][0]
|
||||
return self.data_file[off:endoff]
|
||||
|
||||
def __init__(self, infile, announce = True):
|
||||
if announce:
|
||||
print ('MobiDeDrm v%(__version__)s. '
|
||||
'Copyright 2008-2012 The Dark Reverser et al.' % globals())
|
||||
def cleanup(self):
|
||||
# to match function in Topaz book
|
||||
pass
|
||||
|
||||
def __init__(self, infile):
|
||||
print u"MobiDeDrm v{0:s}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__)
|
||||
|
||||
try:
|
||||
from alfcrypto import Pukall_Cipher
|
||||
except:
|
||||
print u"AlfCrypto not found. Using python PC1 implementation."
|
||||
|
||||
# initial sanity check on file
|
||||
self.data_file = file(infile, 'rb').read()
|
||||
self.mobi_data = ''
|
||||
self.header = self.data_file[0:78]
|
||||
if self.header[0x3C:0x3C+8] != 'BOOKMOBI' and self.header[0x3C:0x3C+8] != 'TEXtREAd':
|
||||
raise DrmException("invalid file format")
|
||||
raise DrmException(u"Invalid file format")
|
||||
self.magic = self.header[0x3C:0x3C+8]
|
||||
self.crypto_type = -1
|
||||
|
||||
|
@ -199,7 +269,7 @@ class MobiBook:
|
|||
self.compression, = struct.unpack('>H', self.sect[0x0:0x0+2])
|
||||
|
||||
if self.magic == 'TEXtREAd':
|
||||
print "Book has format: ", self.magic
|
||||
print u"PalmDoc format book detected."
|
||||
self.extra_data_flags = 0
|
||||
self.mobi_length = 0
|
||||
self.mobi_codepage = 1252
|
||||
|
@ -209,11 +279,11 @@ class MobiBook:
|
|||
self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18])
|
||||
self.mobi_codepage, = struct.unpack('>L',self.sect[0x1c:0x20])
|
||||
self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C])
|
||||
print "MOBI header version = %d, length = %d" %(self.mobi_version, self.mobi_length)
|
||||
print u"MOBI header version {0:d}, header length {1:d}".format(self.mobi_version, self.mobi_length)
|
||||
self.extra_data_flags = 0
|
||||
if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
|
||||
self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4])
|
||||
print "Extra Data Flags = %d" % self.extra_data_flags
|
||||
print u"Extra Data Flags: {0:d}".format(self.extra_data_flags)
|
||||
if (self.compression != 17480):
|
||||
# multibyte utf8 data is included in the encryption for PalmDoc compression
|
||||
# so clear that byte so that we leave it to be decrypted.
|
||||
|
@ -223,10 +293,10 @@ class MobiBook:
|
|||
self.meta_array = {}
|
||||
try:
|
||||
exth_flag, = struct.unpack('>L', self.sect[0x80:0x84])
|
||||
exth = 'NONE'
|
||||
exth = ''
|
||||
if exth_flag & 0x40:
|
||||
exth = self.sect[16 + self.mobi_length:]
|
||||
if (len(exth) >= 4) and (exth[:4] == 'EXTH'):
|
||||
if (len(exth) >= 12) and (exth[:4] == 'EXTH'):
|
||||
nitems, = struct.unpack('>I', exth[8:12])
|
||||
pos = 12
|
||||
for i in xrange(nitems):
|
||||
|
@ -236,10 +306,10 @@ class MobiBook:
|
|||
# reset the text to speech flag and clipping limit, if present
|
||||
if type == 401 and size == 9:
|
||||
# set clipping limit to 100%
|
||||
self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8)
|
||||
self.patchSection(0, '\144', 16 + self.mobi_length + pos + 8)
|
||||
elif type == 404 and size == 9:
|
||||
# make sure text to speech is enabled
|
||||
self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8)
|
||||
self.patchSection(0, '\0', 16 + self.mobi_length + pos + 8)
|
||||
# print type, size, content, content.encode('hex')
|
||||
pos += size
|
||||
except:
|
||||
|
@ -265,8 +335,8 @@ class MobiBook:
|
|||
codec = codec_map[self.mobi_codepage]
|
||||
if title == '':
|
||||
title = self.header[:32]
|
||||
title = title.split("\0")[0]
|
||||
return unicode(title, codec).encode('utf-8')
|
||||
title = title.split('\0')[0]
|
||||
return unicode(title, codec)
|
||||
|
||||
def getPIDMetaInfo(self):
|
||||
rec209 = ''
|
||||
|
@ -297,7 +367,7 @@ class MobiBook:
|
|||
|
||||
def parseDRM(self, data, count, pidlist):
|
||||
found_key = None
|
||||
keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96"
|
||||
keyvec1 = '\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96'
|
||||
for pid in pidlist:
|
||||
bigpid = pid.ljust(16,'\0')
|
||||
temp_key = PC1(keyvec1, bigpid, False)
|
||||
|
@ -315,7 +385,7 @@ class MobiBook:
|
|||
break
|
||||
if not found_key:
|
||||
# Then try the default encoding that doesn't require a PID
|
||||
pid = "00000000"
|
||||
pid = '00000000'
|
||||
temp_key = keyvec1
|
||||
temp_key_sum = sum(map(ord,temp_key)) & 0xff
|
||||
for i in xrange(count):
|
||||
|
@ -328,82 +398,90 @@ class MobiBook:
|
|||
break
|
||||
return [found_key,pid]
|
||||
|
||||
def getMobiFile(self, outpath):
|
||||
def getFile(self, outpath):
|
||||
file(outpath,'wb').write(self.mobi_data)
|
||||
|
||||
def getMobiVersion(self):
|
||||
return self.mobi_version
|
||||
def getBookType(self):
|
||||
if self.print_replica:
|
||||
return u"Print Replica"
|
||||
if self.mobi_version >= 8:
|
||||
return u"Kindle Format 8"
|
||||
return u"Mobipocket"
|
||||
|
||||
def getPrintReplica(self):
|
||||
return self.print_replica
|
||||
def getBookExtension(self):
|
||||
if self.print_replica:
|
||||
return u".azw4"
|
||||
if self.mobi_version >= 8:
|
||||
return u".azw3"
|
||||
return u".mobi"
|
||||
|
||||
def processBook(self, pidlist):
|
||||
crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2])
|
||||
print 'Crypto Type is: ', crypto_type
|
||||
print u"Crypto Type is: {0:d}".format(crypto_type)
|
||||
self.crypto_type = crypto_type
|
||||
if crypto_type == 0:
|
||||
print "This book is not encrypted."
|
||||
print u"This book is not encrypted."
|
||||
# we must still check for Print Replica
|
||||
self.print_replica = (self.loadSection(1)[0:4] == '%MOP')
|
||||
self.mobi_data = self.data_file
|
||||
return
|
||||
if crypto_type != 2 and crypto_type != 1:
|
||||
raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type)
|
||||
raise DrmException(u"Cannot decode unknown Mobipocket encryption type {0:d}".format(crypto_type))
|
||||
if 406 in self.meta_array:
|
||||
data406 = self.meta_array[406]
|
||||
val406, = struct.unpack('>Q',data406)
|
||||
if val406 != 0:
|
||||
raise DrmException("Cannot decode library or rented ebooks.")
|
||||
raise DrmException(u"Cannot decode library or rented ebooks.")
|
||||
|
||||
goodpids = []
|
||||
for pid in pidlist:
|
||||
if len(pid)==10:
|
||||
if checksumPid(pid[0:-2]) != pid:
|
||||
print "Warning: PID " + pid + " has incorrect checksum, should have been "+checksumPid(pid[0:-2])
|
||||
print u"Warning: PID {0} has incorrect checksum, should have been {1}".format(pid,checksumPid(pid[0:-2]))
|
||||
goodpids.append(pid[0:-2])
|
||||
elif len(pid)==8:
|
||||
goodpids.append(pid)
|
||||
|
||||
if self.crypto_type == 1:
|
||||
t1_keyvec = "QDCVEPMU675RUBSZ"
|
||||
t1_keyvec = 'QDCVEPMU675RUBSZ'
|
||||
if self.magic == 'TEXtREAd':
|
||||
bookkey_data = self.sect[0x0E:0x0E+16]
|
||||
elif self.mobi_version < 0:
|
||||
bookkey_data = self.sect[0x90:0x90+16]
|
||||
else:
|
||||
bookkey_data = self.sect[self.mobi_length+16:self.mobi_length+32]
|
||||
pid = "00000000"
|
||||
pid = '00000000'
|
||||
found_key = PC1(t1_keyvec, bookkey_data)
|
||||
else :
|
||||
# calculate the keys
|
||||
drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16])
|
||||
if drm_count == 0:
|
||||
raise DrmException("Not yet initialised with PID. Must be opened with Mobipocket Reader first.")
|
||||
raise DrmException(u"Encryption not initialised. Must be opened with Mobipocket Reader first.")
|
||||
found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids)
|
||||
if not found_key:
|
||||
raise DrmException("No key found in " + str(len(goodpids)) + " keys tried. Read the FAQs at Alf's blog. Only if none apply, report this failure for help.")
|
||||
raise DrmException(u"No key found in {0:d} keys tried. Read the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(len(goodpids)))
|
||||
# kill the drm keys
|
||||
self.patchSection(0, "\0" * drm_size, drm_ptr)
|
||||
self.patchSection(0, '\0' * drm_size, drm_ptr)
|
||||
# kill the drm pointers
|
||||
self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8)
|
||||
self.patchSection(0, '\xff' * 4 + '\0' * 12, 0xA8)
|
||||
|
||||
if pid=="00000000":
|
||||
print "File has default encryption, no specific PID."
|
||||
if pid=='00000000':
|
||||
print u"File has default encryption, no specific key needed."
|
||||
else:
|
||||
print "File is encoded with PID "+checksumPid(pid)+"."
|
||||
print u"File is encoded with PID {0}.".format(checksumPid(pid))
|
||||
|
||||
# clear the crypto type
|
||||
self.patchSection(0, "\0" * 2, 0xC)
|
||||
|
||||
# decrypt sections
|
||||
print "Decrypting. Please wait . . .",
|
||||
print u"Decrypting. Please wait . . .",
|
||||
mobidataList = []
|
||||
mobidataList.append(self.data_file[:self.sections[1][0]])
|
||||
for i in xrange(1, self.records+1):
|
||||
data = self.loadSection(i)
|
||||
extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags)
|
||||
if i%100 == 0:
|
||||
print ".",
|
||||
print u".",
|
||||
# print "record %d, extra_size %d" %(i,extra_size)
|
||||
decoded_data = PC1(found_key, data[0:len(data) - extra_size])
|
||||
if i==1:
|
||||
|
@ -414,31 +492,24 @@ class MobiBook:
|
|||
if self.num_sections > self.records+1:
|
||||
mobidataList.append(self.data_file[self.sections[self.records+1][0]:])
|
||||
self.mobi_data = "".join(mobidataList)
|
||||
print "done"
|
||||
print u"done"
|
||||
return
|
||||
|
||||
def getUnencryptedBook(infile,pid,announce=True):
|
||||
def getUnencryptedBook(infile,pidlist):
|
||||
if not os.path.isfile(infile):
|
||||
raise DrmException('Input File Not Found')
|
||||
book = MobiBook(infile,announce)
|
||||
book.processBook([pid])
|
||||
return book.mobi_data
|
||||
|
||||
def getUnencryptedBookWithList(infile,pidlist,announce=True):
|
||||
if not os.path.isfile(infile):
|
||||
raise DrmException('Input File Not Found')
|
||||
book = MobiBook(infile, announce)
|
||||
raise DrmException(u"Input File Not Found.")
|
||||
book = MobiBook(infile)
|
||||
book.processBook(pidlist)
|
||||
return book.mobi_data
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
print ('MobiDeDrm v%(__version__)s. '
|
||||
'Copyright 2008-2012 The Dark Reverser et al.' % globals())
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
if len(argv)<3 or len(argv)>4:
|
||||
print "Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks"
|
||||
print "Usage:"
|
||||
print " %s <infile> <outfile> [<Comma separated list of PIDs to try>]" % sys.argv[0]
|
||||
print u"MobiDeDrm v{0}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__)
|
||||
print u"Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks"
|
||||
print u"Usage:"
|
||||
print u" {0} <infile> <outfile> [<Comma separated list of PIDs to try>]".format(os.path.basename(sys.argv[0]))
|
||||
return 1
|
||||
else:
|
||||
infile = argv[1]
|
||||
|
@ -446,15 +517,17 @@ def main(argv=sys.argv):
|
|||
if len(argv) is 4:
|
||||
pidlist = argv[3].split(',')
|
||||
else:
|
||||
pidlist = {}
|
||||
pidlist = []
|
||||
try:
|
||||
stripped_file = getUnencryptedBookWithList(infile, pidlist, False)
|
||||
stripped_file = getUnencryptedBook(infile, pidlist)
|
||||
file(outfile, 'wb').write(stripped_file)
|
||||
except DrmException, e:
|
||||
print "Error: %s" % e
|
||||
print u"MobiDeDRM v{0} Error: {0:s}".format(__version__,e.args[0])
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
if __name__ == '__main__':
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
|
|
|
@ -1,43 +1,90 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
class Unbuffered:
|
||||
# topazextract.py, version ?
|
||||
# Mostly written by some_updates based on code from many others
|
||||
|
||||
__version__ = '4.8'
|
||||
|
||||
import sys
|
||||
import os, csv, getopt
|
||||
import zlib, zipfile, tempfile, shutil
|
||||
import traceback
|
||||
from struct import pack
|
||||
from struct import unpack
|
||||
from alfcrypto import Topaz_Cipher
|
||||
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return [u"mobidedrm.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = 'utf-8'
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
buildXML = False
|
||||
|
||||
import os, csv, getopt
|
||||
import zlib, zipfile, tempfile, shutil
|
||||
from struct import pack
|
||||
from struct import unpack
|
||||
from alfcrypto import Topaz_Cipher
|
||||
|
||||
class TpzDRMError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# local support routines
|
||||
if inCalibre:
|
||||
from calibre_plugins.k4mobidedrm import kgenpids
|
||||
else:
|
||||
inCalibre = False
|
||||
import kgenpids
|
||||
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# recursive zip creation support routine
|
||||
def zipUpDir(myzip, tdir, localname):
|
||||
currentdir = tdir
|
||||
if localname != "":
|
||||
if localname != u"":
|
||||
currentdir = os.path.join(currentdir,localname)
|
||||
list = os.listdir(currentdir)
|
||||
for file in list:
|
||||
|
@ -73,7 +120,7 @@ def bookReadEncodedNumber(fo):
|
|||
# Get a length prefixed string from file
|
||||
def bookReadString(fo):
|
||||
stringLength = bookReadEncodedNumber(fo)
|
||||
return unpack(str(stringLength)+"s",fo.read(stringLength))[0]
|
||||
return unpack(str(stringLength)+'s',fo.read(stringLength))[0]
|
||||
|
||||
#
|
||||
# crypto routines
|
||||
|
@ -112,13 +159,13 @@ def decryptRecord(data,PID):
|
|||
# Try to decrypt a dkey record (contains the bookPID)
|
||||
def decryptDkeyRecord(data,PID):
|
||||
record = decryptRecord(data,PID)
|
||||
fields = unpack("3sB8sB8s3s",record)
|
||||
if fields[0] != "PID" or fields[5] != "pid" :
|
||||
raise TpzDRMError("Didn't find PID magic numbers in record")
|
||||
fields = unpack('3sB8sB8s3s',record)
|
||||
if fields[0] != 'PID' or fields[5] != 'pid' :
|
||||
raise DrmException(u"Didn't find PID magic numbers in record")
|
||||
elif fields[1] != 8 or fields[3] != 8 :
|
||||
raise TpzDRMError("Record didn't contain correct length fields")
|
||||
raise DrmException(u"Record didn't contain correct length fields")
|
||||
elif fields[2] != PID :
|
||||
raise TpzDRMError("Record didn't contain PID")
|
||||
raise DrmException(u"Record didn't contain PID")
|
||||
return fields[4]
|
||||
|
||||
# Decrypt all dkey records (contain the book PID)
|
||||
|
@ -131,11 +178,11 @@ def decryptDkeyRecords(data,PID):
|
|||
try:
|
||||
key = decryptDkeyRecord(data[1:length+1],PID)
|
||||
records.append(key)
|
||||
except TpzDRMError:
|
||||
except DrmException:
|
||||
pass
|
||||
data = data[1+length:]
|
||||
if len(records) == 0:
|
||||
raise TpzDRMError("BookKey Not Found")
|
||||
raise DrmException(u"BookKey Not Found")
|
||||
return records
|
||||
|
||||
|
||||
|
@ -148,9 +195,9 @@ class TopazBook:
|
|||
self.bookHeaderRecords = {}
|
||||
self.bookMetadata = {}
|
||||
self.bookKey = None
|
||||
magic = unpack("4s",self.fo.read(4))[0]
|
||||
magic = unpack('4s',self.fo.read(4))[0]
|
||||
if magic != 'TPZ0':
|
||||
raise TpzDRMError("Parse Error : Invalid Header, not a Topaz file")
|
||||
raise DrmException(u"Parse Error : Invalid Header, not a Topaz file")
|
||||
self.parseTopazHeaders()
|
||||
self.parseMetadata()
|
||||
|
||||
|
@ -167,7 +214,7 @@ class TopazBook:
|
|||
# Read and parse one header record at the current book file position and return the associated data
|
||||
# [[offset,decompressedLength,compressedLength],...]
|
||||
if ord(self.fo.read(1)) != 0x63:
|
||||
raise TpzDRMError("Parse Error : Invalid Header")
|
||||
raise DrmException(u"Parse Error : Invalid Header")
|
||||
tag = bookReadString(self.fo)
|
||||
record = bookReadHeaderRecordData()
|
||||
return [tag,record]
|
||||
|
@ -177,15 +224,15 @@ class TopazBook:
|
|||
# print result[0], result[1]
|
||||
self.bookHeaderRecords[result[0]] = result[1]
|
||||
if ord(self.fo.read(1)) != 0x64 :
|
||||
raise TpzDRMError("Parse Error : Invalid Header")
|
||||
raise DrmException(u"Parse Error : Invalid Header")
|
||||
self.bookPayloadOffset = self.fo.tell()
|
||||
|
||||
def parseMetadata(self):
|
||||
# Parse the metadata record from the book payload and return a list of [key,values]
|
||||
self.fo.seek(self.bookPayloadOffset + self.bookHeaderRecords["metadata"][0][0])
|
||||
self.fo.seek(self.bookPayloadOffset + self.bookHeaderRecords['metadata'][0][0])
|
||||
tag = bookReadString(self.fo)
|
||||
if tag != "metadata" :
|
||||
raise TpzDRMError("Parse Error : Record Names Don't Match")
|
||||
if tag != 'metadata' :
|
||||
raise DrmException(u"Parse Error : Record Names Don't Match")
|
||||
flags = ord(self.fo.read(1))
|
||||
nbRecords = ord(self.fo.read(1))
|
||||
# print nbRecords
|
||||
|
@ -210,7 +257,7 @@ class TopazBook:
|
|||
title = ''
|
||||
if 'Title' in self.bookMetadata:
|
||||
title = self.bookMetadata['Title']
|
||||
return title
|
||||
return title.decode('utf-8')
|
||||
|
||||
def setBookKey(self, key):
|
||||
self.bookKey = key
|
||||
|
@ -223,13 +270,13 @@ class TopazBook:
|
|||
try:
|
||||
recordOffset = self.bookHeaderRecords[name][index][0]
|
||||
except:
|
||||
raise TpzDRMError("Parse Error : Invalid Record, record not found")
|
||||
raise DrmException("Parse Error : Invalid Record, record not found")
|
||||
|
||||
self.fo.seek(self.bookPayloadOffset + recordOffset)
|
||||
|
||||
tag = bookReadString(self.fo)
|
||||
if tag != name :
|
||||
raise TpzDRMError("Parse Error : Invalid Record, record name doesn't match")
|
||||
raise DrmException("Parse Error : Invalid Record, record name doesn't match")
|
||||
|
||||
recordIndex = bookReadEncodedNumber(self.fo)
|
||||
if recordIndex < 0 :
|
||||
|
@ -237,7 +284,7 @@ class TopazBook:
|
|||
recordIndex = -recordIndex -1
|
||||
|
||||
if recordIndex != index :
|
||||
raise TpzDRMError("Parse Error : Invalid Record, index doesn't match")
|
||||
raise DrmException("Parse Error : Invalid Record, index doesn't match")
|
||||
|
||||
if (self.bookHeaderRecords[name][index][2] > 0):
|
||||
compressed = True
|
||||
|
@ -250,7 +297,7 @@ class TopazBook:
|
|||
ctx = topazCryptoInit(self.bookKey)
|
||||
record = topazCryptoDecrypt(record,ctx)
|
||||
else :
|
||||
raise TpzDRMError("Error: Attempt to decrypt without bookKey")
|
||||
raise DrmException("Error: Attempt to decrypt without bookKey")
|
||||
|
||||
if compressed:
|
||||
record = zlib.decompress(record)
|
||||
|
@ -262,12 +309,12 @@ class TopazBook:
|
|||
fixedimage=True
|
||||
try:
|
||||
keydata = self.getBookPayloadRecord('dkey', 0)
|
||||
except TpzDRMError, e:
|
||||
print "no dkey record found, book may not be encrypted"
|
||||
print "attempting to extrct files without a book key"
|
||||
except DrmException, e:
|
||||
print u"no dkey record found, book may not be encrypted"
|
||||
print u"attempting to extrct files without a book key"
|
||||
self.createBookDirectory()
|
||||
self.extractFiles()
|
||||
print "Successfully Extracted Topaz contents"
|
||||
print u"Successfully Extracted Topaz contents"
|
||||
if inCalibre:
|
||||
from calibre_plugins.k4mobidedrm import genbook
|
||||
else:
|
||||
|
@ -275,7 +322,7 @@ class TopazBook:
|
|||
|
||||
rv = genbook.generateBook(self.outdir, raw, fixedimage)
|
||||
if rv == 0:
|
||||
print "\nBook Successfully generated"
|
||||
print u"Book Successfully generated."
|
||||
return rv
|
||||
|
||||
# try each pid to decode the file
|
||||
|
@ -283,25 +330,25 @@ class TopazBook:
|
|||
for pid in pidlst:
|
||||
# use 8 digit pids here
|
||||
pid = pid[0:8]
|
||||
print "\nTrying: ", pid
|
||||
print u"Trying: {0}".format(pid)
|
||||
bookKeys = []
|
||||
data = keydata
|
||||
try:
|
||||
bookKeys+=decryptDkeyRecords(data,pid)
|
||||
except TpzDRMError, e:
|
||||
except DrmException, e:
|
||||
pass
|
||||
else:
|
||||
bookKey = bookKeys[0]
|
||||
print "Book Key Found!"
|
||||
print u"Book Key Found! ({0})".format(bookKey.encode('hex'))
|
||||
break
|
||||
|
||||
if not bookKey:
|
||||
raise TpzDRMError("Topaz Book. No key found in " + str(len(pidlst)) + " keys tried. Read the FAQs at Alf's blog. Only if none apply, report this failure for help.")
|
||||
raise DrmException(u"No key found in {0:d} keys tried. Read the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(len(pidlst)))
|
||||
|
||||
self.setBookKey(bookKey)
|
||||
self.createBookDirectory()
|
||||
self.extractFiles()
|
||||
print "Successfully Extracted Topaz contents"
|
||||
print u"Successfully Extracted Topaz contents"
|
||||
if inCalibre:
|
||||
from calibre_plugins.k4mobidedrm import genbook
|
||||
else:
|
||||
|
@ -309,7 +356,7 @@ class TopazBook:
|
|||
|
||||
rv = genbook.generateBook(self.outdir, raw, fixedimage)
|
||||
if rv == 0:
|
||||
print "\nBook Successfully generated"
|
||||
print u"Book Successfully generated"
|
||||
return rv
|
||||
|
||||
def createBookDirectory(self):
|
||||
|
@ -317,16 +364,16 @@ class TopazBook:
|
|||
# create output directory structure
|
||||
if not os.path.exists(outdir):
|
||||
os.makedirs(outdir)
|
||||
destdir = os.path.join(outdir,'img')
|
||||
destdir = os.path.join(outdir,u"img")
|
||||
if not os.path.exists(destdir):
|
||||
os.makedirs(destdir)
|
||||
destdir = os.path.join(outdir,'color_img')
|
||||
destdir = os.path.join(outdir,u"color_img")
|
||||
if not os.path.exists(destdir):
|
||||
os.makedirs(destdir)
|
||||
destdir = os.path.join(outdir,'page')
|
||||
destdir = os.path.join(outdir,u"page")
|
||||
if not os.path.exists(destdir):
|
||||
os.makedirs(destdir)
|
||||
destdir = os.path.join(outdir,'glyphs')
|
||||
destdir = os.path.join(outdir,u"glyphs")
|
||||
if not os.path.exists(destdir):
|
||||
os.makedirs(destdir)
|
||||
|
||||
|
@ -334,149 +381,148 @@ class TopazBook:
|
|||
outdir = self.outdir
|
||||
for headerRecord in self.bookHeaderRecords:
|
||||
name = headerRecord
|
||||
if name != "dkey" :
|
||||
ext = '.dat'
|
||||
if name == 'img' : ext = '.jpg'
|
||||
if name == 'color' : ext = '.jpg'
|
||||
print "\nProcessing Section: %s " % name
|
||||
if name != 'dkey':
|
||||
ext = u".dat"
|
||||
if name == 'img': ext = u".jpg"
|
||||
if name == 'color' : ext = u".jpg"
|
||||
print u"Processing Section: {0}\n. . .".format(name),
|
||||
for index in range (0,len(self.bookHeaderRecords[name])) :
|
||||
fnum = "%04d" % index
|
||||
fname = name + fnum + ext
|
||||
fname = u"{0}{1:04d}{2}".format(name,index,ext)
|
||||
destdir = outdir
|
||||
if name == 'img':
|
||||
destdir = os.path.join(outdir,'img')
|
||||
destdir = os.path.join(outdir,u"img")
|
||||
if name == 'color':
|
||||
destdir = os.path.join(outdir,'color_img')
|
||||
destdir = os.path.join(outdir,u"color_img")
|
||||
if name == 'page':
|
||||
destdir = os.path.join(outdir,'page')
|
||||
destdir = os.path.join(outdir,u"page")
|
||||
if name == 'glyphs':
|
||||
destdir = os.path.join(outdir,'glyphs')
|
||||
destdir = os.path.join(outdir,u"glyphs")
|
||||
outputFile = os.path.join(destdir,fname)
|
||||
print ".",
|
||||
print u".",
|
||||
record = self.getBookPayloadRecord(name,index)
|
||||
if record != '':
|
||||
file(outputFile, 'wb').write(record)
|
||||
print " "
|
||||
print u" "
|
||||
|
||||
def getHTMLZip(self, zipname):
|
||||
def getFile(self, zipname):
|
||||
htmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
htmlzip.write(os.path.join(self.outdir,'book.html'),'book.html')
|
||||
htmlzip.write(os.path.join(self.outdir,'book.opf'),'book.opf')
|
||||
if os.path.isfile(os.path.join(self.outdir,'cover.jpg')):
|
||||
htmlzip.write(os.path.join(self.outdir,'cover.jpg'),'cover.jpg')
|
||||
htmlzip.write(os.path.join(self.outdir,'style.css'),'style.css')
|
||||
zipUpDir(htmlzip, self.outdir, 'img')
|
||||
htmlzip.write(os.path.join(self.outdir,u"book.html"),u"book.html")
|
||||
htmlzip.write(os.path.join(self.outdir,u"book.opf"),u"book.opf")
|
||||
if os.path.isfile(os.path.join(self.outdir,u"cover.jpg")):
|
||||
htmlzip.write(os.path.join(self.outdir,u"cover.jpg"),u"cover.jpg")
|
||||
htmlzip.write(os.path.join(self.outdir,u"style.css"),u"style.css")
|
||||
zipUpDir(htmlzip, self.outdir, u"img")
|
||||
htmlzip.close()
|
||||
|
||||
def getBookType(self):
|
||||
return u"Topaz"
|
||||
|
||||
def getBookExtension(self):
|
||||
return u".htmlz"
|
||||
|
||||
def getSVGZip(self, zipname):
|
||||
svgzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
svgzip.write(os.path.join(self.outdir,'index_svg.xhtml'),'index_svg.xhtml')
|
||||
zipUpDir(svgzip, self.outdir, 'svg')
|
||||
zipUpDir(svgzip, self.outdir, 'img')
|
||||
svgzip.write(os.path.join(self.outdir,u"index_svg.xhtml"),u"index_svg.xhtml")
|
||||
zipUpDir(svgzip, self.outdir, u"svg")
|
||||
zipUpDir(svgzip, self.outdir, u"img")
|
||||
svgzip.close()
|
||||
|
||||
def getXMLZip(self, zipname):
|
||||
xmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
targetdir = os.path.join(self.outdir,'xml')
|
||||
zipUpDir(xmlzip, targetdir, '')
|
||||
zipUpDir(xmlzip, self.outdir, 'img')
|
||||
xmlzip.close()
|
||||
|
||||
def cleanup(self):
|
||||
if os.path.isdir(self.outdir):
|
||||
shutil.rmtree(self.outdir, True)
|
||||
|
||||
def usage(progname):
|
||||
print "Removes DRM protection from Topaz ebooks and extract the contents"
|
||||
print "Usage:"
|
||||
print " %s [-k <kindle.info>] [-p <pidnums>] [-s <kindleSerialNumbers>] <infile> <outdir> " % progname
|
||||
|
||||
print u"Removes DRM protection from Topaz ebooks and extracts the contents"
|
||||
print u"Usage:"
|
||||
print u" {0} [-k <kindle.info>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] <infile> <outdir>".format(progname)
|
||||
|
||||
# Main
|
||||
def main(argv=sys.argv):
|
||||
global buildXML
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
k4 = False
|
||||
pids = []
|
||||
serials = []
|
||||
kInfoFiles = []
|
||||
print u"TopazExtract v{0}.".format(__version__)
|
||||
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:")
|
||||
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:x")
|
||||
except getopt.GetoptError, err:
|
||||
print str(err)
|
||||
print u"Error in options or arguments: {0}".format(err.args[0])
|
||||
usage(progname)
|
||||
return 1
|
||||
if len(args)<2:
|
||||
usage(progname)
|
||||
return 1
|
||||
|
||||
for o, a in opts:
|
||||
if o == "-k":
|
||||
if a == None :
|
||||
print "Invalid parameter for -k"
|
||||
return 1
|
||||
kInfoFiles.append(a)
|
||||
if o == "-p":
|
||||
if a == None :
|
||||
print "Invalid parameter for -p"
|
||||
return 1
|
||||
pids = a.split(',')
|
||||
if o == "-s":
|
||||
if a == None :
|
||||
print "Invalid parameter for -s"
|
||||
return 1
|
||||
serials = a.split(',')
|
||||
k4 = True
|
||||
|
||||
infile = args[0]
|
||||
outdir = args[1]
|
||||
|
||||
if not os.path.isfile(infile):
|
||||
print "Input File Does Not Exist"
|
||||
print u"Input File {0} Does Not Exist.".format(infile)
|
||||
return 1
|
||||
|
||||
if not os.path.exists(outdir):
|
||||
print u"Output Directory {0} Does Not Exist.".format(outdir)
|
||||
return 1
|
||||
|
||||
kInfoFiles = []
|
||||
serials = []
|
||||
pids = []
|
||||
|
||||
for o, a in opts:
|
||||
if o == '-k':
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -k")
|
||||
kInfoFiles.append(a)
|
||||
if o == '-p':
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -p")
|
||||
pids = a.split(',')
|
||||
if o == '-s':
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -s")
|
||||
serials = [serial.replace(" ","") for serial in a.split(',')]
|
||||
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
|
||||
tb = TopazBook(infile)
|
||||
title = tb.getBookTitle()
|
||||
print "Processing Book: ", title
|
||||
keysRecord, keysRecordRecord = tb.getPIDMetaInfo()
|
||||
pids.extend(kgenpids.getPidList(keysRecord, keysRecordRecord, k4, serials, kInfoFiles))
|
||||
print u"Processing Book: {0}".format(title)
|
||||
md1, md2 = tb.getPIDMetaInfo()
|
||||
pids.extend(kgenpids.getPidList(md1, md2, serials, kInfoFiles))
|
||||
|
||||
try:
|
||||
print "Decrypting Book"
|
||||
print u"Decrypting Book"
|
||||
tb.processBook(pids)
|
||||
|
||||
print " Creating HTML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_nodrm' + '.htmlz')
|
||||
tb.getHTMLZip(zipname)
|
||||
print u" Creating HTML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + u"_nodrm.htmlz")
|
||||
tb.getFile(zipname)
|
||||
|
||||
print " Creating SVG ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_SVG' + '.zip')
|
||||
print u" Creating SVG ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + u"_SVG.zip")
|
||||
tb.getSVGZip(zipname)
|
||||
|
||||
if buildXML:
|
||||
print " Creating XML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_XML' + '.zip')
|
||||
tb.getXMLZip(zipname)
|
||||
|
||||
# removing internal temporary directory of pieces
|
||||
tb.cleanup()
|
||||
|
||||
except TpzDRMError, e:
|
||||
print str(e)
|
||||
# tb.cleanup()
|
||||
except DrmException, e:
|
||||
print u"Decryption failed\n{0}".format(traceback.format_exc())
|
||||
|
||||
try:
|
||||
tb.cleanup()
|
||||
except:
|
||||
pass
|
||||
return 1
|
||||
|
||||
except Exception, e:
|
||||
print str(e)
|
||||
# tb.cleanup
|
||||
print u"Decryption failed\m{0}".format(traceback.format_exc())
|
||||
try:
|
||||
tb.cleanup()
|
||||
except:
|
||||
pass
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
sys.exit(main())
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import zlib
|
||||
|
@ -27,14 +28,10 @@ class fixZip:
|
|||
self.ztype = 'zip'
|
||||
if zinput.lower().find('.epub') >= 0 :
|
||||
self.ztype = 'epub'
|
||||
print "opening input"
|
||||
self.inzip = zipfilerugged.ZipFile(zinput,'r')
|
||||
print "opening outout"
|
||||
self.outzip = zipfilerugged.ZipFile(zoutput,'w')
|
||||
print "opening input as raw file"
|
||||
# open the input zip for reading only as a raw file
|
||||
self.bzf = file(zinput,'rb')
|
||||
print "finished initialising"
|
||||
|
||||
def getlocalname(self, zi):
|
||||
local_header_offset = zi.header_offset
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
#!/usr/bin/env python
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# DeDRM.pyw, version 5.5
|
||||
# By some_updates and Apprentice Alf
|
||||
|
||||
import sys
|
||||
import os, os.path
|
||||
sys.path.append(sys.path[0]+os.sep+'lib')
|
||||
sys.path.append(os.path.join(sys.path[0],"lib"))
|
||||
os.environ['PYTHONIOENCODING'] = "utf-8"
|
||||
|
||||
import shutil
|
||||
|
@ -21,7 +24,7 @@ import re
|
|||
import simpleprefs
|
||||
|
||||
|
||||
__version__ = '5.4.1'
|
||||
__version__ = '5.5'
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
@ -327,7 +330,7 @@ class ConvDialog(Toplevel):
|
|||
self.running = 'inactive'
|
||||
self.numgood = 0
|
||||
self.numbad = 0
|
||||
self.log = ''
|
||||
self.log = u""
|
||||
self.status = Tkinter.Label(self, text='DeDRM processing...')
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
|
@ -375,18 +378,16 @@ class ConvDialog(Toplevel):
|
|||
if len(self.filenames) > 0:
|
||||
filename = self.filenames.pop(0)
|
||||
if filename == None:
|
||||
msg = '\nComplete: '
|
||||
msg += 'Successes: %d, ' % self.numgood
|
||||
msg += 'Failures: %d\n' % self.numbad
|
||||
msg = u"\nComplete: Successes: {0}, Failures: {1}\n".format(self.numgood,self.numbad)
|
||||
self.showCmdOutput(msg)
|
||||
if self.numbad == 0:
|
||||
self.after(2000,self.conversion_done())
|
||||
logfile = os.path.join(rscpath,'dedrm.log')
|
||||
file(logfile,'w').write(self.log)
|
||||
file(logfile,'w').write(self.log.encode('utf8'))
|
||||
return
|
||||
infile = filename
|
||||
bname = os.path.basename(infile)
|
||||
msg = 'Processing: ' + bname + ' ... '
|
||||
msg = u"Processing: {0} ... ".format(bname)
|
||||
self.log += msg
|
||||
self.showCmdOutput(msg)
|
||||
outdir = os.path.dirname(filename)
|
||||
|
@ -400,7 +401,7 @@ class ConvDialog(Toplevel):
|
|||
self.running = 'active'
|
||||
self.processPipe()
|
||||
else:
|
||||
msg = 'Unknown File: ' + bname + '\n'
|
||||
msg = u"Unknown File: {0}\n".format(bname)
|
||||
self.log += msg
|
||||
self.showCmdOutput(msg)
|
||||
self.numbad += 1
|
||||
|
@ -433,18 +434,17 @@ class ConvDialog(Toplevel):
|
|||
if poll != None:
|
||||
self.bar.stop()
|
||||
if poll == 0:
|
||||
msg = 'Success\n'
|
||||
msg = u"\nSuccess\n"
|
||||
self.numgood += 1
|
||||
text = self.p2.read()
|
||||
text += self.p2.readerr()
|
||||
text = self.p2.read().decode('utf8')
|
||||
text += self.p2.readerr().decode('utf8')
|
||||
self.log += text
|
||||
self.log += msg
|
||||
if poll != 0:
|
||||
msg = 'Failed\n'
|
||||
text = self.p2.read()
|
||||
text += self.p2.readerr()
|
||||
else:
|
||||
text = self.p2.read().decode('utf8')
|
||||
text += self.p2.readerr().decode('utf8')
|
||||
msg += text
|
||||
msg += '\n'
|
||||
msg += u"\nFailed\n"
|
||||
self.numbad += 1
|
||||
self.log += msg
|
||||
self.showCmdOutput(msg)
|
||||
|
@ -491,7 +491,7 @@ def runit(apphome, ncmd, nparms):
|
|||
# cmdline = pengine + ' "' + os.path.join(apphome, ncmd) + '" '
|
||||
cmdline += nparms
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p2 = subasyncio.Process(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
p2 = subasyncio.Process(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False, env = os.environ)
|
||||
return p2
|
||||
|
||||
def processK4MOBI(apphome, infile, outdir, rscpath):
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
#! /usr/bin/env python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# crypto library mainly by some_updates
|
||||
|
||||
# pbkdf2.py pbkdf2 code taken from pbkdf2.py
|
||||
# pbkdf2.py Copyright © 2004 Matt Johnston <matt @ ucc asn au>
|
||||
# pbkdf2.py Copyright © 2009 Daniel Holth <dholth@fastmail.fm>
|
||||
# pbkdf2.py This code may be freely used and modified for any purpose.
|
||||
|
||||
import sys, os
|
||||
import hmac
|
||||
from struct import pack
|
||||
import hashlib
|
||||
|
||||
|
||||
# interface to needed routines libalfcrypto
|
||||
def _load_libalfcrypto():
|
||||
import ctypes
|
||||
|
@ -26,8 +33,8 @@ def _load_libalfcrypto():
|
|||
name_of_lib = 'libalfcrypto32.so'
|
||||
else:
|
||||
name_of_lib = 'libalfcrypto64.so'
|
||||
|
||||
libalfcrypto = sys.path[0] + os.sep + name_of_lib
|
||||
|
||||
libalfcrypto = os.path.join(sys.path[0],name_of_lib)
|
||||
|
||||
if not os.path.isfile(libalfcrypto):
|
||||
raise Exception('libalfcrypto not found')
|
||||
|
@ -55,7 +62,7 @@ def _load_libalfcrypto():
|
|||
#
|
||||
# int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key);
|
||||
#
|
||||
#
|
||||
#
|
||||
# void AES_cbc_encrypt(const unsigned char *in, unsigned char *out,
|
||||
# const unsigned long length, const AES_KEY *key,
|
||||
# unsigned char *ivec, const int enc);
|
||||
|
@ -147,7 +154,7 @@ def _load_libalfcrypto():
|
|||
topazCryptoDecrypt(ctx, data, out, len(data))
|
||||
return out.raw
|
||||
|
||||
print "Using Library AlfCrypto DLL/DYLIB/SO"
|
||||
print u"Using Library AlfCrypto DLL/DYLIB/SO"
|
||||
return (AES_CBC, Pukall_Cipher, Topaz_Cipher)
|
||||
|
||||
|
||||
|
@ -164,8 +171,7 @@ def _load_python_alfcrypto():
|
|||
sum2 = 0;
|
||||
keyXorVal = 0;
|
||||
if len(key)!=16:
|
||||
print "Bad key length!"
|
||||
return None
|
||||
raise Exception('Pukall_Cipher: Bad key length.')
|
||||
wkey = []
|
||||
for i in xrange(8):
|
||||
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
|
||||
|
@ -234,6 +240,7 @@ def _load_python_alfcrypto():
|
|||
cleartext = self.aes.decrypt(iv + data)
|
||||
return cleartext
|
||||
|
||||
print u"Using Library AlfCrypto Python"
|
||||
return (AES_CBC, Pukall_Cipher, Topaz_Cipher)
|
||||
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from PyQt4.Qt import QWidget, QVBoxLayout, QLabel, QLineEdit
|
||||
|
||||
from calibre.utils.config import JSONConfig
|
||||
|
|
|
@ -230,6 +230,7 @@ class PageParser(object):
|
|||
'empty' : (1, 'snippets', 1, 0),
|
||||
|
||||
'page' : (1, 'snippets', 1, 0),
|
||||
'page.class' : (1, 'scalar_text', 0, 0),
|
||||
'page.pageid' : (1, 'scalar_text', 0, 0),
|
||||
'page.pagelabel' : (1, 'scalar_text', 0, 0),
|
||||
'page.type' : (1, 'scalar_text', 0, 0),
|
||||
|
@ -238,11 +239,13 @@ class PageParser(object):
|
|||
'page.startID' : (1, 'scalar_number', 0, 0),
|
||||
|
||||
'group' : (1, 'snippets', 1, 0),
|
||||
'group.class' : (1, 'scalar_text', 0, 0),
|
||||
'group.type' : (1, 'scalar_text', 0, 0),
|
||||
'group._tag' : (1, 'scalar_text', 0, 0),
|
||||
'group.orientation': (1, 'scalar_text', 0, 0),
|
||||
|
||||
'region' : (1, 'snippets', 1, 0),
|
||||
'region.class' : (1, 'scalar_text', 0, 0),
|
||||
'region.type' : (1, 'scalar_text', 0, 0),
|
||||
'region.x' : (1, 'scalar_number', 0, 0),
|
||||
'region.y' : (1, 'scalar_number', 0, 0),
|
||||
|
|
|
@ -35,7 +35,7 @@ def main(argv=sys.argv):
|
|||
except ValueError:
|
||||
print ' Error parsing user supplied social drm data.'
|
||||
return 1
|
||||
rv = erdr2pml.decryptBook(infile, outdir, name, cc8, True)
|
||||
rv = erdr2pml.decryptBook(infile, outdir, True, erdr2pml.getuser_key(name, cc8) )
|
||||
if rv == 0:
|
||||
break
|
||||
return rv
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# base64.py, version 1.0
|
||||
# Copyright © 2010 Apprentice Alf
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||
# later. <http://www.gnu.org/licenses/>
|
||||
|
||||
# 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 <infile>" % 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())
|
169
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/epubtest.py
Normal file
169
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/epubtest.py
Normal file
|
@ -0,0 +1,169 @@
|
|||
#!/usr/bin/python
|
||||
#
|
||||
# This is a python script. You need a Python interpreter to run it.
|
||||
# For example, ActiveState Python, which exists for windows.
|
||||
#
|
||||
# Changelog drmcheck
|
||||
# 1.00 - Initial version, with code from various other scripts
|
||||
# 1.01 - Moved authorship announcement to usage section.
|
||||
#
|
||||
# Changelog drmcheck
|
||||
# 1.00 - Cut to drmtest.py, testing ePub files only by Apprentice Alf
|
||||
#
|
||||
# Written in 2011 by Paul Durrant
|
||||
# Released with unlicense. See http://unlicense.org/
|
||||
#
|
||||
#############################################################################
|
||||
#
|
||||
# This is free and unencumbered software released into the public domain.
|
||||
#
|
||||
# Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
# distribute this software, either in source code form or as a compiled
|
||||
# binary, for any purpose, commercial or non-commercial, and by any
|
||||
# means.
|
||||
#
|
||||
# In jurisdictions that recognize copyright laws, the author or authors
|
||||
# of this software dedicate any and all copyright interest in the
|
||||
# software to the public domain. We make this dedication for the benefit
|
||||
# of the public at large and to the detriment of our heirs and
|
||||
# successors. We intend this dedication to be an overt act of
|
||||
# relinquishment in perpetuity of all present and future rights to this
|
||||
# software under copyright law.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
#############################################################################
|
||||
#
|
||||
# It's still polite to give attribution if you do reuse this code.
|
||||
#
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
__version__ = '1.00'
|
||||
|
||||
import sys, struct, os
|
||||
import zlib
|
||||
import zipfile
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
||||
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
def unicode_argv():
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
_FILENAME_LEN_OFFSET = 26
|
||||
_EXTRA_LEN_OFFSET = 28
|
||||
_FILENAME_OFFSET = 30
|
||||
_MAX_SIZE = 64 * 1024
|
||||
|
||||
|
||||
def uncompress(cmpdata):
|
||||
dc = zlib.decompressobj(-15)
|
||||
data = ''
|
||||
while len(cmpdata) > 0:
|
||||
if len(cmpdata) > _MAX_SIZE :
|
||||
newdata = cmpdata[0:_MAX_SIZE]
|
||||
cmpdata = cmpdata[_MAX_SIZE:]
|
||||
else:
|
||||
newdata = cmpdata
|
||||
cmpdata = ''
|
||||
newdata = dc.decompress(newdata)
|
||||
unprocessed = dc.unconsumed_tail
|
||||
if len(unprocessed) == 0:
|
||||
newdata += dc.flush()
|
||||
data += newdata
|
||||
cmpdata += unprocessed
|
||||
unprocessed = ''
|
||||
return data
|
||||
|
||||
def getfiledata(file, zi):
|
||||
# get file name length and exta data length to find start of file data
|
||||
local_header_offset = zi.header_offset
|
||||
|
||||
file.seek(local_header_offset + _FILENAME_LEN_OFFSET)
|
||||
leninfo = file.read(2)
|
||||
local_name_length, = struct.unpack('<H', leninfo)
|
||||
|
||||
file.seek(local_header_offset + _EXTRA_LEN_OFFSET)
|
||||
exinfo = file.read(2)
|
||||
extra_field_length, = struct.unpack('<H', exinfo)
|
||||
|
||||
file.seek(local_header_offset + _FILENAME_OFFSET + local_name_length + extra_field_length)
|
||||
data = None
|
||||
|
||||
# if not compressed we are good to go
|
||||
if zi.compress_type == zipfile.ZIP_STORED:
|
||||
data = file.read(zi.file_size)
|
||||
|
||||
# if compressed we must decompress it using zlib
|
||||
if zi.compress_type == zipfile.ZIP_DEFLATED:
|
||||
cmpdata = file.read(zi.compress_size)
|
||||
data = uncompress(cmpdata)
|
||||
|
||||
return data
|
||||
|
||||
def main(argv=unicode_argv()):
|
||||
infile = argv[1]
|
||||
kind = "Unknown"
|
||||
encryption = "Unknown"
|
||||
with file(infile,'rb') as infileobject:
|
||||
bookdata = infileobject.read(58)
|
||||
# Check for Mobipocket/Kindle
|
||||
if bookdata[0:0+2] == "PK":
|
||||
if bookdata[30:30+28] == 'mimetypeapplication/epub+zip':
|
||||
kind = "ePub"
|
||||
else:
|
||||
kind = "ZIP"
|
||||
encryption = "Unencrypted"
|
||||
foundrights = False
|
||||
foundencryption = False
|
||||
inzip = zipfile.ZipFile(infile,'r')
|
||||
namelist = set(inzip.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or 'META-INF/encryption.xml' not in namelist:
|
||||
encryption = "Unencrypted"
|
||||
else:
|
||||
rights = etree.fromstring(inzip.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:
|
||||
encryption = "Adobe"
|
||||
elif len(bookkey) == 64:
|
||||
encryption = "B&N"
|
||||
else:
|
||||
encryption = "Unknown"
|
||||
|
||||
print u"{0} {1}".format(encryption, kind)
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(main())
|
|
@ -1,8 +1,11 @@
|
|||
#!/usr/bin/env python
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# erdr2pml.py
|
||||
# Copyright © 2008 The Dark Reverser
|
||||
#
|
||||
# Modified 2008–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
|
||||
# This is a python script. You need a Python interpreter to run it.
|
||||
# For example, ActiveState Python, which exists for windows.
|
||||
# Changelog
|
||||
|
@ -62,19 +65,11 @@
|
|||
# 0.21 - Support eReader (drm) version 11.
|
||||
# - Don't reject dictionary format.
|
||||
# - Ignore sidebars for dictionaries (different format?)
|
||||
# 0.22 - Unicode and plugin support, different image folders for PMLZ and source
|
||||
|
||||
__version__='0.21'
|
||||
__version__='0.22'
|
||||
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
def write(self, data):
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
import sys, re
|
||||
import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
|
@ -82,8 +77,66 @@ if 'calibre' in sys.modules:
|
|||
else:
|
||||
inCalibre = False
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return [u"mobidedrm.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
Des = None
|
||||
if sys.platform.startswith('win'):
|
||||
if iswindows:
|
||||
# first try with pycrypto
|
||||
if inCalibre:
|
||||
from calibre_plugins.erdrpdb2pml import pycrypto_des
|
||||
|
@ -168,17 +221,30 @@ class Sectionizer(object):
|
|||
off = self.sections[section][0]
|
||||
return self.contents[off:end_off]
|
||||
|
||||
def sanitizeFileName(s):
|
||||
r = ''
|
||||
for c in s:
|
||||
if c in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-":
|
||||
r += c
|
||||
return r
|
||||
# cleanup unicode filenames
|
||||
# borrowed from calibre from calibre/src/calibre/__init__.py
|
||||
# added in removal of control (<32) chars
|
||||
# and removal of . at start and end
|
||||
# and with some (heavily edited) code from Paul Durrant's kindlenamer.py
|
||||
def sanitizeFileName(name):
|
||||
# substitute filename unfriendly characters
|
||||
name = name.replace(u"<",u"[").replace(u">",u"]").replace(u" : ",u" – ").replace(u": ",u" – ").replace(u":",u"—").replace(u"/",u"_").replace(u"\\",u"_").replace(u"|",u"_").replace(u"\"",u"\'")
|
||||
# delete control characters
|
||||
name = u"".join(char for char in name if ord(char)>=32)
|
||||
# white space to single space, delete leading and trailing while space
|
||||
name = re.sub(ur"\s", u" ", name).strip()
|
||||
# remove leading dots
|
||||
while len(name)>0 and name[0] == u".":
|
||||
name = name[1:]
|
||||
# remove trailing dots (Windows doesn't like them)
|
||||
if name.endswith(u'.'):
|
||||
name = name[:-1]
|
||||
return name
|
||||
|
||||
def fixKey(key):
|
||||
def fixByte(b):
|
||||
return b ^ ((b ^ (b<<1) ^ (b<<2) ^ (b<<3) ^ (b<<4) ^ (b<<5) ^ (b<<6) ^ (b<<7) ^ 0x80) & 0x80)
|
||||
return "".join([chr(fixByte(ord(a))) for a in key])
|
||||
return "".join([chr(fixByte(ord(a))) for a in key])
|
||||
|
||||
def deXOR(text, sp, table):
|
||||
r=''
|
||||
|
@ -191,7 +257,7 @@ def deXOR(text, sp, table):
|
|||
return r
|
||||
|
||||
class EreaderProcessor(object):
|
||||
def __init__(self, sect, username, creditcard):
|
||||
def __init__(self, sect, user_key):
|
||||
self.section_reader = sect.loadSection
|
||||
data = self.section_reader(0)
|
||||
version, = struct.unpack('>H', data[0:2])
|
||||
|
@ -212,18 +278,10 @@ class EreaderProcessor(object):
|
|||
for i in xrange(len(data)):
|
||||
j = (j + shuf) % len(data)
|
||||
r[j] = data[i]
|
||||
assert len("".join(r)) == len(data)
|
||||
assert len("".join(r)) == len(data)
|
||||
return "".join(r)
|
||||
r = unshuff(input[0:-8], cookie_shuf)
|
||||
|
||||
def fixUsername(s):
|
||||
r = ''
|
||||
for c in s.lower():
|
||||
if (c >= 'a' and c <= 'z' or c >= '0' and c <= '9'):
|
||||
r += c
|
||||
return r
|
||||
|
||||
user_key = struct.pack('>LL', binascii.crc32(fixUsername(username)) & 0xffffffff, binascii.crc32(creditcard[-8:])& 0xffffffff)
|
||||
drm_sub_version = struct.unpack('>H', r[0:2])[0]
|
||||
self.num_text_pages = struct.unpack('>H', r[2:4])[0] - 1
|
||||
self.num_image_pages = struct.unpack('>H', r[26:26+2])[0]
|
||||
|
@ -302,7 +360,7 @@ class EreaderProcessor(object):
|
|||
sect = self.section_reader(self.first_image_page + i)
|
||||
name = sect[4:4+32].strip('\0')
|
||||
data = sect[62:]
|
||||
return sanitizeFileName(name), data
|
||||
return sanitizeFileName(unicode(name,'windows-1252')), data
|
||||
|
||||
|
||||
# def getChapterNamePMLOffsetData(self):
|
||||
|
@ -399,60 +457,53 @@ class EreaderProcessor(object):
|
|||
return r
|
||||
|
||||
def cleanPML(pml):
|
||||
# Convert special characters to proper PML code. High ASCII start at (\x80, \a128) and go up to (\xff, \a255)
|
||||
# Convert special characters to proper PML code. High ASCII start at (\x80, \a128) and go up to (\xff, \a255)
|
||||
pml2 = pml
|
||||
for k in xrange(128,256):
|
||||
badChar = chr(k)
|
||||
pml2 = pml2.replace(badChar, '\\a%03d' % k)
|
||||
return pml2
|
||||
|
||||
def convertEreaderToPml(infile, name, cc, outdir):
|
||||
if not os.path.exists(outdir):
|
||||
os.makedirs(outdir)
|
||||
def decryptBook(infile, outpath, make_pmlz, user_key):
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
print " Decoding File"
|
||||
sect = Sectionizer(infile, 'PNRdPPrs')
|
||||
er = EreaderProcessor(sect, name, cc)
|
||||
|
||||
if er.getNumImages() > 0:
|
||||
print " Extracting images"
|
||||
imagedir = bookname + '_img/'
|
||||
imagedirpath = os.path.join(outdir,imagedir)
|
||||
if not os.path.exists(imagedirpath):
|
||||
os.makedirs(imagedirpath)
|
||||
for i in xrange(er.getNumImages()):
|
||||
name, contents = er.getImage(i)
|
||||
file(os.path.join(imagedirpath, name), 'wb').write(contents)
|
||||
|
||||
print " Extracting pml"
|
||||
pml_string = er.getText()
|
||||
pmlfilename = bookname + ".pml"
|
||||
file(os.path.join(outdir, pmlfilename),'wb').write(cleanPML(pml_string))
|
||||
|
||||
# bkinfo = er.getBookInfo()
|
||||
# if bkinfo != '':
|
||||
# print " Extracting book meta information"
|
||||
# file(os.path.join(outdir, 'bookinfo.txt'),'wb').write(bkinfo)
|
||||
|
||||
|
||||
|
||||
def decryptBook(infile, outdir, name, cc, make_pmlz):
|
||||
if make_pmlz :
|
||||
# ignore specified outdir, use tempdir instead
|
||||
if make_pmlz:
|
||||
# outpath is actually pmlz name
|
||||
pmlzname = outpath
|
||||
outdir = tempfile.mkdtemp()
|
||||
imagedirpath = os.path.join(outdir,u"images")
|
||||
else:
|
||||
pmlzname = None
|
||||
outdir = outpath
|
||||
imagedirpath = os.path.join(outdir,bookname + u"_img")
|
||||
|
||||
try:
|
||||
print "Processing..."
|
||||
convertEreaderToPml(infile, name, cc, outdir)
|
||||
if make_pmlz :
|
||||
if not os.path.exists(outdir):
|
||||
os.makedirs(outdir)
|
||||
print u"Decoding File"
|
||||
sect = Sectionizer(infile, 'PNRdPPrs')
|
||||
er = EreaderProcessor(sect, user_key)
|
||||
|
||||
if er.getNumImages() > 0:
|
||||
print u"Extracting images"
|
||||
if not os.path.exists(imagedirpath):
|
||||
os.makedirs(imagedirpath)
|
||||
for i in xrange(er.getNumImages()):
|
||||
name, contents = er.getImage(i)
|
||||
file(os.path.join(imagedirpath, name), 'wb').write(contents)
|
||||
|
||||
print u"Extracting pml"
|
||||
pml_string = er.getText()
|
||||
pmlfilename = bookname + ".pml"
|
||||
file(os.path.join(outdir, pmlfilename),'wb').write(cleanPML(pml_string))
|
||||
if pmlzname is not None:
|
||||
import zipfile
|
||||
import shutil
|
||||
print " Creating PMLZ file"
|
||||
zipname = infile[:-4] + '.pmlz'
|
||||
myZipFile = zipfile.ZipFile(zipname,'w',zipfile.ZIP_STORED, False)
|
||||
print u"Creating PMLZ file {0}".format(os.path.basename(pmlzname))
|
||||
myZipFile = zipfile.ZipFile(pmlzname,'w',zipfile.ZIP_STORED, False)
|
||||
list = os.listdir(outdir)
|
||||
for file in list:
|
||||
localname = file
|
||||
filePath = os.path.join(outdir,file)
|
||||
for filename in list:
|
||||
localname = filename
|
||||
filePath = os.path.join(outdir,filename)
|
||||
if os.path.isfile(filePath):
|
||||
myZipFile.write(filePath, localname)
|
||||
elif os.path.isdir(filePath):
|
||||
|
@ -466,36 +517,46 @@ def decryptBook(infile, outdir, name, cc, make_pmlz):
|
|||
myZipFile.close()
|
||||
# remove temporary directory
|
||||
shutil.rmtree(outdir, True)
|
||||
print 'output is %s' % zipname
|
||||
print u"Output is {0}".format(pmlzname)
|
||||
else :
|
||||
print 'output in %s' % outdir
|
||||
print u"Output is in {0}".format(outdir)
|
||||
print "done"
|
||||
except ValueError, e:
|
||||
print "Error: %s" % e
|
||||
print u"Error: {0}".format(e.args[0])
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def usage():
|
||||
print "Converts DRMed eReader books to PML Source"
|
||||
print "Usage:"
|
||||
print " erdr2pml [options] infile.pdb [outdir] \"your name\" credit_card_number "
|
||||
print " "
|
||||
print "Options: "
|
||||
print " -h prints this message"
|
||||
print " --make-pmlz create PMLZ instead of using output directory"
|
||||
print " "
|
||||
print "Note:"
|
||||
print " if ommitted, outdir defaults based on 'infile.pdb'"
|
||||
print " It's enough to enter the last 8 digits of the credit card number"
|
||||
print u"Converts DRMed eReader books to PML Source"
|
||||
print u"Usage:"
|
||||
print u" erdr2pml [options] infile.pdb [outpath] \"your name\" credit_card_number"
|
||||
print u" "
|
||||
print u"Options: "
|
||||
print u" -h prints this message"
|
||||
print u" -p create PMLZ instead of source folder"
|
||||
print u" --make-pmlz create PMLZ instead of source folder"
|
||||
print u" "
|
||||
print u"Note:"
|
||||
print u" if outpath is ommitted, creates source in 'infile_Source' folder"
|
||||
print u" if outpath is ommitted and pmlz option, creates PMLZ 'infile.pmlz'"
|
||||
print u" if source folder created, images are in infile_img folder"
|
||||
print u" if pmlz file created, images are in images folder"
|
||||
print u" It's enough to enter the last 8 digits of the credit card number"
|
||||
return
|
||||
|
||||
def getuser_key(name,cc):
|
||||
newname = "".join(c for c in name.lower() if c >= 'a' and c <= 'z' or c >= '0' and c <= '9')
|
||||
cc = cc.replace(" ","")
|
||||
return struct.pack('>LL', binascii.crc32(newname) & 0xffffffff,binascii.crc32(cc[-8:])& 0xffffffff)
|
||||
|
||||
def cli_main(argv=unicode_argv()):
|
||||
print u"eRdr2Pml v{0}. Copyright © 2009–2012 The Dark Reverser et al.".format(__version__)
|
||||
|
||||
def main(argv=None):
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], "h", ["make-pmlz"])
|
||||
opts, args = getopt.getopt(argv[1:], "hp", ["make-pmlz"])
|
||||
except getopt.GetoptError, err:
|
||||
print str(err)
|
||||
print err.args[0]
|
||||
usage()
|
||||
return 1
|
||||
make_pmlz = False
|
||||
|
@ -503,24 +564,31 @@ def main(argv=None):
|
|||
if o == "-h":
|
||||
usage()
|
||||
return 0
|
||||
elif o == "-p":
|
||||
make_pmlz = True
|
||||
elif o == "--make-pmlz":
|
||||
make_pmlz = True
|
||||
|
||||
print "eRdr2Pml v%s. Copyright (c) 2009 The Dark Reverser" % __version__
|
||||
|
||||
if len(args)!=3 and len(args)!=4:
|
||||
usage()
|
||||
return 1
|
||||
|
||||
if len(args)==3:
|
||||
infile, name, cc = args[0], args[1], args[2]
|
||||
outdir = infile[:-4] + '_Source'
|
||||
infile, name, cc = args
|
||||
if make_pmlz:
|
||||
outpath = os.path.splitext(infile)[0] + u".pmlz"
|
||||
else:
|
||||
outpath = os.path.splitext(infile)[0] + u"_Source"
|
||||
elif len(args)==4:
|
||||
infile, outdir, name, cc = args[0], args[1], args[2], args[3]
|
||||
infile, outpath, name, cc = args
|
||||
|
||||
return decryptBook(infile, outdir, name, cc, make_pmlz)
|
||||
print getuser_key(name,cc).encode('hex')
|
||||
|
||||
return decryptBook(infile, outpath, make_pmlz, getuser_key(name,cc))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
sys.exit(main())
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
|
||||
|
|
|
@ -1,13 +1,25 @@
|
|||
#! /usr/bin/python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ignobleepub.pyw, version 3.5
|
||||
# ignobleepub.pyw, version 3.6
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# To run this program install Python 2.6 from <http://www.python.org/download/>
|
||||
# and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
|
||||
# (make sure to install the version for Python 2.6). Save this script file as
|
||||
# ignobleepub.pyw and double-click on it to run it.
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# from <http://www.python.org/download/> and PyCrypto from
|
||||
# <http://www.voidspace.org.uk/python/modules.shtml#pycrypto> (make sure to
|
||||
# install the version for Python 2.6). Save this script file as
|
||||
# ineptepub.pyw and double-click on it to run it.
|
||||
#
|
||||
# Mac OS X users: Save this script file as ineptepub.pyw. You can run this
|
||||
# program from the command line (pythonw ineptepub.pyw) or by double-clicking
|
||||
# it when it has been associated with PythonLauncher.
|
||||
|
||||
# Revision history:
|
||||
# 1 - Initial release
|
||||
|
@ -18,21 +30,83 @@ from __future__ import with_statement
|
|||
# 3.3 - On Windows try PyCrypto first and OpenSSL next
|
||||
# 3.4 - Modify interace to allow use with import
|
||||
# 3.5 - Fix for potential problem with PyCrypto
|
||||
# 3.6 - Revised to allow use in calibre plugins to eliminate need for duplicate code
|
||||
|
||||
"""
|
||||
Decrypt Barnes & Noble encrypted ePub books.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "3.6"
|
||||
|
||||
import sys
|
||||
import os
|
||||
import traceback
|
||||
import zlib
|
||||
import zipfile
|
||||
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||
from contextlib import closing
|
||||
import xml.etree.ElementTree as etree
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
try:
|
||||
from calibre.constants import iswindows, isosx
|
||||
except:
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
return [u"ineptepub.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
class IGNOBLEError(Exception):
|
||||
pass
|
||||
|
@ -42,10 +116,11 @@ def _load_crypto_libcrypto():
|
|||
Structure, c_ulong, create_string_buffer, cast
|
||||
from ctypes.util import find_library
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
if iswindows:
|
||||
libcrypto = find_library('libeay32')
|
||||
else:
|
||||
libcrypto = find_library('crypto')
|
||||
|
||||
if libcrypto is None:
|
||||
raise IGNOBLEError('libcrypto not found')
|
||||
libcrypto = CDLL(libcrypto)
|
||||
|
@ -66,9 +141,6 @@ def _load_crypto_libcrypto():
|
|||
func.argtypes = argtypes
|
||||
return func
|
||||
|
||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
||||
c_int])
|
||||
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
|
||||
[c_char_p, c_int, AES_KEY_p])
|
||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||
|
@ -123,13 +195,6 @@ def _load_crypto():
|
|||
|
||||
AES = _load_crypto()
|
||||
|
||||
|
||||
|
||||
"""
|
||||
Decrypt Barnes & Noble ADEPT encrypted EPUB books.
|
||||
"""
|
||||
|
||||
|
||||
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
|
||||
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
||||
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
||||
|
@ -144,7 +209,6 @@ class ZipInfo(zipfile.ZipInfo):
|
|||
class Decryptor(object):
|
||||
def __init__(self, bookkey, encryption):
|
||||
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
|
||||
# self._aes = AES.new(bookkey, AES.MODE_CBC, '\x00'*16)
|
||||
self._aes = AES(bookkey)
|
||||
encryption = etree.fromstring(encryption)
|
||||
self._encrypted = encrypted = set()
|
||||
|
@ -152,8 +216,8 @@ class Decryptor(object):
|
|||
enc('CipherReference'))
|
||||
for elem in encryption.findall(expr):
|
||||
path = elem.get('URI', None)
|
||||
path = path.encode('utf-8')
|
||||
if path is not None:
|
||||
path = path.encode('utf-8')
|
||||
encrypted.add(path)
|
||||
|
||||
def decompress(self, bytes):
|
||||
|
@ -171,167 +235,186 @@ class Decryptor(object):
|
|||
data = self.decompress(data)
|
||||
return data
|
||||
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = Tkinter.Label(self, text='Select files for decryption')
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
Tkinter.Label(body, text='Key file').grid(row=0)
|
||||
self.keypath = Tkinter.Entry(body, width=30)
|
||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||
if os.path.exists('bnepubkey.b64'):
|
||||
self.keypath.insert(0, 'bnepubkey.b64')
|
||||
button = Tkinter.Button(body, text="...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
Tkinter.Label(body, text='Input file').grid(row=1)
|
||||
self.inpath = Tkinter.Entry(body, width=30)
|
||||
self.inpath.grid(row=1, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
Tkinter.Label(body, text='Output file').grid(row=2)
|
||||
self.outpath = Tkinter.Entry(body, width=30)
|
||||
self.outpath.grid(row=2, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_outpath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = Tkinter.Button(
|
||||
buttons, text="Decrypt", width=10, command=self.decrypt)
|
||||
botton.pack(side=Tkconstants.LEFT)
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
button = Tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quit)
|
||||
button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkFileDialog.askopenfilename(
|
||||
parent=None, title='Select B&N EPUB key file',
|
||||
defaultextension='.b64',
|
||||
filetypes=[('base64-encoded files', '.b64'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(keypath)
|
||||
self.keypath.delete(0, Tkconstants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
def get_inpath(self):
|
||||
inpath = tkFileDialog.askopenfilename(
|
||||
parent=None, title='Select B&N-encrypted EPUB file to decrypt',
|
||||
defaultextension='.epub', filetypes=[('EPUB files', '.epub'),
|
||||
('All files', '.*')])
|
||||
if inpath:
|
||||
inpath = os.path.normpath(inpath)
|
||||
self.inpath.delete(0, Tkconstants.END)
|
||||
self.inpath.insert(0, inpath)
|
||||
return
|
||||
|
||||
def get_outpath(self):
|
||||
outpath = tkFileDialog.asksaveasfilename(
|
||||
parent=None, title='Select unencrypted EPUB file to produce',
|
||||
defaultextension='.epub', filetypes=[('EPUB files', '.epub'),
|
||||
('All files', '.*')])
|
||||
if outpath:
|
||||
outpath = os.path.normpath(outpath)
|
||||
self.outpath.delete(0, Tkconstants.END)
|
||||
self.outpath.insert(0, outpath)
|
||||
return
|
||||
|
||||
def decrypt(self):
|
||||
keypath = self.keypath.get()
|
||||
inpath = self.inpath.get()
|
||||
outpath = self.outpath.get()
|
||||
if not keypath or not os.path.exists(keypath):
|
||||
self.status['text'] = 'Specified key file does not exist'
|
||||
return
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = 'Specified input file does not exist'
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = 'Output file not specified'
|
||||
return
|
||||
if inpath == outpath:
|
||||
self.status['text'] = 'Must have different input and output files'
|
||||
return
|
||||
argv = [sys.argv[0], keypath, inpath, outpath]
|
||||
self.status['text'] = 'Decrypting...'
|
||||
try:
|
||||
cli_main(argv)
|
||||
except Exception, e:
|
||||
self.status['text'] = 'Error: ' + str(e)
|
||||
return
|
||||
self.status['text'] = 'File successfully decrypted'
|
||||
|
||||
|
||||
def decryptBook(keypath, inpath, outpath):
|
||||
with open(keypath, 'rb') as f:
|
||||
keyb64 = f.read()
|
||||
key = keyb64.decode('base64')[:16]
|
||||
# aes = AES.new(key, AES.MODE_CBC, '\x00'*16)
|
||||
aes = AES(key)
|
||||
|
||||
# check file to make check whether it's probably an Adobe Adept encrypted ePub
|
||||
def ignobleBook(inpath):
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
raise IGNOBLEError('%s: not an B&N ADEPT EPUB' % (inpath,))
|
||||
return False
|
||||
try:
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) == 64:
|
||||
return True
|
||||
except:
|
||||
# if we couldn't check, assume it is
|
||||
return True
|
||||
return False
|
||||
|
||||
# return error code and error message duple
|
||||
def decryptBook(keyb64, inpath, outpath):
|
||||
if AES is None:
|
||||
# 1 means don't try again
|
||||
return (1, u"PyCrypto or OpenSSL must be installed.")
|
||||
key = keyb64.decode('base64')[:16]
|
||||
aes = AES(key)
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
return (1, u"Not a secure Barnes & Noble ePub.")
|
||||
for name in META_NAMES:
|
||||
namelist.remove(name)
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
bookkey = aes.decrypt(bookkey.decode('base64'))
|
||||
bookkey = bookkey[:-ord(bookkey[-1])]
|
||||
encryption = inf.read('META-INF/encryption.xml')
|
||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
||||
outf.writestr(zi, inf.read('mimetype'))
|
||||
for path in namelist:
|
||||
data = inf.read(path)
|
||||
outf.writestr(path, decryptor.decrypt(path, data))
|
||||
return 0
|
||||
try:
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) != 64:
|
||||
return (1, u"Not a secure Barnes & Noble ePub.")
|
||||
bookkey = aes.decrypt(bookkey.decode('base64'))
|
||||
bookkey = bookkey[:-ord(bookkey[-1])]
|
||||
encryption = inf.read('META-INF/encryption.xml')
|
||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
||||
outf.writestr(zi, inf.read('mimetype'))
|
||||
for path in namelist:
|
||||
data = inf.read(path)
|
||||
outf.writestr(path, decryptor.decrypt(path, data))
|
||||
except Exception, e:
|
||||
return (2, u"{0}.".format(e.args[0]))
|
||||
return (0, u"Success")
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
if AES is None:
|
||||
print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \
|
||||
"separately. Read the top-of-script comment for details." % \
|
||||
(progname,)
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,)
|
||||
print u"usage: {0} <keyfile.der> <inbook.epub> <outbook.epub>".format(progname)
|
||||
return 1
|
||||
keypath, inpath, outpath = argv[1:]
|
||||
return decryptBook(keypath, inpath, outpath)
|
||||
|
||||
userkey = open(keypath,'rb').read()
|
||||
result = decryptBook(userkey, inpath, outpath)
|
||||
print result[1]
|
||||
return result[0]
|
||||
|
||||
def gui_main():
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import traceback
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = Tkinter.Label(self, text=u"Select files for decryption")
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
Tkinter.Label(body, text=u"Key file").grid(row=0)
|
||||
self.keypath = Tkinter.Entry(body, width=30)
|
||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||
if os.path.exists(u"bnepubkey.b64"):
|
||||
self.keypath.insert(0, u"bnepubkey.b64")
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
Tkinter.Label(body, text=u"Input file").grid(row=1)
|
||||
self.inpath = Tkinter.Entry(body, width=30)
|
||||
self.inpath.grid(row=1, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
Tkinter.Label(body, text=u"Output file").grid(row=2)
|
||||
self.outpath = Tkinter.Entry(body, width=30)
|
||||
self.outpath.grid(row=2, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_outpath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = Tkinter.Button(
|
||||
buttons, text=u"Decrypt", width=10, command=self.decrypt)
|
||||
botton.pack(side=Tkconstants.LEFT)
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
button = Tkinter.Button(
|
||||
buttons, text=u"Quit", width=10, command=self.quit)
|
||||
button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkFileDialog.askopenfilename(
|
||||
parent=None, title=u"Select Barnes & Noble \'.b64\' key file",
|
||||
defaultextension=u".b64",
|
||||
filetypes=[('base64-encoded files', '.b64'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(keypath)
|
||||
self.keypath.delete(0, Tkconstants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
def get_inpath(self):
|
||||
inpath = tkFileDialog.askopenfilename(
|
||||
parent=None, title=u"Select B&N-encrypted ePub file to decrypt",
|
||||
defaultextension=u".epub", filetypes=[('ePub files', '.epub')])
|
||||
if inpath:
|
||||
inpath = os.path.normpath(inpath)
|
||||
self.inpath.delete(0, Tkconstants.END)
|
||||
self.inpath.insert(0, inpath)
|
||||
return
|
||||
|
||||
def get_outpath(self):
|
||||
outpath = tkFileDialog.asksaveasfilename(
|
||||
parent=None, title=u"Select unencrypted ePub file to produce",
|
||||
defaultextension=u".epub", filetypes=[('ePub files', '.epub')])
|
||||
if outpath:
|
||||
outpath = os.path.normpath(outpath)
|
||||
self.outpath.delete(0, Tkconstants.END)
|
||||
self.outpath.insert(0, outpath)
|
||||
return
|
||||
|
||||
def decrypt(self):
|
||||
keypath = self.keypath.get()
|
||||
inpath = self.inpath.get()
|
||||
outpath = self.outpath.get()
|
||||
if not keypath or not os.path.exists(keypath):
|
||||
self.status['text'] = u"Specified key file does not exist"
|
||||
return
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = u"Specified input file does not exist"
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = u"Output file not specified"
|
||||
return
|
||||
if inpath == outpath:
|
||||
self.status['text'] = u"Must have different input and output files"
|
||||
return
|
||||
userkey = open(keypath,'rb').read()
|
||||
self.status['text'] = u"Decrypting..."
|
||||
try:
|
||||
decrypt_status = decryptBook(userkey, inpath, outpath)
|
||||
except Exception, e:
|
||||
self.status['text'] = u"Error: {0}".format(e.args[0])
|
||||
return
|
||||
if decrypt_status[0] == 0:
|
||||
self.status['text'] = u"File successfully decrypted"
|
||||
else:
|
||||
self.status['text'] = decrypt_status[1]
|
||||
|
||||
root = Tkinter.Tk()
|
||||
if AES is None:
|
||||
root.withdraw()
|
||||
tkMessageBox.showerror(
|
||||
"Ignoble EPUB Decrypter",
|
||||
"This script requires OpenSSL or PyCrypto, which must be installed "
|
||||
"separately. Read the top-of-script comment for details.")
|
||||
return 1
|
||||
root.title('Ignoble EPUB Decrypter')
|
||||
root.title(u"Barnes & Noble ePub Decrypter v.{0}".format(__version__))
|
||||
root.resizable(True, False)
|
||||
root.minsize(300, 0)
|
||||
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
||||
|
|
|
@ -1,13 +1,25 @@
|
|||
#! /usr/bin/python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ignoblekeygen.pyw, version 2.4
|
||||
# ignoblekeygen.pyw, version 2.5
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# To run this program install Python 2.6 from <http://www.python.org/download/>
|
||||
# and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
|
||||
# (make sure to install the version for Python 2.6). Save this script file as
|
||||
# ignoblekeygen.pyw and double-click on it to run it.
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# from <http://www.python.org/download/> and PyCrypto from
|
||||
# <http://www.voidspace.org.uk/python/modules.shtml#pycrypto> (make sure to
|
||||
# install the version for Python 2.6). Save this script file as
|
||||
# ignoblekeygen.pyw and double-click on it to run it.
|
||||
#
|
||||
# Mac OS X users: Save this script file as ignoblekeygen.pyw. You can run this
|
||||
# program from the command line (pythonw ignoblekeygen.pyw) or by double-clicking
|
||||
# it when it has been associated with PythonLauncher.
|
||||
|
||||
# Revision history:
|
||||
# 1 - Initial release
|
||||
|
@ -16,36 +28,92 @@ from __future__ import with_statement
|
|||
# 2.2 - On Windows try PyCrypto first and then OpenSSL next
|
||||
# 2.3 - Modify interface to allow use of import
|
||||
# 2.4 - Improvements to UI and now works in plugins
|
||||
# 2.5 - Additional improvement for unicode and plugin support
|
||||
|
||||
"""
|
||||
Generate Barnes & Noble EPUB user key from name and credit card number.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "2.5"
|
||||
|
||||
import sys
|
||||
import os
|
||||
import hashlib
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return [u"ignoblekeygen.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
# use openssl's libcrypt if it exists in place of pycrypto
|
||||
# code extracted from the Adobe Adept DRM removal code also by I HeartCabbages
|
||||
class IGNOBLEError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _load_crypto_libcrypto():
|
||||
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
||||
Structure, c_ulong, create_string_buffer, cast
|
||||
from ctypes.util import find_library
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
if iswindows:
|
||||
libcrypto = find_library('libeay32')
|
||||
else:
|
||||
libcrypto = find_library('crypto')
|
||||
|
||||
if libcrypto is None:
|
||||
print 'libcrypto not found'
|
||||
raise IGNOBLEError('libcrypto not found')
|
||||
libcrypto = CDLL(libcrypto)
|
||||
|
||||
|
@ -70,6 +138,7 @@ def _load_crypto_libcrypto():
|
|||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
||||
c_int])
|
||||
|
||||
class AES(object):
|
||||
def __init__(self, userkey, iv):
|
||||
self._blocksize = len(userkey)
|
||||
|
@ -88,7 +157,6 @@ def _load_crypto_libcrypto():
|
|||
|
||||
return AES
|
||||
|
||||
|
||||
def _load_crypto_pycrypto():
|
||||
from Crypto.Cipher import AES as _AES
|
||||
|
||||
|
@ -120,25 +188,28 @@ def normalize_name(name):
|
|||
return ''.join(x for x in name.lower() if x != ' ')
|
||||
|
||||
|
||||
def generate_keyfile(name, ccn, outpath):
|
||||
def generate_key(name, ccn):
|
||||
# remove spaces and case from name and CC numbers.
|
||||
if type(name)==unicode:
|
||||
name = name.encode('utf-8')
|
||||
if type(ccn)==unicode:
|
||||
ccn = ccn.encode('utf-8')
|
||||
|
||||
name = normalize_name(name) + '\x00'
|
||||
ccn = normalize_name(ccn) + '\x00'
|
||||
|
||||
|
||||
name_sha = hashlib.sha1(name).digest()[:16]
|
||||
ccn_sha = hashlib.sha1(ccn).digest()[:16]
|
||||
both_sha = hashlib.sha1(name + ccn).digest()
|
||||
aes = AES(ccn_sha, name_sha)
|
||||
crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c))
|
||||
userkey = hashlib.sha1(crypt).digest()
|
||||
with open(outpath, 'wb') as f:
|
||||
f.write(userkey.encode('base64'))
|
||||
return userkey
|
||||
return userkey.encode('base64')
|
||||
|
||||
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
if AES is None:
|
||||
print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \
|
||||
|
@ -146,10 +217,11 @@ def cli_main(argv=sys.argv):
|
|||
(progname,)
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print "usage: %s NAME CC# OUTFILE" % (progname,)
|
||||
print u"usage: {0} <Name> <CC#> <keyfileout.b64>".format(progname)
|
||||
return 1
|
||||
name, ccn, outpath = argv[1:]
|
||||
generate_keyfile(name, ccn, outpath)
|
||||
name, ccn, keypath = argv[1:]
|
||||
userkey = generate_key(name, ccn)
|
||||
open(keypath,'wb').write(userkey)
|
||||
return 0
|
||||
|
||||
|
||||
|
@ -162,38 +234,38 @@ def gui_main():
|
|||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = Tkinter.Label(self, text='Enter parameters')
|
||||
self.status = Tkinter.Label(self, text=u"Enter parameters")
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
Tkinter.Label(body, text='Account Name').grid(row=0)
|
||||
Tkinter.Label(body, text=u"Account Name").grid(row=0)
|
||||
self.name = Tkinter.Entry(body, width=40)
|
||||
self.name.grid(row=0, column=1, sticky=sticky)
|
||||
Tkinter.Label(body, text='CC#').grid(row=1)
|
||||
Tkinter.Label(body, text=u"CC#").grid(row=1)
|
||||
self.ccn = Tkinter.Entry(body, width=40)
|
||||
self.ccn.grid(row=1, column=1, sticky=sticky)
|
||||
Tkinter.Label(body, text='Output file').grid(row=2)
|
||||
Tkinter.Label(body, text=u"Output file").grid(row=2)
|
||||
self.keypath = Tkinter.Entry(body, width=40)
|
||||
self.keypath.grid(row=2, column=1, sticky=sticky)
|
||||
self.keypath.insert(2, 'bnepubkey.b64')
|
||||
button = Tkinter.Button(body, text="...", command=self.get_keypath)
|
||||
self.keypath.insert(2, u"bnepubkey.b64")
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = Tkinter.Button(
|
||||
buttons, text="Generate", width=10, command=self.generate)
|
||||
buttons, text=u"Generate", width=10, command=self.generate)
|
||||
botton.pack(side=Tkconstants.LEFT)
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
button = Tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quit)
|
||||
buttons, text=u"Quit", width=10, command=self.quit)
|
||||
button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkFileDialog.asksaveasfilename(
|
||||
parent=None, title='Select B&N EPUB key file to produce',
|
||||
defaultextension='.b64',
|
||||
parent=None, title=u"Select B&N ePub key file to produce",
|
||||
defaultextension=u".b64",
|
||||
filetypes=[('base64-encoded files', '.b64'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
|
@ -201,27 +273,28 @@ def gui_main():
|
|||
self.keypath.delete(0, Tkconstants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
|
||||
def generate(self):
|
||||
name = self.name.get()
|
||||
ccn = self.ccn.get()
|
||||
keypath = self.keypath.get()
|
||||
if not name:
|
||||
self.status['text'] = 'Name not specified'
|
||||
self.status['text'] = u"Name not specified"
|
||||
return
|
||||
if not ccn:
|
||||
self.status['text'] = 'Credit card number not specified'
|
||||
self.status['text'] = u"Credit card number not specified"
|
||||
return
|
||||
if not keypath:
|
||||
self.status['text'] = 'Output keyfile path not specified'
|
||||
self.status['text'] = u"Output keyfile path not specified"
|
||||
return
|
||||
self.status['text'] = 'Generating...'
|
||||
self.status['text'] = u"Generating..."
|
||||
try:
|
||||
generate_keyfile(name, ccn, keypath)
|
||||
userkey = generate_key(name, ccn)
|
||||
except Exception, e:
|
||||
self.status['text'] = 'Error: ' + str(e)
|
||||
self.status['text'] = u"Error: (0}".format(e.args[0])
|
||||
return
|
||||
self.status['text'] = 'Keyfile successfully generated'
|
||||
open(keypath,'wb').write(userkey)
|
||||
self.status['text'] = u"Keyfile successfully generated"
|
||||
|
||||
root = Tkinter.Tk()
|
||||
if AES is None:
|
||||
|
@ -231,7 +304,7 @@ def gui_main():
|
|||
"This script requires OpenSSL or PyCrypto, which must be installed "
|
||||
"separately. Read the top-of-script comment for details.")
|
||||
return 1
|
||||
root.title('Ignoble EPUB Keyfile Generator')
|
||||
root.title(u"Barnes & Noble ePub Keyfile Generator v.{0}".format(__version__))
|
||||
root.resizable(True, False)
|
||||
root.minsize(300, 0)
|
||||
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||
|
@ -240,5 +313,7 @@ def gui_main():
|
|||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
||||
|
|
|
@ -3,11 +3,13 @@
|
|||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ineptepub.pyw, version 5.6
|
||||
# Copyright © 2009-2010 i♥cabbages
|
||||
# ineptepub.pyw, version 5.8
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||
# later. <http://www.gnu.org/licenses/>
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# from <http://www.python.org/download/> and PyCrypto from
|
||||
|
@ -31,24 +33,83 @@ from __future__ import with_statement
|
|||
# 5.5 - On Windows try PyCrypto first, OpenSSL next
|
||||
# 5.6 - Modify interface to allow use with import
|
||||
# 5.7 - Fix for potential problem with PyCrypto
|
||||
# 5.8 - Revised to allow use in calibre plugins to eliminate need for duplicate code
|
||||
|
||||
"""
|
||||
Decrypt Adobe ADEPT-encrypted EPUB books.
|
||||
Decrypt Adobe Digital Editions encrypted ePub books.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "5.8"
|
||||
|
||||
import sys
|
||||
import os
|
||||
import traceback
|
||||
import zlib
|
||||
import zipfile
|
||||
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||
from contextlib import closing
|
||||
import xml.etree.ElementTree as etree
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
try:
|
||||
from calibre.constants import iswindows, isosx
|
||||
except:
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
return [u"ineptepub.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
class ADEPTError(Exception):
|
||||
pass
|
||||
|
@ -58,7 +119,7 @@ def _load_crypto_libcrypto():
|
|||
Structure, c_ulong, create_string_buffer, cast
|
||||
from ctypes.util import find_library
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
if iswindows:
|
||||
libcrypto = find_library('libeay32')
|
||||
else:
|
||||
libcrypto = find_library('crypto')
|
||||
|
@ -272,6 +333,7 @@ def _load_crypto():
|
|||
except (ImportError, ADEPTError):
|
||||
pass
|
||||
return (AES, RSA)
|
||||
|
||||
AES, RSA = _load_crypto()
|
||||
|
||||
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
|
||||
|
@ -314,158 +376,181 @@ class Decryptor(object):
|
|||
data = self.decompress(data)
|
||||
return data
|
||||
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = Tkinter.Label(self, text='Select files for decryption')
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
Tkinter.Label(body, text='Key file').grid(row=0)
|
||||
self.keypath = Tkinter.Entry(body, width=30)
|
||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||
if os.path.exists('adeptkey.der'):
|
||||
self.keypath.insert(0, 'adeptkey.der')
|
||||
button = Tkinter.Button(body, text="...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
Tkinter.Label(body, text='Input file').grid(row=1)
|
||||
self.inpath = Tkinter.Entry(body, width=30)
|
||||
self.inpath.grid(row=1, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
Tkinter.Label(body, text='Output file').grid(row=2)
|
||||
self.outpath = Tkinter.Entry(body, width=30)
|
||||
self.outpath.grid(row=2, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_outpath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = Tkinter.Button(
|
||||
buttons, text="Decrypt", width=10, command=self.decrypt)
|
||||
botton.pack(side=Tkconstants.LEFT)
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
button = Tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quit)
|
||||
button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkFileDialog.askopenfilename(
|
||||
parent=None, title='Select ADEPT key file',
|
||||
defaultextension='.der', filetypes=[('DER-encoded files', '.der'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(keypath)
|
||||
self.keypath.delete(0, Tkconstants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
def get_inpath(self):
|
||||
inpath = tkFileDialog.askopenfilename(
|
||||
parent=None, title='Select ADEPT-encrypted EPUB file to decrypt',
|
||||
defaultextension='.epub', filetypes=[('EPUB files', '.epub'),
|
||||
('All files', '.*')])
|
||||
if inpath:
|
||||
inpath = os.path.normpath(inpath)
|
||||
self.inpath.delete(0, Tkconstants.END)
|
||||
self.inpath.insert(0, inpath)
|
||||
return
|
||||
|
||||
def get_outpath(self):
|
||||
outpath = tkFileDialog.asksaveasfilename(
|
||||
parent=None, title='Select unencrypted EPUB file to produce',
|
||||
defaultextension='.epub', filetypes=[('EPUB files', '.epub'),
|
||||
('All files', '.*')])
|
||||
if outpath:
|
||||
outpath = os.path.normpath(outpath)
|
||||
self.outpath.delete(0, Tkconstants.END)
|
||||
self.outpath.insert(0, outpath)
|
||||
return
|
||||
|
||||
def decrypt(self):
|
||||
keypath = self.keypath.get()
|
||||
inpath = self.inpath.get()
|
||||
outpath = self.outpath.get()
|
||||
if not keypath or not os.path.exists(keypath):
|
||||
self.status['text'] = 'Specified key file does not exist'
|
||||
return
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = 'Specified input file does not exist'
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = 'Output file not specified'
|
||||
return
|
||||
if inpath == outpath:
|
||||
self.status['text'] = 'Must have different input and output files'
|
||||
return
|
||||
argv = [sys.argv[0], keypath, inpath, outpath]
|
||||
self.status['text'] = 'Decrypting...'
|
||||
try:
|
||||
cli_main(argv)
|
||||
except Exception, e:
|
||||
self.status['text'] = 'Error: ' + str(e)
|
||||
return
|
||||
self.status['text'] = 'File successfully decrypted'
|
||||
|
||||
|
||||
def decryptBook(keypath, inpath, outpath):
|
||||
with open(keypath, 'rb') as f:
|
||||
keyder = f.read()
|
||||
rsa = RSA(keyder)
|
||||
# check file to make check whether it's probably an Adobe Adept encrypted ePub
|
||||
def adeptBook(inpath):
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
raise ADEPTError('%s: not an ADEPT EPUB' % (inpath,))
|
||||
return False
|
||||
try:
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) == 172:
|
||||
return True
|
||||
except:
|
||||
# if we couldn't check, assume it is
|
||||
return True
|
||||
return False
|
||||
|
||||
def decryptBook(userkey, inpath, outpath):
|
||||
if AES is None:
|
||||
raise ADEPTError(u"PyCrypto or OpenSSL must be installed.")
|
||||
rsa = RSA(userkey)
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
print u"{0:s} is DRM-free.".format(os.path.basename(inpath))
|
||||
return 1
|
||||
for name in META_NAMES:
|
||||
namelist.remove(name)
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
bookkey = rsa.decrypt(bookkey.decode('base64'))
|
||||
# Padded as per RSAES-PKCS1-v1_5
|
||||
if bookkey[-17] != '\x00':
|
||||
raise ADEPTError('problem decrypting session key')
|
||||
encryption = inf.read('META-INF/encryption.xml')
|
||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
||||
outf.writestr(zi, inf.read('mimetype'))
|
||||
for path in namelist:
|
||||
data = inf.read(path)
|
||||
outf.writestr(path, decryptor.decrypt(path, data))
|
||||
try:
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) != 172:
|
||||
print u"{0:s} is not a secure Adobe Adept ePub.".format(os.path.basename(inpath))
|
||||
return 1
|
||||
bookkey = rsa.decrypt(bookkey.decode('base64'))
|
||||
# Padded as per RSAES-PKCS1-v1_5
|
||||
if bookkey[-17] != '\x00':
|
||||
print u"Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath))
|
||||
return 2
|
||||
encryption = inf.read('META-INF/encryption.xml')
|
||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
||||
outf.writestr(zi, inf.read('mimetype'))
|
||||
for path in namelist:
|
||||
data = inf.read(path)
|
||||
outf.writestr(path, decryptor.decrypt(path, data))
|
||||
except:
|
||||
print u"Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc())
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
if AES is None:
|
||||
print "%s: This script requires OpenSSL or PyCrypto, which must be" \
|
||||
" installed separately. Read the top-of-script comment for" \
|
||||
" details." % (progname,)
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,)
|
||||
print u"usage: {0} <keyfile.der> <inbook.epub> <outbook.epub>".format(progname)
|
||||
return 1
|
||||
keypath, inpath, outpath = argv[1:]
|
||||
return decryptBook(keypath, inpath, outpath)
|
||||
|
||||
userkey = open(keypath,'rb').read()
|
||||
result = decryptBook(userkey, inpath, outpath)
|
||||
if result == 0:
|
||||
print u"Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath))
|
||||
return result
|
||||
|
||||
def gui_main():
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import traceback
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = Tkinter.Label(self, text=u"Select files for decryption")
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
Tkinter.Label(body, text=u"Key file").grid(row=0)
|
||||
self.keypath = Tkinter.Entry(body, width=30)
|
||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||
if os.path.exists(u"adeptkey.der"):
|
||||
self.keypath.insert(0, u"adeptkey.der")
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
Tkinter.Label(body, text=u"Input file").grid(row=1)
|
||||
self.inpath = Tkinter.Entry(body, width=30)
|
||||
self.inpath.grid(row=1, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
Tkinter.Label(body, text=u"Output file").grid(row=2)
|
||||
self.outpath = Tkinter.Entry(body, width=30)
|
||||
self.outpath.grid(row=2, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_outpath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = Tkinter.Button(
|
||||
buttons, text=u"Decrypt", width=10, command=self.decrypt)
|
||||
botton.pack(side=Tkconstants.LEFT)
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
button = Tkinter.Button(
|
||||
buttons, text=u"Quit", width=10, command=self.quit)
|
||||
button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkFileDialog.askopenfilename(
|
||||
parent=None, title=u"Select Adobe Adept \'.der\' key file",
|
||||
defaultextension=u".der",
|
||||
filetypes=[('Adobe Adept DER-encoded files', '.der'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(keypath)
|
||||
self.keypath.delete(0, Tkconstants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
def get_inpath(self):
|
||||
inpath = tkFileDialog.askopenfilename(
|
||||
parent=None, title=u"Select ADEPT-encrypted ePub file to decrypt",
|
||||
defaultextension=u".epub", filetypes=[('ePub files', '.epub')])
|
||||
if inpath:
|
||||
inpath = os.path.normpath(inpath)
|
||||
self.inpath.delete(0, Tkconstants.END)
|
||||
self.inpath.insert(0, inpath)
|
||||
return
|
||||
|
||||
def get_outpath(self):
|
||||
outpath = tkFileDialog.asksaveasfilename(
|
||||
parent=None, title=u"Select unencrypted ePub file to produce",
|
||||
defaultextension=u".epub", filetypes=[('ePub files', '.epub')])
|
||||
if outpath:
|
||||
outpath = os.path.normpath(outpath)
|
||||
self.outpath.delete(0, Tkconstants.END)
|
||||
self.outpath.insert(0, outpath)
|
||||
return
|
||||
|
||||
def decrypt(self):
|
||||
keypath = self.keypath.get()
|
||||
inpath = self.inpath.get()
|
||||
outpath = self.outpath.get()
|
||||
if not keypath or not os.path.exists(keypath):
|
||||
self.status['text'] = u"Specified key file does not exist"
|
||||
return
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = u"Specified input file does not exist"
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = u"Output file not specified"
|
||||
return
|
||||
if inpath == outpath:
|
||||
self.status['text'] = u"Must have different input and output files"
|
||||
return
|
||||
userkey = open(keypath,'rb').read()
|
||||
self.status['text'] = u"Decrypting..."
|
||||
try:
|
||||
decrypt_status = decryptBook(userkey, inpath, outpath)
|
||||
except Exception, e:
|
||||
self.status['text'] = u"Error; {0}".format(e)
|
||||
return
|
||||
if decrypt_status == 0:
|
||||
self.status['text'] = u"File successfully decrypted"
|
||||
else:
|
||||
self.status['text'] = u"The was an error decrypting the file."
|
||||
|
||||
root = Tkinter.Tk()
|
||||
if AES is None:
|
||||
root.withdraw()
|
||||
tkMessageBox.showerror(
|
||||
"INEPT EPUB Decrypter",
|
||||
"This script requires OpenSSL or PyCrypto, which must be"
|
||||
" installed separately. Read the top-of-script comment for"
|
||||
" details.")
|
||||
return 1
|
||||
root.title('INEPT EPUB Decrypter')
|
||||
root.title(u"Adobe Adept ePub Decrypter v.{0}".format(__version__))
|
||||
root.resizable(True, False)
|
||||
root.minsize(300, 0)
|
||||
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||
|
@ -474,5 +559,7 @@ def gui_main():
|
|||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
||||
|
|
|
@ -6,8 +6,8 @@ from __future__ import with_statement
|
|||
# ineptkey.pyw, version 5.6
|
||||
# Copyright © 2009-2010 i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||
# later. <http://www.gnu.org/licenses/>
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# from <http://www.python.org/download/> and PyCrypto from
|
||||
|
@ -37,7 +37,7 @@ from __future__ import with_statement
|
|||
# 5.3 - On Windows try PyCrypto first, OpenSSL next
|
||||
# 5.4 - Modify interface to allow use of import
|
||||
# 5.5 - Fix for potential problem with PyCrypto
|
||||
# 5.6 - Revise to allow use in Plugins to eliminate need for duplicate code
|
||||
# 5.6 - Revised to allow use in Plugins to eliminate need for duplicate code
|
||||
|
||||
"""
|
||||
Retrieve Adobe ADEPT user key.
|
||||
|
@ -49,12 +49,65 @@ import sys
|
|||
import os
|
||||
import struct
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
try:
|
||||
from calibre.constants import iswindows, isosx
|
||||
except:
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
return [u"ineptkey.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
class ADEPTError(Exception):
|
||||
pass
|
||||
|
||||
|
@ -80,13 +133,13 @@ if iswindows:
|
|||
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
|
||||
('rounds', c_int)]
|
||||
AES_KEY_p = POINTER(AES_KEY)
|
||||
|
||||
|
||||
def F(restype, name, argtypes):
|
||||
func = getattr(libcrypto, name)
|
||||
func.restype = restype
|
||||
func.argtypes = argtypes
|
||||
return func
|
||||
|
||||
|
||||
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
|
||||
[c_char_p, c_int, AES_KEY_p])
|
||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||
|
@ -308,9 +361,9 @@ if iswindows:
|
|||
cuser = winreg.HKEY_CURRENT_USER
|
||||
try:
|
||||
regkey = winreg.OpenKey(cuser, DEVICE_KEY_PATH)
|
||||
device = winreg.QueryValueEx(regkey, 'key')[0]
|
||||
except WindowsError:
|
||||
raise ADEPTError("Adobe Digital Editions not activated")
|
||||
device = winreg.QueryValueEx(regkey, 'key')[0]
|
||||
keykey = CryptUnprotectData(device, entropy)
|
||||
userkey = None
|
||||
keys = []
|
||||
|
@ -343,7 +396,7 @@ if iswindows:
|
|||
if len(keys) == 0:
|
||||
raise ADEPTError('Could not locate privateLicenseKey')
|
||||
return keys
|
||||
|
||||
|
||||
|
||||
elif isosx:
|
||||
import xml.etree.ElementTree as etree
|
||||
|
@ -386,7 +439,7 @@ else:
|
|||
def retrieve_keys(keypath):
|
||||
raise ADEPTError("This script only supports Windows and Mac OS X.")
|
||||
return []
|
||||
|
||||
|
||||
def retrieve_key(keypath):
|
||||
keys = retrieve_keys()
|
||||
with open(keypath, 'wb') as f:
|
||||
|
@ -397,22 +450,22 @@ def extractKeyfile(keypath):
|
|||
try:
|
||||
success = retrieve_key(keypath)
|
||||
except ADEPTError, e:
|
||||
print "Key generation Error: " + str(e)
|
||||
print u"Key generation Error: {0}".format(e.args[0])
|
||||
return 1
|
||||
except Exception, e:
|
||||
print "General Error: " + str(e)
|
||||
print "General Error: {0}".format(e.args[0])
|
||||
return 1
|
||||
if not success:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
def cli_main(argv=unicode_argv()):
|
||||
keypath = argv[1]
|
||||
return extractKeyfile(keypath)
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
def gui_main(argv=unicode_argv()):
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkMessageBox
|
||||
|
@ -421,24 +474,24 @@ def main(argv=sys.argv):
|
|||
class ExceptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root, text):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
label = Tkinter.Label(self, text="Unexpected error:",
|
||||
label = Tkinter.Label(self, text=u"Unexpected error:",
|
||||
anchor=Tkconstants.W, justify=Tkconstants.LEFT)
|
||||
label.pack(fill=Tkconstants.X, expand=0)
|
||||
self.text = Tkinter.Text(self)
|
||||
self.text.pack(fill=Tkconstants.BOTH, expand=1)
|
||||
|
||||
|
||||
self.text.insert(Tkconstants.END, text)
|
||||
|
||||
|
||||
root = Tkinter.Tk()
|
||||
root.withdraw()
|
||||
progname = os.path.basename(argv[0])
|
||||
keypath = os.path.abspath("adeptkey.der")
|
||||
keypath, progname = os.path.split(argv[0])
|
||||
keypath = os.path.join(keypath, u"adeptkey.der")
|
||||
success = False
|
||||
try:
|
||||
success = retrieve_key(keypath)
|
||||
except ADEPTError, e:
|
||||
tkMessageBox.showerror("ADEPT Key", "Error: " + str(e))
|
||||
tkMessageBox.showerror(u"ADEPT Key", "Error: {0}".format(e.args[0]))
|
||||
except Exception:
|
||||
root.wm_state('normal')
|
||||
root.title('ADEPT Key')
|
||||
|
@ -448,10 +501,12 @@ def main(argv=sys.argv):
|
|||
if not success:
|
||||
return 1
|
||||
tkMessageBox.showinfo(
|
||||
"ADEPT Key", "Key successfully retrieved to %s" % (keypath))
|
||||
u"ADEPT Key", u"Key successfully retrieved to {0}".format(keypath))
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
sys.exit(main())
|
||||
sys.exit(gui_main())
|
||||
|
|
|
@ -1,13 +1,25 @@
|
|||
#! /usr/bin/env python
|
||||
# ineptpdf.pyw, version 7.11
|
||||
#! /usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# To run this program install Python 2.6 from http://www.python.org/download/
|
||||
# and OpenSSL (already installed on Mac OS X and Linux) OR
|
||||
# PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
|
||||
# (make sure to install the version for Python 2.6). Save this script file as
|
||||
# ineptpdf.pyw and double-click on it to run it.
|
||||
# ineptpdf.pyw, version 7.11
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# from <http://www.python.org/download/> and PyCrypto from
|
||||
# <http://www.voidspace.org.uk/python/modules.shtml#pycrypto> (make sure to
|
||||
# install the version for Python 2.6). Save this script file as
|
||||
# ineptepub.pyw and double-click on it to run it.
|
||||
#
|
||||
# Mac OS X users: Save this script file as ineptepub.pyw. You can run this
|
||||
# program from the command line (pythonw ineptepub.pyw) or by double-clicking
|
||||
# it when it has been associated with PythonLauncher.
|
||||
|
||||
# Revision history:
|
||||
# 1 - Initial release
|
||||
|
@ -36,12 +48,14 @@ from __future__ import with_statement
|
|||
# 7.9 - Bug fix for some session key errors when len(bookkey) > length required
|
||||
# 7.10 - Various tweaks to fix minor problems.
|
||||
# 7.11 - More tweaks to fix minor problems.
|
||||
# 7.12 - Revised to allow use in calibre plugins to eliminate need for duplicate code
|
||||
|
||||
"""
|
||||
Decrypts Adobe ADEPT-encrypted PDF files.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "7.12"
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
@ -51,10 +65,63 @@ import struct
|
|||
import hashlib
|
||||
from itertools import chain, islice
|
||||
import xml.etree.ElementTree as etree
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
return [u"ineptepub.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
class ADEPTError(Exception):
|
||||
pass
|
||||
|
@ -1520,9 +1587,7 @@ class PDFDocument(object):
|
|||
|
||||
def initialize_ebx(self, password, docid, param):
|
||||
self.is_printable = self.is_modifiable = self.is_extractable = True
|
||||
with open(password, 'rb') as f:
|
||||
keyder = f.read()
|
||||
rsa = RSA(keyder)
|
||||
rsa = RSA(password)
|
||||
length = int_value(param.get('Length', 0)) / 8
|
||||
rights = str_value(param.get('ADEPT_LICENSE')).decode('base64')
|
||||
rights = zlib.decompress(rights, -15)
|
||||
|
@ -1907,14 +1972,14 @@ class PDFObjStrmParser(PDFParser):
|
|||
### My own code, for which there is none else to blame
|
||||
|
||||
class PDFSerializer(object):
|
||||
def __init__(self, inf, keypath):
|
||||
def __init__(self, inf, userkey):
|
||||
global GEN_XREF_STM, gen_xref_stm
|
||||
gen_xref_stm = GEN_XREF_STM > 1
|
||||
self.version = inf.read(8)
|
||||
inf.seek(0)
|
||||
self.doc = doc = PDFDocument()
|
||||
parser = PDFParser(doc, inf)
|
||||
doc.initialize(keypath)
|
||||
doc.initialize(userkey)
|
||||
self.objids = objids = set()
|
||||
for xref in reversed(doc.xrefs):
|
||||
trailer = xref.trailer
|
||||
|
@ -2097,142 +2162,144 @@ class PDFSerializer(object):
|
|||
self.write('endobj\n')
|
||||
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
ltext='Select file for decryption\n'
|
||||
self.status = Tkinter.Label(self, text=ltext)
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
Tkinter.Label(body, text='Key file').grid(row=0)
|
||||
self.keypath = Tkinter.Entry(body, width=30)
|
||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||
if os.path.exists('adeptkey.der'):
|
||||
self.keypath.insert(0, 'adeptkey.der')
|
||||
button = Tkinter.Button(body, text="...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
Tkinter.Label(body, text='Input file').grid(row=1)
|
||||
self.inpath = Tkinter.Entry(body, width=30)
|
||||
self.inpath.grid(row=1, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
Tkinter.Label(body, text='Output file').grid(row=2)
|
||||
self.outpath = Tkinter.Entry(body, width=30)
|
||||
self.outpath.grid(row=2, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_outpath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
|
||||
|
||||
botton = Tkinter.Button(
|
||||
buttons, text="Decrypt", width=10, command=self.decrypt)
|
||||
botton.pack(side=Tkconstants.LEFT)
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
button = Tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quit)
|
||||
button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkFileDialog.askopenfilename(
|
||||
parent=None, title='Select ADEPT key file',
|
||||
defaultextension='.der', filetypes=[('DER-encoded files', '.der'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(os.path.realpath(keypath))
|
||||
self.keypath.delete(0, Tkconstants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
def get_inpath(self):
|
||||
inpath = tkFileDialog.askopenfilename(
|
||||
parent=None, title='Select ADEPT encrypted PDF file to decrypt',
|
||||
defaultextension='.pdf', filetypes=[('PDF files', '.pdf'),
|
||||
('All files', '.*')])
|
||||
if inpath:
|
||||
inpath = os.path.normpath(os.path.realpath(inpath))
|
||||
self.inpath.delete(0, Tkconstants.END)
|
||||
self.inpath.insert(0, inpath)
|
||||
return
|
||||
|
||||
def get_outpath(self):
|
||||
outpath = tkFileDialog.asksaveasfilename(
|
||||
parent=None, title='Select unencrypted PDF file to produce',
|
||||
defaultextension='.pdf', filetypes=[('PDF files', '.pdf'),
|
||||
('All files', '.*')])
|
||||
if outpath:
|
||||
outpath = os.path.normpath(os.path.realpath(outpath))
|
||||
self.outpath.delete(0, Tkconstants.END)
|
||||
self.outpath.insert(0, outpath)
|
||||
return
|
||||
|
||||
def decrypt(self):
|
||||
keypath = self.keypath.get()
|
||||
inpath = self.inpath.get()
|
||||
outpath = self.outpath.get()
|
||||
if not keypath or not os.path.exists(keypath):
|
||||
# keyfile doesn't exist
|
||||
self.status['text'] = 'Specified Adept key file does not exist'
|
||||
return
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = 'Specified input file does not exist'
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = 'Output file not specified'
|
||||
return
|
||||
if inpath == outpath:
|
||||
self.status['text'] = 'Must have different input and output files'
|
||||
return
|
||||
# patch for non-ascii characters
|
||||
argv = [sys.argv[0], keypath, inpath, outpath]
|
||||
self.status['text'] = 'Processing ...'
|
||||
try:
|
||||
cli_main(argv)
|
||||
except Exception, a:
|
||||
self.status['text'] = 'Error: ' + str(a)
|
||||
return
|
||||
self.status['text'] = 'File successfully decrypted.\n'+\
|
||||
'Close this window or decrypt another pdf file.'
|
||||
return
|
||||
|
||||
|
||||
def decryptBook(keypath, inpath, outpath):
|
||||
def decryptBook(userkey, inpath, outpath):
|
||||
if RSA is None:
|
||||
raise ADEPTError(u"PyCrypto or OpenSSL must be installed.")
|
||||
with open(inpath, 'rb') as inf:
|
||||
try:
|
||||
serializer = PDFSerializer(inf, keypath)
|
||||
serializer = PDFSerializer(inf, userkey)
|
||||
except:
|
||||
print "Error serializing pdf. Probably wrong key."
|
||||
return 1
|
||||
print u"Error serializing pdf {0}. Probably wrong key.".format(os.path.basename(inpath))
|
||||
return 2
|
||||
# hope this will fix the 'bad file descriptor' problem
|
||||
with open(outpath, 'wb') as outf:
|
||||
# help construct to make sure the method runs to the end
|
||||
# help construct to make sure the method runs to the end
|
||||
try:
|
||||
serializer.dump(outf)
|
||||
except:
|
||||
print "error writing pdf."
|
||||
return 1
|
||||
except Exception, e:
|
||||
print u"error writing pdf: {0}".format(e.args[0])
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
if RSA is None:
|
||||
print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \
|
||||
"separately. Read the top-of-script comment for details." % \
|
||||
(progname,)
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,)
|
||||
print u"usage: {0} <keyfile.der> <inbook.pdf> <outbook.pdf>".format(progname)
|
||||
return 1
|
||||
keypath, inpath, outpath = argv[1:]
|
||||
return decryptBook(keypath, inpath, outpath)
|
||||
userkey = open(keypath,'rb').read()
|
||||
result = decryptBook(userkey, inpath, outpath)
|
||||
if result == 0:
|
||||
print u"Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath))
|
||||
return result
|
||||
|
||||
|
||||
def gui_main():
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = Tkinter.Label(self, text=u"Select files for decryption")
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
Tkinter.Label(body, text=u"Key file").grid(row=0)
|
||||
self.keypath = Tkinter.Entry(body, width=30)
|
||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||
if os.path.exists(u"adeptkey.der"):
|
||||
self.keypath.insert(0, u"adeptkey.der")
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
Tkinter.Label(body, text=u"Input file").grid(row=1)
|
||||
self.inpath = Tkinter.Entry(body, width=30)
|
||||
self.inpath.grid(row=1, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
Tkinter.Label(body, text=u"Output file").grid(row=2)
|
||||
self.outpath = Tkinter.Entry(body, width=30)
|
||||
self.outpath.grid(row=2, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_outpath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = Tkinter.Button(
|
||||
buttons, text=u"Decrypt", width=10, command=self.decrypt)
|
||||
botton.pack(side=Tkconstants.LEFT)
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
button = Tkinter.Button(
|
||||
buttons, text=u"Quit", width=10, command=self.quit)
|
||||
button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkFileDialog.askopenfilename(
|
||||
parent=None, title=u"Select Adobe Adept \'.der\' key file",
|
||||
defaultextension=u".der",
|
||||
filetypes=[('Adobe Adept DER-encoded files', '.der'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(keypath)
|
||||
self.keypath.delete(0, Tkconstants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
def get_inpath(self):
|
||||
inpath = tkFileDialog.askopenfilename(
|
||||
parent=None, title=u"Select ADEPT-encrypted PDF file to decrypt",
|
||||
defaultextension=u".pdf", filetypes=[('PDF files', '.pdf')])
|
||||
if inpath:
|
||||
inpath = os.path.normpath(inpath)
|
||||
self.inpath.delete(0, Tkconstants.END)
|
||||
self.inpath.insert(0, inpath)
|
||||
return
|
||||
|
||||
def get_outpath(self):
|
||||
outpath = tkFileDialog.asksaveasfilename(
|
||||
parent=None, title=u"Select unencrypted PDF file to produce",
|
||||
defaultextension=u".pdf", filetypes=[('PDF files', '.pdf')])
|
||||
if outpath:
|
||||
outpath = os.path.normpath(outpath)
|
||||
self.outpath.delete(0, Tkconstants.END)
|
||||
self.outpath.insert(0, outpath)
|
||||
return
|
||||
|
||||
def decrypt(self):
|
||||
keypath = self.keypath.get()
|
||||
inpath = self.inpath.get()
|
||||
outpath = self.outpath.get()
|
||||
if not keypath or not os.path.exists(keypath):
|
||||
self.status['text'] = u"Specified key file does not exist"
|
||||
return
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = u"Specified input file does not exist"
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = u"Output file not specified"
|
||||
return
|
||||
if inpath == outpath:
|
||||
self.status['text'] = u"Must have different input and output files"
|
||||
return
|
||||
userkey = open(keypath,'rb').read()
|
||||
self.status['text'] = u"Decrypting..."
|
||||
try:
|
||||
decrypt_status = decryptBook(userkey, inpath, outpath)
|
||||
except Exception, e:
|
||||
self.status['text'] = u"Error; {0}".format(e.args[0])
|
||||
return
|
||||
if decrypt_status == 0:
|
||||
self.status['text'] = u"File successfully decrypted"
|
||||
else:
|
||||
self.status['text'] = u"The was an error decrypting the file."
|
||||
|
||||
|
||||
root = Tkinter.Tk()
|
||||
if RSA is None:
|
||||
root.withdraw()
|
||||
|
@ -2241,7 +2308,7 @@ def gui_main():
|
|||
"This script requires OpenSSL or PyCrypto, which must be installed "
|
||||
"separately. Read the top-of-script comment for details.")
|
||||
return 1
|
||||
root.title('INEPT PDF Decrypter')
|
||||
root.title(u"Adobe Adept PDF Decrypter v.{0}".format(__version__))
|
||||
root.resizable(True, False)
|
||||
root.minsize(370, 0)
|
||||
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||
|
@ -2251,5 +2318,7 @@ def gui_main():
|
|||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ignobleepub.pyw, version 3.6
|
||||
# Copyright © 2009-2012 by DiapDealer et al.
|
||||
|
||||
# engine to remove drm from Kindle for Mac and Kindle for PC books
|
||||
# for personal use for archiving and converting your ebooks
|
||||
|
||||
|
@ -12,30 +16,51 @@ from __future__ import with_statement
|
|||
# be able to read OUR books on whatever device we want and to keep
|
||||
# readable for a long, long time
|
||||
|
||||
# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
|
||||
# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
|
||||
# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates
|
||||
# and many many others
|
||||
# Special thanks to The Dark Reverser for MobiDeDrm and CMBDTC for cmbdtc_dump
|
||||
# from which this script borrows most unashamedly.
|
||||
|
||||
|
||||
__version__ = '4.4'
|
||||
# Changelog
|
||||
# 1.0 - Name change to k4mobidedrm. Adds Mac support, Adds plugin code
|
||||
# 1.1 - Adds support for additional kindle.info files
|
||||
# 1.2 - Better error handling for older Mobipocket
|
||||
# 1.3 - Don't try to decrypt Topaz books
|
||||
# 1.7 - Add support for Topaz books and Kindle serial numbers. Split code.
|
||||
# 1.9 - Tidy up after Topaz, minor exception changes
|
||||
# 2.1 - Topaz fix and filename sanitizing
|
||||
# 2.2 - Topaz Fix and minor Mac code fix
|
||||
# 2.3 - More Topaz fixes
|
||||
# 2.4 - K4PC/Mac key generation fix
|
||||
# 2.6 - Better handling of non-K4PC/Mac ebooks
|
||||
# 2.7 - Better trailing bytes handling in mobidedrm
|
||||
# 2.8 - Moved parsing of kindle.info files to mac & pc util files.
|
||||
# 3.1 - Updated for new calibre interface. Now __init__ in plugin.
|
||||
# 3.5 - Now support Kindle for PC/Mac 1.6
|
||||
# 3.6 - Even better trailing bytes handling in mobidedrm
|
||||
# 3.7 - Add support for Amazon Print Replica ebooks.
|
||||
# 3.8 - Improved Topaz support
|
||||
# 4.1 - Improved Topaz support and faster decryption with alfcrypto
|
||||
# 4.2 - Added support for Amazon's KF8 format ebooks
|
||||
# 4.4 - Linux calls to Wine added, and improved configuration dialog
|
||||
# 4.5 - Linux works again without Wine. Some Mac key file search changes
|
||||
# 4.6 - First attempt to handle unicode properly
|
||||
# 4.7 - Added timing reports, and changed search for Mac key files
|
||||
# 4.8 - Much better unicode handling, matching the updated inept and ignoble scripts
|
||||
# - Moved back into plugin, __init__ in plugin now only contains plugin code.
|
||||
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
def write(self, data):
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
__version__ = '4.8'
|
||||
|
||||
import sys
|
||||
import os, csv, getopt
|
||||
import string
|
||||
|
||||
import sys, os, re
|
||||
import csv
|
||||
import getopt
|
||||
import re
|
||||
import traceback
|
||||
import time
|
||||
|
||||
buildXML = False
|
||||
import htmlentitydefs
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
@ -54,161 +79,203 @@ else:
|
|||
import topazextract
|
||||
import kgenpids
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
# cleanup bytestring filenames
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return [u"mobidedrm.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
# cleanup unicode filenames
|
||||
# borrowed from calibre from calibre/src/calibre/__init__.py
|
||||
# added in removal of non-printing chars
|
||||
# and removal of . at start
|
||||
# convert underscores to spaces (we're OK with spaces in file names)
|
||||
# added in removal of control (<32) chars
|
||||
# and removal of . at start and end
|
||||
# and with some (heavily edited) code from Paul Durrant's kindlenamer.py
|
||||
def cleanup_name(name):
|
||||
_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+/]')
|
||||
substitute='_'
|
||||
one = ''.join(char for char in name if char in string.printable)
|
||||
one = _filename_sanitize.sub(substitute, one)
|
||||
one = re.sub(r'\s', ' ', one).strip()
|
||||
one = re.sub(r'^\.+$', '_', one)
|
||||
one = one.replace('..', substitute)
|
||||
# Windows doesn't like path components that end with a period
|
||||
if one.endswith('.'):
|
||||
one = one[:-1]+substitute
|
||||
# Mac and Unix don't like file names that begin with a full stop
|
||||
if len(one) > 0 and one[0] == '.':
|
||||
one = substitute+one[1:]
|
||||
one = one.replace('_',' ')
|
||||
return one
|
||||
|
||||
def decryptBook(infile, outdir, k4, kInfoFiles, serials, pids):
|
||||
global buildXML
|
||||
# substitute filename unfriendly characters
|
||||
name = name.replace(u"<",u"[").replace(u">",u"]").replace(u" : ",u" – ").replace(u": ",u" – ").replace(u":",u"—").replace(u"/",u"_").replace(u"\\",u"_").replace(u"|",u"_").replace(u"\"",u"\'")
|
||||
# delete control characters
|
||||
name = u"".join(char for char in name if ord(char)>=32)
|
||||
# white space to single space, delete leading and trailing while space
|
||||
name = re.sub(ur"\s", u" ", name).strip()
|
||||
# remove leading dots
|
||||
while len(name)>0 and name[0] == u".":
|
||||
name = name[1:]
|
||||
# remove trailing dots (Windows doesn't like them)
|
||||
if name.endswith(u'.'):
|
||||
name = name[:-1]
|
||||
return name
|
||||
|
||||
# must be passed unicode
|
||||
def unescape(text):
|
||||
def fixup(m):
|
||||
text = m.group(0)
|
||||
if text[:2] == u"&#":
|
||||
# character reference
|
||||
try:
|
||||
if text[:3] == u"&#x":
|
||||
return unichr(int(text[3:-1], 16))
|
||||
else:
|
||||
return unichr(int(text[2:-1]))
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
# named entity
|
||||
try:
|
||||
text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
|
||||
except KeyError:
|
||||
pass
|
||||
return text # leave as is
|
||||
return re.sub(u"&#?\w+;", fixup, text)
|
||||
|
||||
def GetDecryptedBook(infile, kInfoFiles, serials, pids, starttime = time.time()):
|
||||
# handle the obvious cases at the beginning
|
||||
if not os.path.isfile(infile):
|
||||
print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: Input file does not exist"
|
||||
return 1
|
||||
|
||||
starttime = time.time()
|
||||
print "Starting decryptBook routine."
|
||||
|
||||
raise DRMException (u"Input file does not exist.")
|
||||
|
||||
mobi = True
|
||||
magic3 = file(infile,'rb').read(3)
|
||||
if magic3 == 'TPZ':
|
||||
mobi = False
|
||||
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
|
||||
if mobi:
|
||||
mb = mobidedrm.MobiBook(infile)
|
||||
else:
|
||||
mb = topazextract.TopazBook(infile)
|
||||
|
||||
title = mb.getBookTitle()
|
||||
print "Processing Book: ", title
|
||||
filenametitle = cleanup_name(title)
|
||||
outfilename = cleanup_name(bookname)
|
||||
bookname = unescape(mb.getBookTitle())
|
||||
print u"Decrypting {1} ebook: {0}".format(bookname, mb.getBookType())
|
||||
|
||||
# generate 'sensible' filename, that will sort with the original name,
|
||||
# but is close to the name from the file.
|
||||
outlength = len(outfilename)
|
||||
comparelength = min(8,min(outlength,len(filenametitle)))
|
||||
copylength = min(max(outfilename.find(' '),8),len(outfilename))
|
||||
if outlength==0:
|
||||
outfilename = filenametitle
|
||||
elif comparelength > 0:
|
||||
if outfilename[:comparelength] == filenametitle[:comparelength]:
|
||||
outfilename = filenametitle
|
||||
else:
|
||||
outfilename = outfilename[:copylength] + " " + filenametitle
|
||||
# extend PID list with book-specific PIDs
|
||||
md1, md2 = mb.getPIDMetaInfo()
|
||||
pids.extend(kgenpids.getPidList(md1, md2, serials, kInfoFiles))
|
||||
print u"Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(pids))
|
||||
|
||||
try:
|
||||
mb.processBook(pids)
|
||||
except:
|
||||
mb.cleanup
|
||||
raise
|
||||
|
||||
print u"Decryption succeeded after {0:.1f} seconds".format(time.time()-starttime)
|
||||
return mb
|
||||
|
||||
|
||||
# infile, outdir and kInfoFiles should be unicode strings
|
||||
def decryptBook(infile, outdir, kInfoFiles, serials, pids):
|
||||
starttime = time.time()
|
||||
print "Starting decryptBook routine."
|
||||
try:
|
||||
book = GetDecryptedBook(infile, kInfoFiles, serials, pids, starttime)
|
||||
except Exception, e:
|
||||
print u"Error decrypting book after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime)
|
||||
return 1
|
||||
|
||||
# if we're saving to the same folder as the original, use file name_
|
||||
# if to a different folder, use book name
|
||||
if os.path.normcase(os.path.normpath(outdir)) == os.path.normcase(os.path.normpath(os.path.dirname(infile))):
|
||||
outfilename = os.path.splitext(os.path.basename(infile))[0]
|
||||
else:
|
||||
outfilename = cleanup_name(book.getBookTitle())
|
||||
|
||||
# avoid excessively long file names
|
||||
if len(outfilename)>150:
|
||||
outfilename = outfilename[:150]
|
||||
|
||||
# build pid list
|
||||
md1, md2 = mb.getPIDMetaInfo()
|
||||
pids.extend(kgenpids.getPidList(md1, md2, k4, serials, kInfoFiles))
|
||||
outfilename = outfilename+u"_nodrm"
|
||||
outfile = os.path.join(outdir, outfilename + book.getBookExtension())
|
||||
|
||||
print "Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(pids))
|
||||
book.getFile(outfile)
|
||||
print u"Saved decrypted book {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename)
|
||||
|
||||
|
||||
try:
|
||||
mb.processBook(pids)
|
||||
|
||||
except mobidedrm.DrmException, e:
|
||||
print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||
print "Failed to decrypted book after {0:.1f} seconds".format(time.time()-starttime)
|
||||
return 1
|
||||
except topazextract.TpzDRMError, e:
|
||||
print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||
print "Failed to decrypted book after {0:.1f} seconds".format(time.time()-starttime)
|
||||
return 1
|
||||
except Exception, e:
|
||||
print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||
print "Failed to decrypted book after {0:.1f} seconds".format(time.time()-starttime)
|
||||
return 1
|
||||
|
||||
print "Successfully decrypted book after {0:.1f} seconds".format(time.time()-starttime)
|
||||
|
||||
if mobi:
|
||||
if mb.getPrintReplica():
|
||||
outfile = os.path.join(outdir, outfilename + '_nodrm' + '.azw4')
|
||||
elif mb.getMobiVersion() >= 8:
|
||||
outfile = os.path.join(outdir, outfilename + '_nodrm' + '.azw3')
|
||||
else:
|
||||
outfile = os.path.join(outdir, outfilename + '_nodrm' + '.mobi')
|
||||
mb.getMobiFile(outfile)
|
||||
print "Saved decrypted book {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename + '_nodrm')
|
||||
return 0
|
||||
|
||||
# topaz:
|
||||
print " Creating NoDRM HTMLZ Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_nodrm' + '.htmlz')
|
||||
mb.getHTMLZip(zipname)
|
||||
|
||||
print " Creating SVG ZIP Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_SVG' + '.zip')
|
||||
mb.getSVGZip(zipname)
|
||||
|
||||
if buildXML:
|
||||
print " Creating XML ZIP Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_XML' + '.zip')
|
||||
mb.getXMLZip(zipname)
|
||||
if book.getBookType()==u"Topaz":
|
||||
zipname = os.path.join(outdir, outfilename + u"_SVG.zip")
|
||||
book.getSVGZip(zipname)
|
||||
print u"Saved SVG ZIP Archive for {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename)
|
||||
|
||||
# remove internal temporary directory of Topaz pieces
|
||||
mb.cleanup()
|
||||
print "Saved decrypted Topaz book parts after {0:.1f} seconds".format(time.time()-starttime)
|
||||
return 0
|
||||
book.cleanup()
|
||||
|
||||
|
||||
def usage(progname):
|
||||
print "Removes DRM protection from K4PC/M, Kindle, Mobi and Topaz ebooks"
|
||||
print "Usage:"
|
||||
print " %s [-k <kindle.info>] [-p <pidnums>] [-s <kindleSerialNumbers>] <infile> <outdir> " % progname
|
||||
print u"Removes DRM protection from Mobipocket, Amazon KF8, Amazon Print Replica and Amazon Topaz ebooks"
|
||||
print u"Usage:"
|
||||
print u" {0} [-k <kindle.info>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] <infile> <outdir>".format(progname)
|
||||
|
||||
#
|
||||
# Main
|
||||
#
|
||||
def main(argv=sys.argv):
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
|
||||
k4 = False
|
||||
kInfoFiles = []
|
||||
serials = []
|
||||
pids = []
|
||||
|
||||
print ('K4MobiDeDrm v%(__version__)s '
|
||||
'provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc .' % globals())
|
||||
print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__)
|
||||
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:")
|
||||
except getopt.GetoptError, err:
|
||||
print str(err)
|
||||
print u"Error in options or arguments: {0}".format(err.args[0])
|
||||
usage(progname)
|
||||
sys.exit(2)
|
||||
if len(args)<2:
|
||||
usage(progname)
|
||||
sys.exit(2)
|
||||
|
||||
infile = args[0]
|
||||
outdir = args[1]
|
||||
kInfoFiles = []
|
||||
serials = []
|
||||
pids = []
|
||||
|
||||
for o, a in opts:
|
||||
if o == "-k":
|
||||
if a == None :
|
||||
|
@ -223,16 +290,13 @@ def main(argv=sys.argv):
|
|||
raise DrmException("Invalid parameter for -s")
|
||||
serials = a.split(',')
|
||||
|
||||
# try with built in Kindle Info files
|
||||
k4 = True
|
||||
if sys.platform.startswith('linux'):
|
||||
k4 = False
|
||||
kInfoFiles = None
|
||||
infile = args[0]
|
||||
outdir = args[1]
|
||||
return decryptBook(infile, outdir, k4, kInfoFiles, serials, pids)
|
||||
# try with built in Kindle Info files if not on Linux
|
||||
k4 = not sys.platform.startswith('linux')
|
||||
|
||||
return decryptBook(infile, outdir, kInfoFiles, serials, pids)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
sys.exit(main())
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# standlone set of Mac OSX specific routines needed for KindleBooks
|
||||
|
||||
from __future__ import with_statement
|
||||
|
@ -22,7 +25,7 @@ def _load_crypto_libcrypto():
|
|||
|
||||
libcrypto = find_library('crypto')
|
||||
if libcrypto is None:
|
||||
raise DrmException('libcrypto not found')
|
||||
raise DrmException(u"libcrypto not found")
|
||||
libcrypto = CDLL(libcrypto)
|
||||
|
||||
# From OpenSSL's crypto aes header
|
||||
|
@ -80,14 +83,14 @@ def _load_crypto_libcrypto():
|
|||
def set_decrypt_key(self, userkey, iv):
|
||||
self._blocksize = len(userkey)
|
||||
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
||||
raise DrmException('AES improper key used')
|
||||
raise DrmException(u"AES improper key used")
|
||||
return
|
||||
keyctx = self._keyctx = AES_KEY()
|
||||
self._iv = iv
|
||||
self._userkey = userkey
|
||||
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
|
||||
if rv < 0:
|
||||
raise DrmException('Failed to initialize AES key')
|
||||
raise DrmException(u"Failed to initialize AES key")
|
||||
|
||||
def decrypt(self, data):
|
||||
out = create_string_buffer(len(data))
|
||||
|
@ -95,7 +98,7 @@ def _load_crypto_libcrypto():
|
|||
keyctx = self._keyctx
|
||||
rv = AES_cbc_encrypt(data, out, len(data), keyctx, mutable_iv, 0)
|
||||
if rv == 0:
|
||||
raise DrmException('AES decryption failed')
|
||||
raise DrmException(u"AES decryption failed")
|
||||
return out.raw
|
||||
|
||||
def keyivgen(self, passwd, salt, iter, keylen):
|
||||
|
@ -139,20 +142,20 @@ def SHA256(message):
|
|||
return ctx.digest()
|
||||
|
||||
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
|
||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||
charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM"
|
||||
charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M'
|
||||
charMap2 = 'ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM'
|
||||
|
||||
# For kinf approach of K4Mac 1.6.X or later
|
||||
# On K4PC charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE"
|
||||
# On K4PC charMap5 = 'AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE'
|
||||
# For Mac they seem to re-use charMap2 here
|
||||
charMap5 = charMap2
|
||||
|
||||
# new in K4M 1.9.X
|
||||
testMap8 = "YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD"
|
||||
testMap8 = 'YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD'
|
||||
|
||||
|
||||
def encode(data, map):
|
||||
result = ""
|
||||
result = ''
|
||||
for char in data:
|
||||
value = ord(char)
|
||||
Q = (value ^ 0x80) // len(map)
|
||||
|
@ -167,14 +170,14 @@ def encodeHash(data,map):
|
|||
|
||||
# Decode the string in data with the characters in map. Returns the decoded bytes
|
||||
def decode(data,map):
|
||||
result = ""
|
||||
result = ''
|
||||
for i in range (0,len(data)-1,2):
|
||||
high = map.find(data[i])
|
||||
low = map.find(data[i+1])
|
||||
if (high == -1) or (low == -1) :
|
||||
break
|
||||
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||
result += pack("B",value)
|
||||
result += pack('B',value)
|
||||
return result
|
||||
|
||||
# For K4M 1.6.X and later
|
||||
|
@ -200,7 +203,7 @@ def primes(n):
|
|||
|
||||
|
||||
# uses a sub process to get the Hard Drive Serial Number using ioreg
|
||||
# returns with the serial number of drive whose BSD Name is "disk0"
|
||||
# returns with the serial number of drive whose BSD Name is 'disk0'
|
||||
def GetVolumeSerialNumber():
|
||||
sernum = os.getenv('MYSERIALNUMBER')
|
||||
if sernum != None:
|
||||
|
@ -216,11 +219,11 @@ def GetVolumeSerialNumber():
|
|||
foundIt = False
|
||||
for j in xrange(cnt):
|
||||
resline = reslst[j]
|
||||
pp = resline.find('"Serial Number" = "')
|
||||
pp = resline.find('\"Serial Number\" = \"')
|
||||
if pp >= 0:
|
||||
sernum = resline[pp+19:-1]
|
||||
sernum = sernum.strip()
|
||||
bb = resline.find('"BSD Name" = "')
|
||||
bb = resline.find('\"BSD Name\" = \"')
|
||||
if bb >= 0:
|
||||
bsdname = resline[bb+14:-1]
|
||||
bsdname = bsdname.strip()
|
||||
|
@ -277,7 +280,7 @@ def GetDiskPartitionUUID(diskpart):
|
|||
nest += 1
|
||||
if resline.find('}') >= 0:
|
||||
nest -= 1
|
||||
pp = resline.find('"UUID" = "')
|
||||
pp = resline.find('\"UUID\" = \"')
|
||||
if pp >= 0:
|
||||
uuidnum = resline[pp+10:-1]
|
||||
uuidnum = uuidnum.strip()
|
||||
|
@ -285,7 +288,7 @@ def GetDiskPartitionUUID(diskpart):
|
|||
if partnest == uuidnest and uuidnest > 0:
|
||||
foundIt = True
|
||||
break
|
||||
bb = resline.find('"BSD Name" = "')
|
||||
bb = resline.find('\"BSD Name\" = \"')
|
||||
if bb >= 0:
|
||||
bsdname = resline[bb+14:-1]
|
||||
bsdname = bsdname.strip()
|
||||
|
@ -323,7 +326,7 @@ def GetMACAddressMunged():
|
|||
if pp >= 0:
|
||||
macnum = resline[pp+6:-1]
|
||||
macnum = macnum.strip()
|
||||
# print "original mac", macnum
|
||||
# print 'original mac', macnum
|
||||
# now munge it up the way Kindle app does
|
||||
# by xoring it with 0xa5 and swapping elements 3 and 4
|
||||
maclst = macnum.split(':')
|
||||
|
@ -340,7 +343,7 @@ def GetMACAddressMunged():
|
|||
mlst[2] = maclst[2] ^ 0xa5
|
||||
mlst[1] = maclst[1] ^ 0xa5
|
||||
mlst[0] = maclst[0] ^ 0xa5
|
||||
macnum = "%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x" % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5])
|
||||
macnum = '%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x' % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5])
|
||||
foundIt = True
|
||||
break
|
||||
if not foundIt:
|
||||
|
@ -367,6 +370,19 @@ def isNewInstall():
|
|||
return False
|
||||
|
||||
|
||||
class Memoize:
|
||||
"""Memoize(fn) - an instance which acts like fn but memoizes its arguments
|
||||
Will only work on functions with non-mutable arguments
|
||||
"""
|
||||
def __init__(self, fn):
|
||||
self.fn = fn
|
||||
self.memo = {}
|
||||
def __call__(self, *args):
|
||||
if not self.memo.has_key(args):
|
||||
self.memo[args] = self.fn(*args)
|
||||
return self.memo[args]
|
||||
|
||||
@Memoize
|
||||
def GetIDString():
|
||||
# K4Mac now has an extensive set of ids strings it uses
|
||||
# in encoding pids and in creating unique passwords
|
||||
|
@ -530,7 +546,8 @@ def getKindleInfoFiles():
|
|||
# determine type of kindle info provided and return a
|
||||
# database of keynames and values
|
||||
def getDBfromFile(kInfoFile):
|
||||
names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber", "max_date", "SIGVERIF"]
|
||||
|
||||
names = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber', 'max_date', 'SIGVERIF']
|
||||
DB = {}
|
||||
cnt = 0
|
||||
infoReader = open(kInfoFile, 'r')
|
||||
|
@ -545,12 +562,12 @@ def getDBfromFile(kInfoFile):
|
|||
for item in items:
|
||||
if item != '':
|
||||
keyhash, rawdata = item.split(':')
|
||||
keyname = "unknown"
|
||||
keyname = 'unknown'
|
||||
for name in names:
|
||||
if encodeHash(name,charMap2) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
if keyname == 'unknown':
|
||||
keyname = keyhash
|
||||
encryptedValue = decode(rawdata,charMap2)
|
||||
cleartext = cud.decrypt(encryptedValue)
|
||||
|
@ -563,8 +580,8 @@ def getDBfromFile(kInfoFile):
|
|||
if hdr == '/':
|
||||
|
||||
# else newer style .kinf file used by K4Mac >= 1.6.0
|
||||
# the .kinf file uses "/" to separate it into records
|
||||
# so remove the trailing "/" to make it easy to use split
|
||||
# the .kinf file uses '/' to separate it into records
|
||||
# so remove the trailing '/' to make it easy to use split
|
||||
data = data[:-1]
|
||||
items = data.split('/')
|
||||
cud = CryptUnprotectDataV2()
|
||||
|
@ -578,11 +595,11 @@ def getDBfromFile(kInfoFile):
|
|||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
keyname = "unknown"
|
||||
keyname = 'unknown'
|
||||
|
||||
# the raw keyhash string is also used to create entropy for the actual
|
||||
# CryptProtectData Blob that represents that keys contents
|
||||
# "entropy" not used for K4Mac only K4PC
|
||||
# 'entropy' not used for K4Mac only K4PC
|
||||
# entropy = SHA1(keyhash)
|
||||
|
||||
# the remainder of the first record when decoded with charMap5
|
||||
|
@ -599,12 +616,12 @@ def getDBfromFile(kInfoFile):
|
|||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
keyname = "unknown"
|
||||
keyname = 'unknown'
|
||||
for name in names:
|
||||
if encodeHash(name,charMap5) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
if keyname == 'unknown':
|
||||
keyname = keyhash
|
||||
|
||||
# the charMap5 encoded contents data has had a length
|
||||
|
@ -615,10 +632,10 @@ def getDBfromFile(kInfoFile):
|
|||
|
||||
# The offset into the charMap5 encoded contents seems to be:
|
||||
# len(contents) - largest prime number less than or equal to int(len(content)/3)
|
||||
# (in other words split "about" 2/3rds of the way through)
|
||||
# (in other words split 'about' 2/3rds of the way through)
|
||||
|
||||
# move first offsets chars to end to align for decode by charMap5
|
||||
encdata = "".join(edlst)
|
||||
encdata = ''.join(edlst)
|
||||
contlen = len(encdata)
|
||||
|
||||
# now properly split and recombine
|
||||
|
@ -667,7 +684,7 @@ def getDBfromFile(kInfoFile):
|
|||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
keyname = "unknown"
|
||||
keyname = 'unknown'
|
||||
|
||||
# unlike K4PC the keyhash is not used in generating entropy
|
||||
# entropy = SHA1(keyhash) + added_entropy
|
||||
|
@ -687,12 +704,12 @@ def getDBfromFile(kInfoFile):
|
|||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
keyname = "unknown"
|
||||
keyname = 'unknown'
|
||||
for name in names:
|
||||
if encodeHash(name,testMap8) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
if keyname == 'unknown':
|
||||
keyname = keyhash
|
||||
|
||||
# the testMap8 encoded contents data has had a length
|
||||
|
@ -703,10 +720,10 @@ def getDBfromFile(kInfoFile):
|
|||
|
||||
# The offset into the testMap8 encoded contents seems to be:
|
||||
# len(contents) - largest prime number less than or equal to int(len(content)/3)
|
||||
# (in other words split "about" 2/3rds of the way through)
|
||||
# (in other words split 'about' 2/3rds of the way through)
|
||||
|
||||
# move first offsets chars to end to align for decode by testMap8
|
||||
encdata = "".join(edlst)
|
||||
encdata = ''.join(edlst)
|
||||
contlen = len(encdata)
|
||||
|
||||
# now properly split and recombine
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# K4PC Windows specific routines
|
||||
|
||||
from __future__ import with_statement
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
import sys
|
||||
|
@ -17,26 +18,24 @@ global charMap4
|
|||
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
if inCalibre:
|
||||
if sys.platform.startswith('win'):
|
||||
from calibre.constants import iswindows, isosx
|
||||
if iswindows:
|
||||
from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||
|
||||
if sys.platform.startswith('darwin'):
|
||||
if isosx:
|
||||
from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||
else:
|
||||
if sys.platform.startswith('win'):
|
||||
inCalibre = False
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
if iswindows:
|
||||
from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||
|
||||
if sys.platform.startswith('darwin'):
|
||||
if isosx:
|
||||
from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||
|
||||
|
||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M'
|
||||
charMap3 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||
charMap4 = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
|
||||
|
||||
# crypto digestroutines
|
||||
import hashlib
|
||||
|
@ -54,7 +53,7 @@ def SHA1(message):
|
|||
|
||||
# Encode the bytes in data with the characters in map
|
||||
def encode(data, map):
|
||||
result = ""
|
||||
result = ''
|
||||
for char in data:
|
||||
value = ord(char)
|
||||
Q = (value ^ 0x80) // len(map)
|
||||
|
@ -69,14 +68,14 @@ def encodeHash(data,map):
|
|||
|
||||
# Decode the string in data with the characters in map. Returns the decoded bytes
|
||||
def decode(data,map):
|
||||
result = ""
|
||||
result = ''
|
||||
for i in range (0,len(data)-1,2):
|
||||
high = map.find(data[i])
|
||||
low = map.find(data[i+1])
|
||||
if (high == -1) or (low == -1) :
|
||||
break
|
||||
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||
result += pack("B",value)
|
||||
result += pack('B',value)
|
||||
return result
|
||||
|
||||
#
|
||||
|
@ -98,7 +97,7 @@ def getSixBitsFromBitField(bitField,offset):
|
|||
# 8 bits to six bits encoding from hash to generate PID string
|
||||
def encodePID(hash):
|
||||
global charMap3
|
||||
PID = ""
|
||||
PID = ''
|
||||
for position in range (0,8):
|
||||
PID += charMap3[getSixBitsFromBitField(hash,position)]
|
||||
return PID
|
||||
|
@ -129,7 +128,7 @@ def generatePidSeed(table,dsn) :
|
|||
def generateDevicePID(table,dsn,nbRoll):
|
||||
global charMap4
|
||||
seed = generatePidSeed(table,dsn)
|
||||
pidAscii = ""
|
||||
pidAscii = ''
|
||||
pid = [(seed >>24) &0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF,(seed>>24) & 0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF]
|
||||
index = 0
|
||||
for counter in range (0,nbRoll):
|
||||
|
@ -176,28 +175,31 @@ def pidFromSerial(s, l):
|
|||
|
||||
|
||||
# Parse the EXTH header records and use the Kindle serial number to calculate the book pid.
|
||||
def getKindlePid(pidlst, rec209, token, serialnum):
|
||||
def getKindlePids(rec209, token, serialnum):
|
||||
pids=[]
|
||||
|
||||
# Compute book PID
|
||||
pidHash = SHA1(serialnum+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
pids.append(bookPID)
|
||||
|
||||
# compute fixed pid for old pre 2.5 firmware update pid as well
|
||||
bookPID = pidFromSerial(serialnum, 7) + "*"
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
kindlePID = pidFromSerial(serialnum, 7) + "*"
|
||||
kindlePID = checksumPid(kindlePID)
|
||||
pids.append(kindlePID)
|
||||
|
||||
return pidlst
|
||||
return pids
|
||||
|
||||
|
||||
# parse the Kindleinfo file to calculate the book pid.
|
||||
|
||||
keynames = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
|
||||
keynames = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber']
|
||||
|
||||
def getK4Pids(pidlst, rec209, token, kInfoFile):
|
||||
def getK4Pids(rec209, token, kInfoFile):
|
||||
global charMap1
|
||||
kindleDatabase = None
|
||||
pids = []
|
||||
try:
|
||||
kindleDatabase = getDBfromFile(kInfoFile)
|
||||
except Exception, message:
|
||||
|
@ -206,17 +208,17 @@ def getK4Pids(pidlst, rec209, token, kInfoFile):
|
|||
pass
|
||||
|
||||
if kindleDatabase == None :
|
||||
return pidlst
|
||||
return pids
|
||||
|
||||
try:
|
||||
# Get the Mazama Random number
|
||||
MazamaRandomNumber = kindleDatabase["MazamaRandomNumber"]
|
||||
MazamaRandomNumber = kindleDatabase['MazamaRandomNumber']
|
||||
|
||||
# Get the kindle account token
|
||||
kindleAccountToken = kindleDatabase["kindle.account.tokens"]
|
||||
kindleAccountToken = kindleDatabase['kindle.account.tokens']
|
||||
except KeyError:
|
||||
print "Keys not found in " + kInfoFile
|
||||
return pidlst
|
||||
print u"Keys not found in {0}".format(os.path.basename(kInfoFile))
|
||||
return pids
|
||||
|
||||
# Get the ID string used
|
||||
encodedIDString = encodeHash(GetIDString(),charMap1)
|
||||
|
@ -231,7 +233,7 @@ def getK4Pids(pidlst, rec209, token, kInfoFile):
|
|||
table = generatePidEncryptionTable()
|
||||
devicePID = generateDevicePID(table,DSN,4)
|
||||
devicePID = checksumPid(devicePID)
|
||||
pidlst.append(devicePID)
|
||||
pids.append(devicePID)
|
||||
|
||||
# Compute book PIDs
|
||||
|
||||
|
@ -239,36 +241,38 @@ def getK4Pids(pidlst, rec209, token, kInfoFile):
|
|||
pidHash = SHA1(DSN+kindleAccountToken+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
pids.append(bookPID)
|
||||
|
||||
# variant 1
|
||||
pidHash = SHA1(kindleAccountToken+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
pids.append(bookPID)
|
||||
|
||||
# variant 2
|
||||
pidHash = SHA1(DSN+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
pids.append(bookPID)
|
||||
|
||||
return pidlst
|
||||
return pids
|
||||
|
||||
def getPidList(md1, md2, k4 = True, serials=[], kInfoFiles=[]):
|
||||
def getPidList(md1, md2, serials=[], kInfoFiles=[]):
|
||||
pidlst = []
|
||||
if kInfoFiles is None:
|
||||
kInfoFiles = []
|
||||
if k4:
|
||||
if serials is None:
|
||||
serials = []
|
||||
if iswindows or isosx:
|
||||
kInfoFiles.extend(getKindleInfoFiles())
|
||||
for infoFile in kInfoFiles:
|
||||
try:
|
||||
pidlst = getK4Pids(pidlst, md1, md2, infoFile)
|
||||
except Exception, message:
|
||||
print("Error getting PIDs from " + infoFile + ": " + message)
|
||||
pidlst.extend(getK4Pids(md1, md2, infoFile))
|
||||
except Exception, e:
|
||||
print u"Error getting PIDs from {0}: {1}".format(os.path.basename(infoFile),e.args[0])
|
||||
for serialnum in serials:
|
||||
try:
|
||||
pidlst = getKindlePid(pidlst, md1, md2, serialnum)
|
||||
pidlst.extend(getKindlePids(md1, md2, serialnum))
|
||||
except Exception, message:
|
||||
print("Error getting PIDs from " + serialnum + ": " + message)
|
||||
print u"Error getting PIDs from serial number {0}: {1}".format(serialnum ,e.args[0])
|
||||
return pidlst
|
||||
|
|
142
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kindlepid.py
Normal file
142
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kindlepid.py
Normal file
|
@ -0,0 +1,142 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Mobipocket PID calculator v0.4 for Amazon Kindle.
|
||||
# Copyright (c) 2007, 2009 Igor Skochinsky <skochinsky@mail.ru>
|
||||
# History:
|
||||
# 0.1 Initial release
|
||||
# 0.2 Added support for generating PID for iPhone (thanks to mbp)
|
||||
# 0.3 changed to autoflush stdout, fixed return code usage
|
||||
# 0.3 updated for unicode
|
||||
|
||||
import sys
|
||||
import binascii
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return [u"mobidedrm.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
if sys.hexversion >= 0x3000000:
|
||||
print 'This script is incompatible with Python 3.x. Please install Python 2.7.x.'
|
||||
sys.exit(2)
|
||||
|
||||
letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
|
||||
|
||||
def crc32(s):
|
||||
return (~binascii.crc32(s,-1))&0xFFFFFFFF
|
||||
|
||||
def checksumPid(s):
|
||||
crc = crc32(s)
|
||||
crc = crc ^ (crc >> 16)
|
||||
res = s
|
||||
l = len(letters)
|
||||
for i in (0,1):
|
||||
b = crc & 0xff
|
||||
pos = (b // l) ^ (b % l)
|
||||
res += letters[pos%l]
|
||||
crc >>= 8
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def pidFromSerial(s, l):
|
||||
crc = crc32(s)
|
||||
|
||||
arr1 = [0]*l
|
||||
for i in xrange(len(s)):
|
||||
arr1[i%l] ^= ord(s[i])
|
||||
|
||||
crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff]
|
||||
for i in xrange(l):
|
||||
arr1[i] ^= crc_bytes[i&3]
|
||||
|
||||
pid = ''
|
||||
for i in xrange(l):
|
||||
b = arr1[i] & 0xff
|
||||
pid+=letters[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))]
|
||||
|
||||
return pid
|
||||
|
||||
def cli_main(argv=unicode_argv()):
|
||||
print u"Mobipocket PID calculator for Amazon Kindle. Copyright © 2007, 2009 Igor Skochinsky"
|
||||
if len(sys.argv)==2:
|
||||
serial = sys.argv[1]
|
||||
else:
|
||||
print u"Usage: kindlepid.py <Kindle Serial Number>/<iPhone/iPod Touch UDID>"
|
||||
return 1
|
||||
if len(serial)==16:
|
||||
if serial.startswith("B"):
|
||||
print u"Kindle serial number detected"
|
||||
else:
|
||||
print u"Warning: unrecognized serial number. Please recheck input."
|
||||
return 1
|
||||
pid = pidFromSerial(serial.encode("utf-8"),7)+'*'
|
||||
print u"Mobipocket PID for Kindle serial#{0} is {1} ".format(serial,checksumPid(pid))
|
||||
return 0
|
||||
elif len(serial)==40:
|
||||
print u"iPhone serial number (UDID) detected"
|
||||
pid = pidFromSerial(serial.encode("utf-8"),8)
|
||||
print u"Mobipocket PID for iPhone serial#{0} is {1} ".format(serial,checksumPid(pid))
|
||||
return 0
|
||||
print u"Warning: unrecognized serial number. Please recheck input."
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
|
@ -1,5 +1,11 @@
|
|||
#!/usr/bin/python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# mobidedrm.py, version 0.38
|
||||
# Copyright © 2008 The Dark Reverser
|
||||
#
|
||||
# Modified 2008–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
|
||||
# This is a python script. You need a Python interpreter to run it.
|
||||
# For example, ActiveState Python, which exists for windows.
|
||||
#
|
||||
|
@ -59,26 +65,78 @@
|
|||
# 0.35 - add interface to get mobi_version
|
||||
# 0.36 - fixed problem with TEXtREAd and getBookTitle interface
|
||||
# 0.37 - Fixed double announcement for stand-alone operation
|
||||
# 0.38 - Unicode used wherever possible, cope with absent alfcrypto
|
||||
|
||||
|
||||
__version__ = '0.37'
|
||||
__version__ = u"0.38"
|
||||
|
||||
import sys
|
||||
import os
|
||||
import struct
|
||||
import binascii
|
||||
try:
|
||||
from alfcrypto import Pukall_Cipher
|
||||
except:
|
||||
print u"AlfCrypto not found. Using python PC1 implementation."
|
||||
|
||||
class Unbuffered:
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
|
||||
import os
|
||||
import struct
|
||||
import binascii
|
||||
from alfcrypto import Pukall_Cipher
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return [u"mobidedrm.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = 'utf-8'
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
@ -90,40 +148,45 @@ class DrmException(Exception):
|
|||
|
||||
# Implementation of Pukall Cipher 1
|
||||
def PC1(key, src, decryption=True):
|
||||
return Pukall_Cipher().PC1(key,src,decryption)
|
||||
# sum1 = 0;
|
||||
# sum2 = 0;
|
||||
# keyXorVal = 0;
|
||||
# if len(key)!=16:
|
||||
# print "Bad key length!"
|
||||
# return None
|
||||
# wkey = []
|
||||
# for i in xrange(8):
|
||||
# wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
|
||||
# dst = ""
|
||||
# for i in xrange(len(src)):
|
||||
# temp1 = 0;
|
||||
# byteXorVal = 0;
|
||||
# for j in xrange(8):
|
||||
# temp1 ^= wkey[j]
|
||||
# sum2 = (sum2+j)*20021 + sum1
|
||||
# sum1 = (temp1*346)&0xFFFF
|
||||
# sum2 = (sum2+sum1)&0xFFFF
|
||||
# temp1 = (temp1*20021+1)&0xFFFF
|
||||
# byteXorVal ^= temp1 ^ sum2
|
||||
# curByte = ord(src[i])
|
||||
# if not decryption:
|
||||
# keyXorVal = curByte * 257;
|
||||
# curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
|
||||
# if decryption:
|
||||
# keyXorVal = curByte * 257;
|
||||
# for j in xrange(8):
|
||||
# wkey[j] ^= keyXorVal;
|
||||
# dst+=chr(curByte)
|
||||
# return dst
|
||||
# if we can get it from alfcrypto, use that
|
||||
try:
|
||||
return Pukall_Cipher().PC1(key,src,decryption)
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
# use slow python version, since Pukall_Cipher didn't load
|
||||
sum1 = 0;
|
||||
sum2 = 0;
|
||||
keyXorVal = 0;
|
||||
if len(key)!=16:
|
||||
DrmException (u"PC1: Bad key length")
|
||||
wkey = []
|
||||
for i in xrange(8):
|
||||
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
|
||||
dst = ""
|
||||
for i in xrange(len(src)):
|
||||
temp1 = 0;
|
||||
byteXorVal = 0;
|
||||
for j in xrange(8):
|
||||
temp1 ^= wkey[j]
|
||||
sum2 = (sum2+j)*20021 + sum1
|
||||
sum1 = (temp1*346)&0xFFFF
|
||||
sum2 = (sum2+sum1)&0xFFFF
|
||||
temp1 = (temp1*20021+1)&0xFFFF
|
||||
byteXorVal ^= temp1 ^ sum2
|
||||
curByte = ord(src[i])
|
||||
if not decryption:
|
||||
keyXorVal = curByte * 257;
|
||||
curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
|
||||
if decryption:
|
||||
keyXorVal = curByte * 257;
|
||||
for j in xrange(8):
|
||||
wkey[j] ^= keyXorVal;
|
||||
dst+=chr(curByte)
|
||||
return dst
|
||||
|
||||
def checksumPid(s):
|
||||
letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
|
||||
crc = (~binascii.crc32(s,-1))&0xFFFFFFFF
|
||||
crc = crc ^ (crc >> 16)
|
||||
res = s
|
||||
|
@ -171,17 +234,24 @@ class MobiBook:
|
|||
off = self.sections[section][0]
|
||||
return self.data_file[off:endoff]
|
||||
|
||||
def __init__(self, infile, announce = True):
|
||||
if announce:
|
||||
print ('MobiDeDrm v%(__version__)s. '
|
||||
'Copyright 2008-2012 The Dark Reverser et al.' % globals())
|
||||
def cleanup(self):
|
||||
# to match function in Topaz book
|
||||
pass
|
||||
|
||||
def __init__(self, infile):
|
||||
print u"MobiDeDrm v{0:s}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__)
|
||||
|
||||
try:
|
||||
from alfcrypto import Pukall_Cipher
|
||||
except:
|
||||
print u"AlfCrypto not found. Using python PC1 implementation."
|
||||
|
||||
# initial sanity check on file
|
||||
self.data_file = file(infile, 'rb').read()
|
||||
self.mobi_data = ''
|
||||
self.header = self.data_file[0:78]
|
||||
if self.header[0x3C:0x3C+8] != 'BOOKMOBI' and self.header[0x3C:0x3C+8] != 'TEXtREAd':
|
||||
raise DrmException("invalid file format")
|
||||
raise DrmException(u"Invalid file format")
|
||||
self.magic = self.header[0x3C:0x3C+8]
|
||||
self.crypto_type = -1
|
||||
|
||||
|
@ -199,7 +269,7 @@ class MobiBook:
|
|||
self.compression, = struct.unpack('>H', self.sect[0x0:0x0+2])
|
||||
|
||||
if self.magic == 'TEXtREAd':
|
||||
print "Book has format: ", self.magic
|
||||
print u"PalmDoc format book detected."
|
||||
self.extra_data_flags = 0
|
||||
self.mobi_length = 0
|
||||
self.mobi_codepage = 1252
|
||||
|
@ -209,11 +279,11 @@ class MobiBook:
|
|||
self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18])
|
||||
self.mobi_codepage, = struct.unpack('>L',self.sect[0x1c:0x20])
|
||||
self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C])
|
||||
print "MOBI header version = %d, length = %d" %(self.mobi_version, self.mobi_length)
|
||||
print u"MOBI header version {0:d}, header length {1:d}".format(self.mobi_version, self.mobi_length)
|
||||
self.extra_data_flags = 0
|
||||
if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
|
||||
self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4])
|
||||
print "Extra Data Flags = %d" % self.extra_data_flags
|
||||
print u"Extra Data Flags: {0:d}".format(self.extra_data_flags)
|
||||
if (self.compression != 17480):
|
||||
# multibyte utf8 data is included in the encryption for PalmDoc compression
|
||||
# so clear that byte so that we leave it to be decrypted.
|
||||
|
@ -223,10 +293,10 @@ class MobiBook:
|
|||
self.meta_array = {}
|
||||
try:
|
||||
exth_flag, = struct.unpack('>L', self.sect[0x80:0x84])
|
||||
exth = 'NONE'
|
||||
exth = ''
|
||||
if exth_flag & 0x40:
|
||||
exth = self.sect[16 + self.mobi_length:]
|
||||
if (len(exth) >= 4) and (exth[:4] == 'EXTH'):
|
||||
if (len(exth) >= 12) and (exth[:4] == 'EXTH'):
|
||||
nitems, = struct.unpack('>I', exth[8:12])
|
||||
pos = 12
|
||||
for i in xrange(nitems):
|
||||
|
@ -236,10 +306,10 @@ class MobiBook:
|
|||
# reset the text to speech flag and clipping limit, if present
|
||||
if type == 401 and size == 9:
|
||||
# set clipping limit to 100%
|
||||
self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8)
|
||||
self.patchSection(0, '\144', 16 + self.mobi_length + pos + 8)
|
||||
elif type == 404 and size == 9:
|
||||
# make sure text to speech is enabled
|
||||
self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8)
|
||||
self.patchSection(0, '\0', 16 + self.mobi_length + pos + 8)
|
||||
# print type, size, content, content.encode('hex')
|
||||
pos += size
|
||||
except:
|
||||
|
@ -265,8 +335,8 @@ class MobiBook:
|
|||
codec = codec_map[self.mobi_codepage]
|
||||
if title == '':
|
||||
title = self.header[:32]
|
||||
title = title.split("\0")[0]
|
||||
return unicode(title, codec).encode('utf-8')
|
||||
title = title.split('\0')[0]
|
||||
return unicode(title, codec)
|
||||
|
||||
def getPIDMetaInfo(self):
|
||||
rec209 = ''
|
||||
|
@ -297,7 +367,7 @@ class MobiBook:
|
|||
|
||||
def parseDRM(self, data, count, pidlist):
|
||||
found_key = None
|
||||
keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96"
|
||||
keyvec1 = '\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96'
|
||||
for pid in pidlist:
|
||||
bigpid = pid.ljust(16,'\0')
|
||||
temp_key = PC1(keyvec1, bigpid, False)
|
||||
|
@ -315,7 +385,7 @@ class MobiBook:
|
|||
break
|
||||
if not found_key:
|
||||
# Then try the default encoding that doesn't require a PID
|
||||
pid = "00000000"
|
||||
pid = '00000000'
|
||||
temp_key = keyvec1
|
||||
temp_key_sum = sum(map(ord,temp_key)) & 0xff
|
||||
for i in xrange(count):
|
||||
|
@ -328,82 +398,90 @@ class MobiBook:
|
|||
break
|
||||
return [found_key,pid]
|
||||
|
||||
def getMobiFile(self, outpath):
|
||||
def getFile(self, outpath):
|
||||
file(outpath,'wb').write(self.mobi_data)
|
||||
|
||||
def getMobiVersion(self):
|
||||
return self.mobi_version
|
||||
def getBookType(self):
|
||||
if self.print_replica:
|
||||
return u"Print Replica"
|
||||
if self.mobi_version >= 8:
|
||||
return u"Kindle Format 8"
|
||||
return u"Mobipocket"
|
||||
|
||||
def getPrintReplica(self):
|
||||
return self.print_replica
|
||||
def getBookExtension(self):
|
||||
if self.print_replica:
|
||||
return u".azw4"
|
||||
if self.mobi_version >= 8:
|
||||
return u".azw3"
|
||||
return u".mobi"
|
||||
|
||||
def processBook(self, pidlist):
|
||||
crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2])
|
||||
print 'Crypto Type is: ', crypto_type
|
||||
print u"Crypto Type is: {0:d}".format(crypto_type)
|
||||
self.crypto_type = crypto_type
|
||||
if crypto_type == 0:
|
||||
print "This book is not encrypted."
|
||||
print u"This book is not encrypted."
|
||||
# we must still check for Print Replica
|
||||
self.print_replica = (self.loadSection(1)[0:4] == '%MOP')
|
||||
self.mobi_data = self.data_file
|
||||
return
|
||||
if crypto_type != 2 and crypto_type != 1:
|
||||
raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type)
|
||||
raise DrmException(u"Cannot decode unknown Mobipocket encryption type {0:d}".format(crypto_type))
|
||||
if 406 in self.meta_array:
|
||||
data406 = self.meta_array[406]
|
||||
val406, = struct.unpack('>Q',data406)
|
||||
if val406 != 0:
|
||||
raise DrmException("Cannot decode library or rented ebooks.")
|
||||
raise DrmException(u"Cannot decode library or rented ebooks.")
|
||||
|
||||
goodpids = []
|
||||
for pid in pidlist:
|
||||
if len(pid)==10:
|
||||
if checksumPid(pid[0:-2]) != pid:
|
||||
print "Warning: PID " + pid + " has incorrect checksum, should have been "+checksumPid(pid[0:-2])
|
||||
print u"Warning: PID {0} has incorrect checksum, should have been {1}".format(pid,checksumPid(pid[0:-2]))
|
||||
goodpids.append(pid[0:-2])
|
||||
elif len(pid)==8:
|
||||
goodpids.append(pid)
|
||||
|
||||
if self.crypto_type == 1:
|
||||
t1_keyvec = "QDCVEPMU675RUBSZ"
|
||||
t1_keyvec = 'QDCVEPMU675RUBSZ'
|
||||
if self.magic == 'TEXtREAd':
|
||||
bookkey_data = self.sect[0x0E:0x0E+16]
|
||||
elif self.mobi_version < 0:
|
||||
bookkey_data = self.sect[0x90:0x90+16]
|
||||
else:
|
||||
bookkey_data = self.sect[self.mobi_length+16:self.mobi_length+32]
|
||||
pid = "00000000"
|
||||
pid = '00000000'
|
||||
found_key = PC1(t1_keyvec, bookkey_data)
|
||||
else :
|
||||
# calculate the keys
|
||||
drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16])
|
||||
if drm_count == 0:
|
||||
raise DrmException("Not yet initialised with PID. Must be opened with Mobipocket Reader first.")
|
||||
raise DrmException(u"Encryption not initialised. Must be opened with Mobipocket Reader first.")
|
||||
found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids)
|
||||
if not found_key:
|
||||
raise DrmException("No key found in " + str(len(goodpids)) + " keys tried. Read the FAQs at Alf's blog. Only if none apply, report this failure for help.")
|
||||
raise DrmException(u"No key found in {0:d} keys tried. Read the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(len(goodpids)))
|
||||
# kill the drm keys
|
||||
self.patchSection(0, "\0" * drm_size, drm_ptr)
|
||||
self.patchSection(0, '\0' * drm_size, drm_ptr)
|
||||
# kill the drm pointers
|
||||
self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8)
|
||||
self.patchSection(0, '\xff' * 4 + '\0' * 12, 0xA8)
|
||||
|
||||
if pid=="00000000":
|
||||
print "File has default encryption, no specific PID."
|
||||
if pid=='00000000':
|
||||
print u"File has default encryption, no specific key needed."
|
||||
else:
|
||||
print "File is encoded with PID "+checksumPid(pid)+"."
|
||||
print u"File is encoded with PID {0}.".format(checksumPid(pid))
|
||||
|
||||
# clear the crypto type
|
||||
self.patchSection(0, "\0" * 2, 0xC)
|
||||
|
||||
# decrypt sections
|
||||
print "Decrypting. Please wait . . .",
|
||||
print u"Decrypting. Please wait . . .",
|
||||
mobidataList = []
|
||||
mobidataList.append(self.data_file[:self.sections[1][0]])
|
||||
for i in xrange(1, self.records+1):
|
||||
data = self.loadSection(i)
|
||||
extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags)
|
||||
if i%100 == 0:
|
||||
print ".",
|
||||
print u".",
|
||||
# print "record %d, extra_size %d" %(i,extra_size)
|
||||
decoded_data = PC1(found_key, data[0:len(data) - extra_size])
|
||||
if i==1:
|
||||
|
@ -414,31 +492,24 @@ class MobiBook:
|
|||
if self.num_sections > self.records+1:
|
||||
mobidataList.append(self.data_file[self.sections[self.records+1][0]:])
|
||||
self.mobi_data = "".join(mobidataList)
|
||||
print "done"
|
||||
print u"done"
|
||||
return
|
||||
|
||||
def getUnencryptedBook(infile,pid,announce=True):
|
||||
def getUnencryptedBook(infile,pidlist):
|
||||
if not os.path.isfile(infile):
|
||||
raise DrmException('Input File Not Found')
|
||||
book = MobiBook(infile,announce)
|
||||
book.processBook([pid])
|
||||
return book.mobi_data
|
||||
|
||||
def getUnencryptedBookWithList(infile,pidlist,announce=True):
|
||||
if not os.path.isfile(infile):
|
||||
raise DrmException('Input File Not Found')
|
||||
book = MobiBook(infile, announce)
|
||||
raise DrmException(u"Input File Not Found.")
|
||||
book = MobiBook(infile)
|
||||
book.processBook(pidlist)
|
||||
return book.mobi_data
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
print ('MobiDeDrm v%(__version__)s. '
|
||||
'Copyright 2008-2012 The Dark Reverser et al.' % globals())
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
if len(argv)<3 or len(argv)>4:
|
||||
print "Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks"
|
||||
print "Usage:"
|
||||
print " %s <infile> <outfile> [<Comma separated list of PIDs to try>]" % sys.argv[0]
|
||||
print u"MobiDeDrm v{0}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__)
|
||||
print u"Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks"
|
||||
print u"Usage:"
|
||||
print u" {0} <infile> <outfile> [<Comma separated list of PIDs to try>]".format(os.path.basename(sys.argv[0]))
|
||||
return 1
|
||||
else:
|
||||
infile = argv[1]
|
||||
|
@ -446,15 +517,17 @@ def main(argv=sys.argv):
|
|||
if len(argv) is 4:
|
||||
pidlist = argv[3].split(',')
|
||||
else:
|
||||
pidlist = {}
|
||||
pidlist = []
|
||||
try:
|
||||
stripped_file = getUnencryptedBookWithList(infile, pidlist, False)
|
||||
stripped_file = getUnencryptedBook(infile, pidlist)
|
||||
file(outfile, 'wb').write(stripped_file)
|
||||
except DrmException, e:
|
||||
print "Error: %s" % e
|
||||
print u"MobiDeDRM v{0} Error: {0:s}".format(__version__,e.args[0])
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
if __name__ == '__main__':
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
|
|
|
@ -1,43 +1,90 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
class Unbuffered:
|
||||
# topazextract.py, version ?
|
||||
# Mostly written by some_updates based on code from many others
|
||||
|
||||
__version__ = '4.8'
|
||||
|
||||
import sys
|
||||
import os, csv, getopt
|
||||
import zlib, zipfile, tempfile, shutil
|
||||
import traceback
|
||||
from struct import pack
|
||||
from struct import unpack
|
||||
from alfcrypto import Topaz_Cipher
|
||||
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return [u"mobidedrm.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = 'utf-8'
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
buildXML = False
|
||||
|
||||
import os, csv, getopt
|
||||
import zlib, zipfile, tempfile, shutil
|
||||
from struct import pack
|
||||
from struct import unpack
|
||||
from alfcrypto import Topaz_Cipher
|
||||
|
||||
class TpzDRMError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# local support routines
|
||||
if inCalibre:
|
||||
from calibre_plugins.k4mobidedrm import kgenpids
|
||||
else:
|
||||
inCalibre = False
|
||||
import kgenpids
|
||||
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# recursive zip creation support routine
|
||||
def zipUpDir(myzip, tdir, localname):
|
||||
currentdir = tdir
|
||||
if localname != "":
|
||||
if localname != u"":
|
||||
currentdir = os.path.join(currentdir,localname)
|
||||
list = os.listdir(currentdir)
|
||||
for file in list:
|
||||
|
@ -73,7 +120,7 @@ def bookReadEncodedNumber(fo):
|
|||
# Get a length prefixed string from file
|
||||
def bookReadString(fo):
|
||||
stringLength = bookReadEncodedNumber(fo)
|
||||
return unpack(str(stringLength)+"s",fo.read(stringLength))[0]
|
||||
return unpack(str(stringLength)+'s',fo.read(stringLength))[0]
|
||||
|
||||
#
|
||||
# crypto routines
|
||||
|
@ -112,13 +159,13 @@ def decryptRecord(data,PID):
|
|||
# Try to decrypt a dkey record (contains the bookPID)
|
||||
def decryptDkeyRecord(data,PID):
|
||||
record = decryptRecord(data,PID)
|
||||
fields = unpack("3sB8sB8s3s",record)
|
||||
if fields[0] != "PID" or fields[5] != "pid" :
|
||||
raise TpzDRMError("Didn't find PID magic numbers in record")
|
||||
fields = unpack('3sB8sB8s3s',record)
|
||||
if fields[0] != 'PID' or fields[5] != 'pid' :
|
||||
raise DrmException(u"Didn't find PID magic numbers in record")
|
||||
elif fields[1] != 8 or fields[3] != 8 :
|
||||
raise TpzDRMError("Record didn't contain correct length fields")
|
||||
raise DrmException(u"Record didn't contain correct length fields")
|
||||
elif fields[2] != PID :
|
||||
raise TpzDRMError("Record didn't contain PID")
|
||||
raise DrmException(u"Record didn't contain PID")
|
||||
return fields[4]
|
||||
|
||||
# Decrypt all dkey records (contain the book PID)
|
||||
|
@ -131,11 +178,11 @@ def decryptDkeyRecords(data,PID):
|
|||
try:
|
||||
key = decryptDkeyRecord(data[1:length+1],PID)
|
||||
records.append(key)
|
||||
except TpzDRMError:
|
||||
except DrmException:
|
||||
pass
|
||||
data = data[1+length:]
|
||||
if len(records) == 0:
|
||||
raise TpzDRMError("BookKey Not Found")
|
||||
raise DrmException(u"BookKey Not Found")
|
||||
return records
|
||||
|
||||
|
||||
|
@ -148,9 +195,9 @@ class TopazBook:
|
|||
self.bookHeaderRecords = {}
|
||||
self.bookMetadata = {}
|
||||
self.bookKey = None
|
||||
magic = unpack("4s",self.fo.read(4))[0]
|
||||
magic = unpack('4s',self.fo.read(4))[0]
|
||||
if magic != 'TPZ0':
|
||||
raise TpzDRMError("Parse Error : Invalid Header, not a Topaz file")
|
||||
raise DrmException(u"Parse Error : Invalid Header, not a Topaz file")
|
||||
self.parseTopazHeaders()
|
||||
self.parseMetadata()
|
||||
|
||||
|
@ -167,7 +214,7 @@ class TopazBook:
|
|||
# Read and parse one header record at the current book file position and return the associated data
|
||||
# [[offset,decompressedLength,compressedLength],...]
|
||||
if ord(self.fo.read(1)) != 0x63:
|
||||
raise TpzDRMError("Parse Error : Invalid Header")
|
||||
raise DrmException(u"Parse Error : Invalid Header")
|
||||
tag = bookReadString(self.fo)
|
||||
record = bookReadHeaderRecordData()
|
||||
return [tag,record]
|
||||
|
@ -177,15 +224,15 @@ class TopazBook:
|
|||
# print result[0], result[1]
|
||||
self.bookHeaderRecords[result[0]] = result[1]
|
||||
if ord(self.fo.read(1)) != 0x64 :
|
||||
raise TpzDRMError("Parse Error : Invalid Header")
|
||||
raise DrmException(u"Parse Error : Invalid Header")
|
||||
self.bookPayloadOffset = self.fo.tell()
|
||||
|
||||
def parseMetadata(self):
|
||||
# Parse the metadata record from the book payload and return a list of [key,values]
|
||||
self.fo.seek(self.bookPayloadOffset + self.bookHeaderRecords["metadata"][0][0])
|
||||
self.fo.seek(self.bookPayloadOffset + self.bookHeaderRecords['metadata'][0][0])
|
||||
tag = bookReadString(self.fo)
|
||||
if tag != "metadata" :
|
||||
raise TpzDRMError("Parse Error : Record Names Don't Match")
|
||||
if tag != 'metadata' :
|
||||
raise DrmException(u"Parse Error : Record Names Don't Match")
|
||||
flags = ord(self.fo.read(1))
|
||||
nbRecords = ord(self.fo.read(1))
|
||||
# print nbRecords
|
||||
|
@ -210,7 +257,7 @@ class TopazBook:
|
|||
title = ''
|
||||
if 'Title' in self.bookMetadata:
|
||||
title = self.bookMetadata['Title']
|
||||
return title
|
||||
return title.decode('utf-8')
|
||||
|
||||
def setBookKey(self, key):
|
||||
self.bookKey = key
|
||||
|
@ -223,13 +270,13 @@ class TopazBook:
|
|||
try:
|
||||
recordOffset = self.bookHeaderRecords[name][index][0]
|
||||
except:
|
||||
raise TpzDRMError("Parse Error : Invalid Record, record not found")
|
||||
raise DrmException("Parse Error : Invalid Record, record not found")
|
||||
|
||||
self.fo.seek(self.bookPayloadOffset + recordOffset)
|
||||
|
||||
tag = bookReadString(self.fo)
|
||||
if tag != name :
|
||||
raise TpzDRMError("Parse Error : Invalid Record, record name doesn't match")
|
||||
raise DrmException("Parse Error : Invalid Record, record name doesn't match")
|
||||
|
||||
recordIndex = bookReadEncodedNumber(self.fo)
|
||||
if recordIndex < 0 :
|
||||
|
@ -237,7 +284,7 @@ class TopazBook:
|
|||
recordIndex = -recordIndex -1
|
||||
|
||||
if recordIndex != index :
|
||||
raise TpzDRMError("Parse Error : Invalid Record, index doesn't match")
|
||||
raise DrmException("Parse Error : Invalid Record, index doesn't match")
|
||||
|
||||
if (self.bookHeaderRecords[name][index][2] > 0):
|
||||
compressed = True
|
||||
|
@ -250,7 +297,7 @@ class TopazBook:
|
|||
ctx = topazCryptoInit(self.bookKey)
|
||||
record = topazCryptoDecrypt(record,ctx)
|
||||
else :
|
||||
raise TpzDRMError("Error: Attempt to decrypt without bookKey")
|
||||
raise DrmException("Error: Attempt to decrypt without bookKey")
|
||||
|
||||
if compressed:
|
||||
record = zlib.decompress(record)
|
||||
|
@ -262,12 +309,12 @@ class TopazBook:
|
|||
fixedimage=True
|
||||
try:
|
||||
keydata = self.getBookPayloadRecord('dkey', 0)
|
||||
except TpzDRMError, e:
|
||||
print "no dkey record found, book may not be encrypted"
|
||||
print "attempting to extrct files without a book key"
|
||||
except DrmException, e:
|
||||
print u"no dkey record found, book may not be encrypted"
|
||||
print u"attempting to extrct files without a book key"
|
||||
self.createBookDirectory()
|
||||
self.extractFiles()
|
||||
print "Successfully Extracted Topaz contents"
|
||||
print u"Successfully Extracted Topaz contents"
|
||||
if inCalibre:
|
||||
from calibre_plugins.k4mobidedrm import genbook
|
||||
else:
|
||||
|
@ -275,7 +322,7 @@ class TopazBook:
|
|||
|
||||
rv = genbook.generateBook(self.outdir, raw, fixedimage)
|
||||
if rv == 0:
|
||||
print "\nBook Successfully generated"
|
||||
print u"Book Successfully generated."
|
||||
return rv
|
||||
|
||||
# try each pid to decode the file
|
||||
|
@ -283,25 +330,25 @@ class TopazBook:
|
|||
for pid in pidlst:
|
||||
# use 8 digit pids here
|
||||
pid = pid[0:8]
|
||||
print "\nTrying: ", pid
|
||||
print u"Trying: {0}".format(pid)
|
||||
bookKeys = []
|
||||
data = keydata
|
||||
try:
|
||||
bookKeys+=decryptDkeyRecords(data,pid)
|
||||
except TpzDRMError, e:
|
||||
except DrmException, e:
|
||||
pass
|
||||
else:
|
||||
bookKey = bookKeys[0]
|
||||
print "Book Key Found!"
|
||||
print u"Book Key Found! ({0})".format(bookKey.encode('hex'))
|
||||
break
|
||||
|
||||
if not bookKey:
|
||||
raise TpzDRMError("Topaz Book. No key found in " + str(len(pidlst)) + " keys tried. Read the FAQs at Alf's blog. Only if none apply, report this failure for help.")
|
||||
raise DrmException(u"No key found in {0:d} keys tried. Read the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(len(pidlst)))
|
||||
|
||||
self.setBookKey(bookKey)
|
||||
self.createBookDirectory()
|
||||
self.extractFiles()
|
||||
print "Successfully Extracted Topaz contents"
|
||||
print u"Successfully Extracted Topaz contents"
|
||||
if inCalibre:
|
||||
from calibre_plugins.k4mobidedrm import genbook
|
||||
else:
|
||||
|
@ -309,7 +356,7 @@ class TopazBook:
|
|||
|
||||
rv = genbook.generateBook(self.outdir, raw, fixedimage)
|
||||
if rv == 0:
|
||||
print "\nBook Successfully generated"
|
||||
print u"Book Successfully generated"
|
||||
return rv
|
||||
|
||||
def createBookDirectory(self):
|
||||
|
@ -317,16 +364,16 @@ class TopazBook:
|
|||
# create output directory structure
|
||||
if not os.path.exists(outdir):
|
||||
os.makedirs(outdir)
|
||||
destdir = os.path.join(outdir,'img')
|
||||
destdir = os.path.join(outdir,u"img")
|
||||
if not os.path.exists(destdir):
|
||||
os.makedirs(destdir)
|
||||
destdir = os.path.join(outdir,'color_img')
|
||||
destdir = os.path.join(outdir,u"color_img")
|
||||
if not os.path.exists(destdir):
|
||||
os.makedirs(destdir)
|
||||
destdir = os.path.join(outdir,'page')
|
||||
destdir = os.path.join(outdir,u"page")
|
||||
if not os.path.exists(destdir):
|
||||
os.makedirs(destdir)
|
||||
destdir = os.path.join(outdir,'glyphs')
|
||||
destdir = os.path.join(outdir,u"glyphs")
|
||||
if not os.path.exists(destdir):
|
||||
os.makedirs(destdir)
|
||||
|
||||
|
@ -334,149 +381,148 @@ class TopazBook:
|
|||
outdir = self.outdir
|
||||
for headerRecord in self.bookHeaderRecords:
|
||||
name = headerRecord
|
||||
if name != "dkey" :
|
||||
ext = '.dat'
|
||||
if name == 'img' : ext = '.jpg'
|
||||
if name == 'color' : ext = '.jpg'
|
||||
print "\nProcessing Section: %s " % name
|
||||
if name != 'dkey':
|
||||
ext = u".dat"
|
||||
if name == 'img': ext = u".jpg"
|
||||
if name == 'color' : ext = u".jpg"
|
||||
print u"Processing Section: {0}\n. . .".format(name),
|
||||
for index in range (0,len(self.bookHeaderRecords[name])) :
|
||||
fnum = "%04d" % index
|
||||
fname = name + fnum + ext
|
||||
fname = u"{0}{1:04d}{2}".format(name,index,ext)
|
||||
destdir = outdir
|
||||
if name == 'img':
|
||||
destdir = os.path.join(outdir,'img')
|
||||
destdir = os.path.join(outdir,u"img")
|
||||
if name == 'color':
|
||||
destdir = os.path.join(outdir,'color_img')
|
||||
destdir = os.path.join(outdir,u"color_img")
|
||||
if name == 'page':
|
||||
destdir = os.path.join(outdir,'page')
|
||||
destdir = os.path.join(outdir,u"page")
|
||||
if name == 'glyphs':
|
||||
destdir = os.path.join(outdir,'glyphs')
|
||||
destdir = os.path.join(outdir,u"glyphs")
|
||||
outputFile = os.path.join(destdir,fname)
|
||||
print ".",
|
||||
print u".",
|
||||
record = self.getBookPayloadRecord(name,index)
|
||||
if record != '':
|
||||
file(outputFile, 'wb').write(record)
|
||||
print " "
|
||||
print u" "
|
||||
|
||||
def getHTMLZip(self, zipname):
|
||||
def getFile(self, zipname):
|
||||
htmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
htmlzip.write(os.path.join(self.outdir,'book.html'),'book.html')
|
||||
htmlzip.write(os.path.join(self.outdir,'book.opf'),'book.opf')
|
||||
if os.path.isfile(os.path.join(self.outdir,'cover.jpg')):
|
||||
htmlzip.write(os.path.join(self.outdir,'cover.jpg'),'cover.jpg')
|
||||
htmlzip.write(os.path.join(self.outdir,'style.css'),'style.css')
|
||||
zipUpDir(htmlzip, self.outdir, 'img')
|
||||
htmlzip.write(os.path.join(self.outdir,u"book.html"),u"book.html")
|
||||
htmlzip.write(os.path.join(self.outdir,u"book.opf"),u"book.opf")
|
||||
if os.path.isfile(os.path.join(self.outdir,u"cover.jpg")):
|
||||
htmlzip.write(os.path.join(self.outdir,u"cover.jpg"),u"cover.jpg")
|
||||
htmlzip.write(os.path.join(self.outdir,u"style.css"),u"style.css")
|
||||
zipUpDir(htmlzip, self.outdir, u"img")
|
||||
htmlzip.close()
|
||||
|
||||
def getBookType(self):
|
||||
return u"Topaz"
|
||||
|
||||
def getBookExtension(self):
|
||||
return u".htmlz"
|
||||
|
||||
def getSVGZip(self, zipname):
|
||||
svgzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
svgzip.write(os.path.join(self.outdir,'index_svg.xhtml'),'index_svg.xhtml')
|
||||
zipUpDir(svgzip, self.outdir, 'svg')
|
||||
zipUpDir(svgzip, self.outdir, 'img')
|
||||
svgzip.write(os.path.join(self.outdir,u"index_svg.xhtml"),u"index_svg.xhtml")
|
||||
zipUpDir(svgzip, self.outdir, u"svg")
|
||||
zipUpDir(svgzip, self.outdir, u"img")
|
||||
svgzip.close()
|
||||
|
||||
def getXMLZip(self, zipname):
|
||||
xmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
targetdir = os.path.join(self.outdir,'xml')
|
||||
zipUpDir(xmlzip, targetdir, '')
|
||||
zipUpDir(xmlzip, self.outdir, 'img')
|
||||
xmlzip.close()
|
||||
|
||||
def cleanup(self):
|
||||
if os.path.isdir(self.outdir):
|
||||
shutil.rmtree(self.outdir, True)
|
||||
|
||||
def usage(progname):
|
||||
print "Removes DRM protection from Topaz ebooks and extract the contents"
|
||||
print "Usage:"
|
||||
print " %s [-k <kindle.info>] [-p <pidnums>] [-s <kindleSerialNumbers>] <infile> <outdir> " % progname
|
||||
|
||||
print u"Removes DRM protection from Topaz ebooks and extracts the contents"
|
||||
print u"Usage:"
|
||||
print u" {0} [-k <kindle.info>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] <infile> <outdir>".format(progname)
|
||||
|
||||
# Main
|
||||
def main(argv=sys.argv):
|
||||
global buildXML
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
k4 = False
|
||||
pids = []
|
||||
serials = []
|
||||
kInfoFiles = []
|
||||
print u"TopazExtract v{0}.".format(__version__)
|
||||
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:")
|
||||
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:x")
|
||||
except getopt.GetoptError, err:
|
||||
print str(err)
|
||||
print u"Error in options or arguments: {0}".format(err.args[0])
|
||||
usage(progname)
|
||||
return 1
|
||||
if len(args)<2:
|
||||
usage(progname)
|
||||
return 1
|
||||
|
||||
for o, a in opts:
|
||||
if o == "-k":
|
||||
if a == None :
|
||||
print "Invalid parameter for -k"
|
||||
return 1
|
||||
kInfoFiles.append(a)
|
||||
if o == "-p":
|
||||
if a == None :
|
||||
print "Invalid parameter for -p"
|
||||
return 1
|
||||
pids = a.split(',')
|
||||
if o == "-s":
|
||||
if a == None :
|
||||
print "Invalid parameter for -s"
|
||||
return 1
|
||||
serials = a.split(',')
|
||||
k4 = True
|
||||
|
||||
infile = args[0]
|
||||
outdir = args[1]
|
||||
|
||||
if not os.path.isfile(infile):
|
||||
print "Input File Does Not Exist"
|
||||
print u"Input File {0} Does Not Exist.".format(infile)
|
||||
return 1
|
||||
|
||||
if not os.path.exists(outdir):
|
||||
print u"Output Directory {0} Does Not Exist.".format(outdir)
|
||||
return 1
|
||||
|
||||
kInfoFiles = []
|
||||
serials = []
|
||||
pids = []
|
||||
|
||||
for o, a in opts:
|
||||
if o == '-k':
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -k")
|
||||
kInfoFiles.append(a)
|
||||
if o == '-p':
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -p")
|
||||
pids = a.split(',')
|
||||
if o == '-s':
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -s")
|
||||
serials = [serial.replace(" ","") for serial in a.split(',')]
|
||||
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
|
||||
tb = TopazBook(infile)
|
||||
title = tb.getBookTitle()
|
||||
print "Processing Book: ", title
|
||||
keysRecord, keysRecordRecord = tb.getPIDMetaInfo()
|
||||
pids.extend(kgenpids.getPidList(keysRecord, keysRecordRecord, k4, serials, kInfoFiles))
|
||||
print u"Processing Book: {0}".format(title)
|
||||
md1, md2 = tb.getPIDMetaInfo()
|
||||
pids.extend(kgenpids.getPidList(md1, md2, serials, kInfoFiles))
|
||||
|
||||
try:
|
||||
print "Decrypting Book"
|
||||
print u"Decrypting Book"
|
||||
tb.processBook(pids)
|
||||
|
||||
print " Creating HTML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_nodrm' + '.htmlz')
|
||||
tb.getHTMLZip(zipname)
|
||||
print u" Creating HTML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + u"_nodrm.htmlz")
|
||||
tb.getFile(zipname)
|
||||
|
||||
print " Creating SVG ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_SVG' + '.zip')
|
||||
print u" Creating SVG ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + u"_SVG.zip")
|
||||
tb.getSVGZip(zipname)
|
||||
|
||||
if buildXML:
|
||||
print " Creating XML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_XML' + '.zip')
|
||||
tb.getXMLZip(zipname)
|
||||
|
||||
# removing internal temporary directory of pieces
|
||||
tb.cleanup()
|
||||
|
||||
except TpzDRMError, e:
|
||||
print str(e)
|
||||
# tb.cleanup()
|
||||
except DrmException, e:
|
||||
print u"Decryption failed\n{0}".format(traceback.format_exc())
|
||||
|
||||
try:
|
||||
tb.cleanup()
|
||||
except:
|
||||
pass
|
||||
return 1
|
||||
|
||||
except Exception, e:
|
||||
print str(e)
|
||||
# tb.cleanup
|
||||
print u"Decryption failed\m{0}".format(traceback.format_exc())
|
||||
try:
|
||||
tb.cleanup()
|
||||
except:
|
||||
pass
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
sys.exit(main())
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import zlib
|
||||
|
@ -27,14 +28,10 @@ class fixZip:
|
|||
self.ztype = 'zip'
|
||||
if zinput.lower().find('.epub') >= 0 :
|
||||
self.ztype = 'epub'
|
||||
print "opening input"
|
||||
self.inzip = zipfilerugged.ZipFile(zinput,'r')
|
||||
print "opening outout"
|
||||
self.outzip = zipfilerugged.ZipFile(zoutput,'w')
|
||||
print "opening input as raw file"
|
||||
# open the input zip for reading only as a raw file
|
||||
self.bzf = file(zinput,'rb')
|
||||
print "finished initialising"
|
||||
|
||||
def getlocalname(self, zi):
|
||||
local_header_offset = zi.header_offset
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
ReadMe_DeDRM_v5.4.1_WinApp
|
||||
-----------------------
|
||||
ReadMe_DeDRM_v5.5_WinApp
|
||||
========================
|
||||
|
||||
DeDRM_v5.4.1_WinApp is a pure python drag and drop application that allows users to drag and drop ebooks or folders of ebooks onto the DeDRM_Drop_Target to have the DRM removed. It repackages the"tools" python software in one easy to use program that remembers preferences and settings.
|
||||
DeDRM_v5.5_WinApp is a pure python drag and drop application that allows users to drag and drop ebooks or folders of ebooks onto the DeDRM_Drop_Target to have the DRM removed. It repackages all the "tools" python software in one easy to use program that remembers preferences and settings.
|
||||
|
||||
It should work out of the box with Kindle for PC ebooks and Adobe Adept epub and pdf ebooks.
|
||||
It will work without manual configuration for Kindle for PC ebooks and Adobe Adept epub and pdf ebooks.
|
||||
|
||||
To remove the DRM from standalone Kindle ebooks, eReader pdb ebooks, Barnes and Noble epubs, and Mobipocket ebooks requires the user to double-click the DeDRM_Drop_Target and set some additional Preferences including:
|
||||
|
||||
|
@ -16,14 +16,16 @@ Once these preferences have been set, the user can simply drag and drop ebooks o
|
|||
|
||||
This program requires that a 32 bit version of Python 2.X (tested with Python 2.5 through Python 2.7) and PyCrypto be installed on your computer before it will work. See below for where to get theese programs for Windows.
|
||||
|
||||
NB Although the individual scripts have been updated to work with unicode file names, the Windows DeDRM script has not yet been updated for technical reasons. Therefore, if you try to use it with paths or file names that contain non-ASCII characters, it might not work.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
0. If you don't already have a correct version of Python and PyCrypto installed, follow the "Installing Python on Windows" and "Installing PyCrypto on Windows" sections below before continuing.
|
||||
|
||||
1. Drag the DeDRM_5.4.1 folder from tools_v5.4.1/DeDRM_Applications/Windows to your "My Documents" folder.
|
||||
1. Drag the DeDRM_5.5 folder from tools_v5.5/DeDRM_Applications/Windows to your "My Documents" folder.
|
||||
|
||||
2. Open the DeDRM_5.4.1 folder you've just dragged, and make a short-cut of the DeDRM_Drop_Target.bat file (right-click/Create Shortcut). Drag the shortcut file onto your Desktop.
|
||||
2. Open the DeDRM_5.5 folder you've just dragged, and make a short-cut of the DeDRM_Drop_Target.bat file (right-click/Create Shortcut). Drag the shortcut file onto your Desktop.
|
||||
|
||||
3. To set the preferences simply double-click on your just created short-cut.
|
||||
|
||||
|
|
|
@ -1,217 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# This is a simple tool to identify all Amazon Topaz ebooks in a specific directory.
|
||||
# There always seems to be confusion since Topaz books downloaded to K4PC/Mac can have
|
||||
# almost any extension (.azw, .azw1, .prc, tpz). While the .azw1 and .tpz extensions
|
||||
# are fairly easy to indentify, the others are not (without opening the files in an editor).
|
||||
|
||||
# To run the tool with the GUI frontend, just double-click on the 'FindTopazFiles.pyw' file
|
||||
# and select the folder where all of the ebooks in question are located. Then click 'Search'.
|
||||
# The program will list the file names of the ebooks that are indentified as being Topaz.
|
||||
# You can then isolate those books and use the Topaz tools to decrypt and convert them.
|
||||
|
||||
# You can also run the script from a command line... supplying the folder to search
|
||||
# as a parameter: python FindTopazEbooks.pyw "C:\My Folder" (change appropriately for
|
||||
# your particular O.S.)
|
||||
|
||||
# ** NOTE: This program does NOT decrypt or modify Topaz files in any way. It simply identifies them.
|
||||
|
||||
# PLEASE DO NOT PIRATE EBOOKS!
|
||||
|
||||
# We want all authors and publishers, and eBook stores to live
|
||||
# long and prosperous lives but at the same time we just want to
|
||||
# be able to read OUR books on whatever device we want and to keep
|
||||
# readable for a long, long time
|
||||
|
||||
# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
|
||||
# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates
|
||||
# and many many others
|
||||
|
||||
# Revision history:
|
||||
# 1 - Initial release.
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
import sys
|
||||
import os
|
||||
os.environ['PYTHONIOENCODING'] = "utf-8"
|
||||
import re
|
||||
import shutil
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
|
||||
|
||||
class ScrolledText(Tkinter.Text):
|
||||
def __init__(self, master=None, **kw):
|
||||
self.frame = Tkinter.Frame(master)
|
||||
self.vbar = Tkinter.Scrollbar(self.frame)
|
||||
self.vbar.pack(side=Tkconstants.RIGHT, fill=Tkconstants.Y)
|
||||
kw.update({'yscrollcommand': self.vbar.set})
|
||||
Tkinter.Text.__init__(self, self.frame, **kw)
|
||||
self.pack(side=Tkconstants.LEFT, fill=Tkconstants.BOTH, expand=True)
|
||||
self.vbar['command'] = self.yview
|
||||
# Copy geometry methods of self.frame without overriding Text
|
||||
# methods = hack!
|
||||
text_meths = vars(Tkinter.Text).keys()
|
||||
methods = vars(Tkinter.Pack).keys() + vars(Tkinter.Grid).keys() + vars(Tkinter.Place).keys()
|
||||
methods = set(methods).difference(text_meths)
|
||||
for m in methods:
|
||||
if m[0] != '_' and m != 'config' and m != 'configure':
|
||||
setattr(self, m, getattr(self.frame, m))
|
||||
|
||||
def __str__(self):
|
||||
return str(self.frame)
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv, obj=None):
|
||||
progname = os.path.basename(argv[0])
|
||||
if len(argv) != 2:
|
||||
print "usage: %s DIRECTORY" % (progname,)
|
||||
return 1
|
||||
|
||||
if obj == None:
|
||||
print "\nTopaz search results:\n"
|
||||
else:
|
||||
obj.stext.insert(Tkconstants.END,"Topaz search results:\n\n")
|
||||
|
||||
inpath = argv[1]
|
||||
files = os.listdir(inpath)
|
||||
filefilter = re.compile("(\.azw$)|(\.azw1$)|(\.prc$)|(\.tpz$)", re.IGNORECASE)
|
||||
files = filter(filefilter.search, files)
|
||||
|
||||
if files:
|
||||
topazcount = 0
|
||||
totalcount = 0
|
||||
for filename in files:
|
||||
with open(os.path.join(inpath, filename), 'rb') as f:
|
||||
try:
|
||||
if f.read().startswith('TPZ'):
|
||||
f.close()
|
||||
basename, extension = os.path.splitext(filename)
|
||||
if obj == None:
|
||||
print " %s is a Topaz formatted ebook." % filename
|
||||
"""
|
||||
if extension == '.azw' or extension == '.prc':
|
||||
print " renaming to %s" % (basename + '.tpz')
|
||||
shutil.move(os.path.join(inpath, filename),
|
||||
os.path.join(inpath, basename + '.tpz'))
|
||||
"""
|
||||
else:
|
||||
msg1 = " %s is a Topaz formatted ebook.\n" % filename
|
||||
obj.stext.insert(Tkconstants.END,msg1)
|
||||
"""
|
||||
if extension == '.azw' or extension == '.prc':
|
||||
msg2 = " renaming to %s\n" % (basename + '.tpz')
|
||||
obj.stext.insert(Tkconstants.END,msg2)
|
||||
shutil.move(os.path.join(inpath, filename),
|
||||
os.path.join(inpath, basename + '.tpz'))
|
||||
"""
|
||||
topazcount += 1
|
||||
except:
|
||||
if obj == None:
|
||||
print " Error reading %s." % filename
|
||||
else:
|
||||
msg = " Error reading or %s.\n" % filename
|
||||
obj.stext.insert(Tkconstants.END,msg)
|
||||
pass
|
||||
totalcount += 1
|
||||
if topazcount == 0:
|
||||
if obj == None:
|
||||
print "\nNo Topaz books found in %s." % inpath
|
||||
else:
|
||||
msg = "\nNo Topaz books found in %s.\n\n" % inpath
|
||||
obj.stext.insert(Tkconstants.END,msg)
|
||||
else:
|
||||
if obj == None:
|
||||
print "\n%i Topaz books found in %s\n%i total books checked.\n" % (topazcount, inpath, totalcount)
|
||||
else:
|
||||
msg = "\n%i Topaz books found in %s\n%i total books checked.\n\n" %(topazcount, inpath, totalcount)
|
||||
obj.stext.insert(Tkconstants.END,msg)
|
||||
else:
|
||||
if obj == None:
|
||||
print "No typical Topaz file extensions found in %s.\n" % inpath
|
||||
else:
|
||||
msg = "No typical Topaz file extensions found in %s.\n\n" % inpath
|
||||
obj.stext.insert(Tkconstants.END,msg)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
ltext='Search a directory for Topaz eBooks\n'
|
||||
self.status = Tkinter.Label(self, text=ltext)
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
Tkinter.Label(body, text='Directory to Search').grid(row=1)
|
||||
self.inpath = Tkinter.Entry(body, width=30)
|
||||
self.inpath.grid(row=1, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
msg1 = 'Topaz search results \n\n'
|
||||
self.stext = ScrolledText(body, bd=5, relief=Tkconstants.RIDGE,
|
||||
height=15, width=60, wrap=Tkconstants.WORD)
|
||||
self.stext.grid(row=4, column=0, columnspan=2,sticky=sticky)
|
||||
#self.stext.insert(Tkconstants.END,msg1)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
|
||||
|
||||
self.botton = Tkinter.Button(
|
||||
buttons, text="Search", width=10, command=self.search)
|
||||
self.botton.pack(side=Tkconstants.LEFT)
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
self.button = Tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quit)
|
||||
self.button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
def get_inpath(self):
|
||||
cwd = os.getcwdu()
|
||||
cwd = cwd.encode('utf-8')
|
||||
inpath = tkFileDialog.askdirectory(
|
||||
parent=None, title='Directory to search',
|
||||
initialdir=cwd, initialfile=None)
|
||||
if inpath:
|
||||
inpath = os.path.normpath(inpath)
|
||||
self.inpath.delete(0, Tkconstants.END)
|
||||
self.inpath.insert(0, inpath)
|
||||
return
|
||||
|
||||
|
||||
def search(self):
|
||||
inpath = self.inpath.get()
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = 'Specified directory does not exist'
|
||||
return
|
||||
argv = [sys.argv[0], inpath]
|
||||
self.status['text'] = 'Searching...'
|
||||
self.botton.configure(state='disabled')
|
||||
cli_main(argv, self)
|
||||
self.status['text'] = 'Search a directory for Topaz files'
|
||||
self.botton.configure(state='normal')
|
||||
|
||||
return
|
||||
|
||||
|
||||
def gui_main():
|
||||
root = Tkinter.Tk()
|
||||
root.title('Topaz eBook Finder')
|
||||
root.resizable(True, False)
|
||||
root.minsize(370, 0)
|
||||
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
|
@ -1,146 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
|
||||
import sys
|
||||
sys.path.append('lib')
|
||||
import os, os.path, urllib
|
||||
os.environ['PYTHONIOENCODING'] = "utf-8"
|
||||
import subprocess
|
||||
from subprocess import Popen, PIPE, STDOUT
|
||||
import subasyncio
|
||||
from subasyncio import Process
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
from scrolltextwidget import ScrolledText
|
||||
|
||||
class MainDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.root = root
|
||||
self.interval = 2000
|
||||
self.p2 = None
|
||||
self.status = Tkinter.Label(self, text='Find your Kindle PID')
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
|
||||
Tkinter.Label(body, text='Kindle Serial # or iPhone UDID').grid(row=1, sticky=Tkconstants.E)
|
||||
self.serialnum = Tkinter.StringVar()
|
||||
self.serialinfo = Tkinter.Entry(body, width=45, textvariable=self.serialnum)
|
||||
self.serialinfo.grid(row=1, column=1, sticky=sticky)
|
||||
|
||||
msg1 = 'Conversion Log \n\n'
|
||||
self.stext = ScrolledText(body, bd=5, relief=Tkconstants.RIDGE, height=15, width=60, wrap=Tkconstants.WORD)
|
||||
self.stext.grid(row=3, column=0, columnspan=2,sticky=sticky)
|
||||
self.stext.insert(Tkconstants.END,msg1)
|
||||
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
self.sbotton = Tkinter.Button(
|
||||
buttons, text="Start", width=10, command=self.convertit)
|
||||
self.sbotton.pack(side=Tkconstants.LEFT)
|
||||
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
self.qbutton = Tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quitting)
|
||||
self.qbutton.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
# read from subprocess pipe without blocking
|
||||
# invoked every interval via the widget "after"
|
||||
# option being used, so need to reset it for the next time
|
||||
def processPipe(self):
|
||||
poll = self.p2.wait('nowait')
|
||||
if poll != None:
|
||||
text = self.p2.readerr()
|
||||
text += self.p2.read()
|
||||
msg = text + '\n\n' + 'Kindle PID Successfully Determined\n'
|
||||
if poll != 0:
|
||||
msg = text + '\n\n' + 'Error: Kindle PID Failed\n'
|
||||
self.showCmdOutput(msg)
|
||||
self.p2 = None
|
||||
self.sbotton.configure(state='normal')
|
||||
return
|
||||
text = self.p2.readerr()
|
||||
text += self.p2.read()
|
||||
self.showCmdOutput(text)
|
||||
# make sure we get invoked again by event loop after interval
|
||||
self.stext.after(self.interval,self.processPipe)
|
||||
return
|
||||
|
||||
# post output from subprocess in scrolled text widget
|
||||
def showCmdOutput(self, msg):
|
||||
if msg and msg !='':
|
||||
if sys.platform.startswith('win'):
|
||||
msg = msg.replace('\r\n','\n')
|
||||
self.stext.insert(Tkconstants.END,msg)
|
||||
self.stext.yview_pickplace(Tkconstants.END)
|
||||
return
|
||||
|
||||
# run as a subprocess via pipes and collect stdout
|
||||
def pidrdr(self, serial):
|
||||
# os.putenv('PYTHONUNBUFFERED', '1')
|
||||
pengine = sys.executable
|
||||
if pengine is None or pengine == '':
|
||||
pengine = "python"
|
||||
pengine = os.path.normpath(pengine)
|
||||
cmdline = pengine + ' ./lib/kindlepid.py "' + serial + '"'
|
||||
if sys.platform[0:3] == 'win':
|
||||
# search_path = os.environ['PATH']
|
||||
# search_path = search_path.lower()
|
||||
# if search_path.find('python') >= 0:
|
||||
# cmdline = 'python lib\kindlepid.py "' + serial + '"'
|
||||
# else :
|
||||
# cmdline = 'lib\kindlepid.py "' + serial + '"'
|
||||
cmdline = pengine + ' lib\\kindlepid.py "' + serial + '"'
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p2 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False)
|
||||
return p2
|
||||
|
||||
def quitting(self):
|
||||
# kill any still running subprocess
|
||||
if self.p2 != None:
|
||||
if (self.p2.wait('nowait') == None):
|
||||
self.p2.terminate()
|
||||
self.root.destroy()
|
||||
|
||||
# actually ready to run the subprocess and get its output
|
||||
def convertit(self):
|
||||
# now disable the button to prevent multiple launches
|
||||
self.sbotton.configure(state='disabled')
|
||||
serial = self.serialinfo.get()
|
||||
if not serial or serial == '':
|
||||
self.status['text'] = 'No Kindle Serial Number or iPhone UDID specified'
|
||||
self.sbotton.configure(state='normal')
|
||||
return
|
||||
|
||||
log = 'Command = "python kindlepid.py"\n'
|
||||
log += 'Serial = "' + serial + '"\n'
|
||||
log += '\n\n'
|
||||
log += 'Please Wait ...\n\n'
|
||||
self.stext.insert(Tkconstants.END,log)
|
||||
self.p2 = self.pidrdr(serial)
|
||||
|
||||
# python does not seem to allow you to create
|
||||
# your own eventloop which every other gui does - strange
|
||||
# so need to use the widget "after" command to force
|
||||
# event loop to run non-gui events every interval
|
||||
self.stext.after(self.interval,self.processPipe)
|
||||
return
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
root = Tkinter.Tk()
|
||||
root.title('Kindle and iPhone PID Calculator')
|
||||
root.resizable(True, False)
|
||||
root.minsize(300, 0)
|
||||
MainDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
|
@ -1,170 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
|
||||
import sys
|
||||
sys.path.append('lib')
|
||||
import os, os.path, urllib
|
||||
import subprocess
|
||||
from subprocess import Popen, PIPE, STDOUT
|
||||
import subasyncio
|
||||
from subasyncio import Process
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
from scrolltextwidget import ScrolledText
|
||||
|
||||
class MainDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.root = root
|
||||
self.interval = 2000
|
||||
self.p2 = None
|
||||
self.status = Tkinter.Label(self, text='Fix Encrypted Mobi eBooks so the Kindle can read them')
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
|
||||
Tkinter.Label(body, text='Mobi eBook input file').grid(row=0, sticky=Tkconstants.E)
|
||||
self.mobipath = Tkinter.Entry(body, width=50)
|
||||
self.mobipath.grid(row=0, column=1, sticky=sticky)
|
||||
cwd = os.getcwdu()
|
||||
cwd = cwd.encode('utf-8')
|
||||
self.mobipath.insert(0, cwd)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_mobipath)
|
||||
button.grid(row=0, column=2)
|
||||
|
||||
Tkinter.Label(body, text='10 Character PID').grid(row=1, sticky=Tkconstants.E)
|
||||
self.pidnum = Tkinter.StringVar()
|
||||
self.pidinfo = Tkinter.Entry(body, width=12, textvariable=self.pidnum)
|
||||
self.pidinfo.grid(row=1, column=1, sticky=sticky)
|
||||
|
||||
msg1 = 'Conversion Log \n\n'
|
||||
self.stext = ScrolledText(body, bd=5, relief=Tkconstants.RIDGE, height=15, width=60, wrap=Tkconstants.WORD)
|
||||
self.stext.grid(row=2, column=0, columnspan=2,sticky=sticky)
|
||||
self.stext.insert(Tkconstants.END,msg1)
|
||||
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
self.sbotton = Tkinter.Button(
|
||||
buttons, text="Start", width=10, command=self.convertit)
|
||||
self.sbotton.pack(side=Tkconstants.LEFT)
|
||||
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
self.qbutton = Tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quitting)
|
||||
self.qbutton.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
# read from subprocess pipe without blocking
|
||||
# invoked every interval via the widget "after"
|
||||
# option being used, so need to reset it for the next time
|
||||
def processPipe(self):
|
||||
poll = self.p2.wait('nowait')
|
||||
if poll != None:
|
||||
text = self.p2.readerr()
|
||||
text += self.p2.read()
|
||||
msg = text + '\n\n' + 'Fix for Kindle successful\n'
|
||||
if poll != 0:
|
||||
msg = text + '\n\n' + 'Error: Fix for Kindle Failed\n'
|
||||
self.showCmdOutput(msg)
|
||||
self.p2 = None
|
||||
self.sbotton.configure(state='normal')
|
||||
return
|
||||
text = self.p2.readerr()
|
||||
text += self.p2.read()
|
||||
self.showCmdOutput(text)
|
||||
# make sure we get invoked again by event loop after interval
|
||||
self.stext.after(self.interval,self.processPipe)
|
||||
return
|
||||
|
||||
# post output from subprocess in scrolled text widget
|
||||
def showCmdOutput(self, msg):
|
||||
if msg and msg !='':
|
||||
msg = msg.encode('utf-8')
|
||||
if sys.platform.startswith('win'):
|
||||
msg = msg.replace('\r\n','\n')
|
||||
self.stext.insert(Tkconstants.END,msg)
|
||||
self.stext.yview_pickplace(Tkconstants.END)
|
||||
return
|
||||
|
||||
# run as a subprocess via pipes and collect stdout
|
||||
def krdr(self, infile, pidnum):
|
||||
# os.putenv('PYTHONUNBUFFERED', '1')
|
||||
cmdline = 'python ./lib/kindlefix.py "' + infile + '" "' + pidnum + '"'
|
||||
if sys.platform[0:3] == 'win':
|
||||
search_path = os.environ['PATH']
|
||||
search_path = search_path.lower()
|
||||
if search_path.find('python') >= 0:
|
||||
cmdline = 'python lib\kindlefix.py "' + infile + '" "' + pidnum + '"'
|
||||
else :
|
||||
cmdline = 'lib\kindlefix.py "' + infile + '" "' + pidnum + '"'
|
||||
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p2 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False)
|
||||
return p2
|
||||
|
||||
|
||||
def get_mobipath(self):
|
||||
mobipath = tkFileDialog.askopenfilename(
|
||||
parent=None, title='Select Mobi eBook File',
|
||||
defaultextension='.prc', filetypes=[('Mobi eBook File', '.prc'), ('Mobi eBook File', '.mobi'),
|
||||
('All Files', '.*')])
|
||||
if mobipath:
|
||||
mobipath = os.path.normpath(mobipath)
|
||||
self.mobipath.delete(0, Tkconstants.END)
|
||||
self.mobipath.insert(0, mobipath)
|
||||
return
|
||||
|
||||
def quitting(self):
|
||||
# kill any still running subprocess
|
||||
if self.p2 != None:
|
||||
if (self.p2.wait('nowait') == None):
|
||||
self.p2.terminate()
|
||||
self.root.destroy()
|
||||
|
||||
# actually ready to run the subprocess and get its output
|
||||
def convertit(self):
|
||||
# now disable the button to prevent multiple launches
|
||||
self.sbotton.configure(state='disabled')
|
||||
mobipath = self.mobipath.get()
|
||||
pidnum = self.pidinfo.get()
|
||||
if not mobipath or not os.path.exists(mobipath):
|
||||
self.status['text'] = 'Specified Mobi eBook file does not exist'
|
||||
self.sbotton.configure(state='normal')
|
||||
return
|
||||
if not pidnum or pidnum == '':
|
||||
self.status['text'] = 'No PID specified'
|
||||
self.sbotton.configure(state='normal')
|
||||
return
|
||||
|
||||
log = 'Command = "python kindlefix.py"\n'
|
||||
log += 'Mobi Path = "'+ mobipath + '"\n'
|
||||
log += 'PID = "' + pidnum + '"\n'
|
||||
log += '\n\n'
|
||||
log += 'Please Wait ...\n\n'
|
||||
log = log.encode('utf-8')
|
||||
self.stext.insert(Tkconstants.END,log)
|
||||
self.p2 = self.krdr(mobipath, pidnum)
|
||||
|
||||
# python does not seem to allow you to create
|
||||
# your own eventloop which every other gui does - strange
|
||||
# so need to use the widget "after" command to force
|
||||
# event loop to run non-gui events every interval
|
||||
self.stext.after(self.interval,self.processPipe)
|
||||
return
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
root = Tkinter.Tk()
|
||||
root.title('Fix Encrypted Mobi eBooks to work with the Kindle')
|
||||
root.resizable(True, False)
|
||||
root.minsize(300, 0)
|
||||
MainDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
|
@ -1,203 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
|
||||
import sys
|
||||
sys.path.append('lib')
|
||||
import os, os.path, urllib
|
||||
os.environ['PYTHONIOENCODING'] = "utf-8"
|
||||
import subprocess
|
||||
from subprocess import Popen, PIPE, STDOUT
|
||||
import subasyncio
|
||||
from subasyncio import Process
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
from scrolltextwidget import ScrolledText
|
||||
|
||||
class MainDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.root = root
|
||||
self.interval = 2000
|
||||
self.p2 = None
|
||||
self.status = Tkinter.Label(self, text='Remove Encryption from a Mobi eBook')
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
|
||||
Tkinter.Label(body, text='Mobi eBook input file').grid(row=0, sticky=Tkconstants.E)
|
||||
self.mobipath = Tkinter.Entry(body, width=50)
|
||||
self.mobipath.grid(row=0, column=1, sticky=sticky)
|
||||
cwd = os.getcwdu()
|
||||
cwd = cwd.encode('utf-8')
|
||||
self.mobipath.insert(0, cwd)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_mobipath)
|
||||
button.grid(row=0, column=2)
|
||||
|
||||
Tkinter.Label(body, text='Name for Unencrypted Output File').grid(row=1, sticky=Tkconstants.E)
|
||||
self.outpath = Tkinter.Entry(body, width=50)
|
||||
self.outpath.grid(row=1, column=1, sticky=sticky)
|
||||
self.outpath.insert(0, '')
|
||||
button = Tkinter.Button(body, text="...", command=self.get_outpath)
|
||||
button.grid(row=1, column=2)
|
||||
|
||||
Tkinter.Label(body, text='10 Character PID').grid(row=2, sticky=Tkconstants.E)
|
||||
self.pidnum = Tkinter.StringVar()
|
||||
self.pidinfo = Tkinter.Entry(body, width=12, textvariable=self.pidnum)
|
||||
self.pidinfo.grid(row=2, column=1, sticky=sticky)
|
||||
|
||||
msg1 = 'Conversion Log \n\n'
|
||||
self.stext = ScrolledText(body, bd=5, relief=Tkconstants.RIDGE, height=15, width=60, wrap=Tkconstants.WORD)
|
||||
self.stext.grid(row=3, column=0, columnspan=2,sticky=sticky)
|
||||
self.stext.insert(Tkconstants.END,msg1)
|
||||
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
self.sbotton = Tkinter.Button(
|
||||
buttons, text="Start", width=10, command=self.convertit)
|
||||
self.sbotton.pack(side=Tkconstants.LEFT)
|
||||
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
self.qbutton = Tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quitting)
|
||||
self.qbutton.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
# read from subprocess pipe without blocking
|
||||
# invoked every interval via the widget "after"
|
||||
# option being used, so need to reset it for the next time
|
||||
def processPipe(self):
|
||||
poll = self.p2.wait('nowait')
|
||||
if poll != None:
|
||||
text = self.p2.readerr()
|
||||
text += self.p2.read()
|
||||
msg = text + '\n\n' + 'Encryption successfully removed\n'
|
||||
if poll != 0:
|
||||
msg = text + '\n\n' + 'Error: Encryption Removal Failed\n'
|
||||
self.showCmdOutput(msg)
|
||||
self.p2 = None
|
||||
self.sbotton.configure(state='normal')
|
||||
return
|
||||
text = self.p2.readerr()
|
||||
text += self.p2.read()
|
||||
self.showCmdOutput(text)
|
||||
# make sure we get invoked again by event loop after interval
|
||||
self.stext.after(self.interval,self.processPipe)
|
||||
return
|
||||
|
||||
# post output from subprocess in scrolled text widget
|
||||
def showCmdOutput(self, msg):
|
||||
if msg and msg !='':
|
||||
if sys.platform.startswith('win'):
|
||||
msg = msg.replace('\r\n','\n')
|
||||
self.stext.insert(Tkconstants.END,msg)
|
||||
self.stext.yview_pickplace(Tkconstants.END)
|
||||
return
|
||||
|
||||
# run as a subprocess via pipes and collect stdout
|
||||
def mobirdr(self, infile, outfile, pidnum):
|
||||
pengine = sys.executable
|
||||
if pengine is None or pengine == '':
|
||||
pengine = "python"
|
||||
pengine = os.path.normpath(pengine)
|
||||
# os.putenv('PYTHONUNBUFFERED', '1')
|
||||
cmdline = pengine + ' ./lib/mobidedrm.py "' + infile + '" "' + outfile + '" "' + pidnum + '"'
|
||||
if sys.platform[0:3] == 'win':
|
||||
# search_path = os.environ['PATH']
|
||||
# search_path = search_path.lower()
|
||||
# if search_path.find('python') >= 0:
|
||||
# cmdline = 'python lib\mobidedrm.py "' + infile + '" "' + outfile + '" "' + pidnum + '"'
|
||||
# else :
|
||||
# cmdline = 'lib\mobidedrm.py "' + infile + '" "' + outfile + '" "' + pidnum + '"'
|
||||
cmdline = pengine + ' lib\\mobidedrm.py "' + infile + '" "' + outfile + '" "' + pidnum + '"'
|
||||
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p2 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False)
|
||||
return p2
|
||||
|
||||
|
||||
def get_mobipath(self):
|
||||
mobipath = tkFileDialog.askopenfilename(
|
||||
parent=None, title='Select Mobi eBook File',
|
||||
defaultextension='.prc', filetypes=[('Mobi eBook File', '.prc'), ('Mobi eBook File', '.azw'),('Mobi eBook File', '.mobi'),
|
||||
('All Files', '.*')])
|
||||
if mobipath:
|
||||
mobipath = os.path.normpath(mobipath)
|
||||
self.mobipath.delete(0, Tkconstants.END)
|
||||
self.mobipath.insert(0, mobipath)
|
||||
return
|
||||
|
||||
def get_outpath(self):
|
||||
mobipath = self.mobipath.get()
|
||||
initname = os.path.basename(mobipath)
|
||||
p = initname.find('.')
|
||||
if p >= 0: initname = initname[0:p]
|
||||
initname += '_nodrm.mobi'
|
||||
outpath = tkFileDialog.asksaveasfilename(
|
||||
parent=None, title='Select Unencrypted Mobi File to produce',
|
||||
defaultextension='.mobi', initialfile=initname,
|
||||
filetypes=[('Mobi files', '.mobi'), ('All files', '.*')])
|
||||
if outpath:
|
||||
outpath = os.path.normpath(outpath)
|
||||
self.outpath.delete(0, Tkconstants.END)
|
||||
self.outpath.insert(0, outpath)
|
||||
return
|
||||
|
||||
def quitting(self):
|
||||
# kill any still running subprocess
|
||||
if self.p2 != None:
|
||||
if (self.p2.wait('nowait') == None):
|
||||
self.p2.terminate()
|
||||
self.root.destroy()
|
||||
|
||||
# actually ready to run the subprocess and get its output
|
||||
def convertit(self):
|
||||
# now disable the button to prevent multiple launches
|
||||
self.sbotton.configure(state='disabled')
|
||||
mobipath = self.mobipath.get()
|
||||
outpath = self.outpath.get()
|
||||
pidnum = self.pidinfo.get()
|
||||
if not mobipath or not os.path.exists(mobipath):
|
||||
self.status['text'] = 'Specified Mobi eBook file does not exist'
|
||||
self.sbotton.configure(state='normal')
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = 'No output file specified'
|
||||
self.sbotton.configure(state='normal')
|
||||
return
|
||||
if not pidnum or pidnum == '':
|
||||
self.status['text'] = 'No PID specified'
|
||||
self.sbotton.configure(state='normal')
|
||||
return
|
||||
|
||||
log = 'Command = "python mobidedrm.py"\n'
|
||||
log += 'Mobi Path = "'+ mobipath + '"\n'
|
||||
log += 'Output File = "' + outpath + '"\n'
|
||||
log += 'PID = "' + pidnum + '"\n'
|
||||
log += '\n\n'
|
||||
log += 'Please Wait ...\n\n'
|
||||
self.stext.insert(Tkconstants.END,log)
|
||||
self.p2 = self.mobirdr(mobipath, outpath, pidnum)
|
||||
|
||||
# python does not seem to allow you to create
|
||||
# your own eventloop which every other gui does - strange
|
||||
# so need to use the widget "after" command to force
|
||||
# event loop to run non-gui events every interval
|
||||
self.stext.after(self.interval,self.processPipe)
|
||||
return
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
root = Tkinter.Tk()
|
||||
root.title('Mobi eBook Encryption Removal')
|
||||
root.resizable(True, False)
|
||||
root.minsize(300, 0)
|
||||
MainDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
|
@ -1,172 +0,0 @@
|
|||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
def write(self, data):
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
|
||||
|
||||
import prc, struct
|
||||
from binascii import hexlify
|
||||
|
||||
def strByte(s,off=0):
|
||||
return struct.unpack(">B",s[off])[0];
|
||||
|
||||
def strSWord(s,off=0):
|
||||
return struct.unpack(">h",s[off:off+2])[0];
|
||||
|
||||
def strWord(s,off=0):
|
||||
return struct.unpack(">H",s[off:off+2])[0];
|
||||
|
||||
def strDWord(s,off=0):
|
||||
return struct.unpack(">L",s[off:off+4])[0];
|
||||
|
||||
def strPutDWord(s,off,i):
|
||||
return s[:off]+struct.pack(">L",i)+s[off+4:];
|
||||
|
||||
keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96"
|
||||
|
||||
#implementation of Pukall Cipher 1
|
||||
def PC1(key, src, decryption=True):
|
||||
sum1 = 0;
|
||||
sum2 = 0;
|
||||
keyXorVal = 0;
|
||||
if len(key)!=16:
|
||||
print "Bad key length!"
|
||||
return None
|
||||
wkey = []
|
||||
for i in xrange(8):
|
||||
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
|
||||
|
||||
dst = ""
|
||||
for i in xrange(len(src)):
|
||||
temp1 = 0;
|
||||
byteXorVal = 0;
|
||||
for j in xrange(8):
|
||||
temp1 ^= wkey[j]
|
||||
sum2 = (sum2+j)*20021 + sum1
|
||||
sum1 = (temp1*346)&0xFFFF
|
||||
sum2 = (sum2+sum1)&0xFFFF
|
||||
temp1 = (temp1*20021+1)&0xFFFF
|
||||
byteXorVal ^= temp1 ^ sum2
|
||||
|
||||
curByte = ord(src[i])
|
||||
if not decryption:
|
||||
keyXorVal = curByte * 257;
|
||||
curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
|
||||
if decryption:
|
||||
keyXorVal = curByte * 257;
|
||||
for j in xrange(8):
|
||||
wkey[j] ^= keyXorVal;
|
||||
|
||||
dst+=chr(curByte)
|
||||
|
||||
return dst
|
||||
|
||||
def find_key(rec0, pid):
|
||||
off1 = strDWord(rec0, 0xA8)
|
||||
if off1==0xFFFFFFFF or off1==0:
|
||||
print "No DRM"
|
||||
return None
|
||||
size1 = strDWord(rec0, 0xB0)
|
||||
cnt = strDWord(rec0, 0xAC)
|
||||
flag = strDWord(rec0, 0xB4)
|
||||
|
||||
temp_key = PC1(keyvec1, pid.ljust(16,'\0'), False)
|
||||
cksum = 0
|
||||
#print pid, "->", hexlify(temp_key)
|
||||
for i in xrange(len(temp_key)):
|
||||
cksum += ord(temp_key[i])
|
||||
cksum &= 0xFF
|
||||
temp_key = temp_key.ljust(16,'\0')
|
||||
#print "pid cksum: %02X"%cksum
|
||||
|
||||
#print "Key records: %02X-%02X, count: %d, flag: %02X"%(off1, off1+size1, cnt, flag)
|
||||
iOff = off1
|
||||
drm_key = None
|
||||
for i in xrange(cnt):
|
||||
dwCheck = strDWord(rec0, iOff)
|
||||
dwSize = strDWord(rec0, iOff+4)
|
||||
dwType = strDWord(rec0, iOff+8)
|
||||
nCksum = strByte(rec0, iOff+0xC)
|
||||
#print "Key record %d: check=%08X, size=%d, type=%d, cksum=%02X"%(i, dwCheck, dwSize, dwType, nCksum)
|
||||
if nCksum==cksum:
|
||||
drmInfo = PC1(temp_key, rec0[iOff+0x10:iOff+0x30])
|
||||
dw0, dw4, dw18, dw1c = struct.unpack(">II16xII", drmInfo)
|
||||
#print "Decrypted drmInfo:", "%08X, %08X, %s, %08X, %08X"%(dw0, dw4, hexlify(drmInfo[0x8:0x18]), dw18, dw1c)
|
||||
#print "Decrypted drmInfo:", hexlify(drmInfo)
|
||||
if dw0==dwCheck:
|
||||
print "Found the matching record; setting the CustomDRM flag for Kindle"
|
||||
drmInfo = strPutDWord(drmInfo,4,(dw4|0x800))
|
||||
dw0, dw4, dw18, dw1c = struct.unpack(">II16xII", drmInfo)
|
||||
#print "Updated drmInfo:", "%08X, %08X, %s, %08X, %08X"%(dw0, dw4, hexlify(drmInfo[0x8:0x18]), dw18, dw1c)
|
||||
return rec0[:iOff+0x10] + PC1(temp_key, drmInfo, False) + rec0[:iOff+0x30]
|
||||
iOff += dwSize
|
||||
return None
|
||||
|
||||
def replaceext(filename, newext):
|
||||
nameparts = filename.split(".")
|
||||
if len(nameparts)>1:
|
||||
return (".".join(nameparts[:-1]))+newext
|
||||
else:
|
||||
return nameparts[0]+newext
|
||||
|
||||
def main(argv=sys.argv):
|
||||
print "The Kindleizer v0.2. Copyright (c) 2007 Igor Skochinsky"
|
||||
if len(sys.argv) != 3:
|
||||
print "Fixes encrypted Mobipocket books to be readable by Kindle"
|
||||
print "Usage: kindlefix.py file.mobi PID"
|
||||
return 1
|
||||
fname = sys.argv[1]
|
||||
pid = sys.argv[2]
|
||||
if len(pid)==10 and pid[-3]=='*':
|
||||
pid = pid[:-2]
|
||||
if len(pid)!=8 or pid[-1]!='*':
|
||||
print "PID is not valid! (should be in format AAAAAAA*DD)"
|
||||
return 3
|
||||
db = prc.File(fname)
|
||||
#print dir(db)
|
||||
if db.getDBInfo()["creator"]!='MOBI':
|
||||
print "Not a Mobi file!"
|
||||
return 1
|
||||
rec0 = db.getRecord(0)[0]
|
||||
enc = strSWord(rec0, 0xC)
|
||||
print "Encryption:", enc
|
||||
if enc!=2:
|
||||
print "Unknown encryption type"
|
||||
return 1
|
||||
|
||||
if len(rec0)<0x28 or rec0[0x10:0x14] != 'MOBI':
|
||||
print "bad file format"
|
||||
return 1
|
||||
print "Mobi publication type:", strDWord(rec0, 0x18)
|
||||
formatVer = strDWord(rec0, 0x24)
|
||||
print "Mobi format version:", formatVer
|
||||
last_rec = strWord(rec0, 8)
|
||||
dwE0 = 0
|
||||
if formatVer>=4:
|
||||
new_rec0 = find_key(rec0, pid)
|
||||
if new_rec0:
|
||||
db.setRecordIdx(0,new_rec0)
|
||||
else:
|
||||
print "PID doesn't match this file"
|
||||
return 2
|
||||
else:
|
||||
print "Wrong Mobi format version"
|
||||
return 1
|
||||
|
||||
outfname = replaceext(fname, ".azw")
|
||||
if outfname==fname:
|
||||
outfname = replaceext(fname, "_fixed.azw")
|
||||
db.save(outfname)
|
||||
print "Output written to "+outfname
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
|
@ -1,91 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# Mobipocket PID calculator v0.2 for Amazon Kindle.
|
||||
# Copyright (c) 2007, 2009 Igor Skochinsky <skochinsky@mail.ru>
|
||||
# History:
|
||||
# 0.1 Initial release
|
||||
# 0.2 Added support for generating PID for iPhone (thanks to mbp)
|
||||
# 0.3 changed to autoflush stdout, fixed return code usage
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
def write(self, data):
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
|
||||
import binascii
|
||||
|
||||
if sys.hexversion >= 0x3000000:
|
||||
print "This script is incompatible with Python 3.x. Please install Python 2.6.x from python.org"
|
||||
sys.exit(2)
|
||||
|
||||
letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
|
||||
def crc32(s):
|
||||
return (~binascii.crc32(s,-1))&0xFFFFFFFF
|
||||
|
||||
def checksumPid(s):
|
||||
crc = crc32(s)
|
||||
crc = crc ^ (crc >> 16)
|
||||
res = s
|
||||
l = len(letters)
|
||||
for i in (0,1):
|
||||
b = crc & 0xff
|
||||
pos = (b // l) ^ (b % l)
|
||||
res += letters[pos%l]
|
||||
crc >>= 8
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def pidFromSerial(s, l):
|
||||
crc = crc32(s)
|
||||
|
||||
arr1 = [0]*l
|
||||
for i in xrange(len(s)):
|
||||
arr1[i%l] ^= ord(s[i])
|
||||
|
||||
crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff]
|
||||
for i in xrange(l):
|
||||
arr1[i] ^= crc_bytes[i&3]
|
||||
|
||||
pid = ""
|
||||
for i in xrange(l):
|
||||
b = arr1[i] & 0xff
|
||||
pid+=letters[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))]
|
||||
|
||||
return pid
|
||||
|
||||
def main(argv=sys.argv):
|
||||
print "Mobipocket PID calculator for Amazon Kindle. Copyright (c) 2007, 2009 Igor Skochinsky"
|
||||
if len(sys.argv)==2:
|
||||
serial = sys.argv[1]
|
||||
else:
|
||||
print "Usage: kindlepid.py <Kindle Serial Number>/<iPhone/iPod Touch UDID>"
|
||||
return 1
|
||||
if len(serial)==16:
|
||||
if serial.startswith("B"):
|
||||
print "Kindle serial number detected"
|
||||
else:
|
||||
print "Warning: unrecognized serial number. Please recheck input."
|
||||
return 1
|
||||
pid = pidFromSerial(serial,7)+"*"
|
||||
print "Mobipocked PID for Kindle serial# "+serial+" is "+checksumPid(pid)
|
||||
return 0
|
||||
elif len(serial)==40:
|
||||
print "iPhone serial number (UDID) detected"
|
||||
pid = pidFromSerial(serial,8)
|
||||
print "Mobipocked PID for iPhone serial# "+serial+" is "+checksumPid(pid)
|
||||
return 0
|
||||
else:
|
||||
print "Warning: unrecognized serial number. Please recheck input."
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
|
@ -1,460 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
#
|
||||
# This is a python script. You need a Python interpreter to run it.
|
||||
# For example, ActiveState Python, which exists for windows.
|
||||
#
|
||||
# Changelog
|
||||
# 0.01 - Initial version
|
||||
# 0.02 - Huffdic compressed books were not properly decrypted
|
||||
# 0.03 - Wasn't checking MOBI header length
|
||||
# 0.04 - Wasn't sanity checking size of data record
|
||||
# 0.05 - It seems that the extra data flags take two bytes not four
|
||||
# 0.06 - And that low bit does mean something after all :-)
|
||||
# 0.07 - The extra data flags aren't present in MOBI header < 0xE8 in size
|
||||
# 0.08 - ...and also not in Mobi header version < 6
|
||||
# 0.09 - ...but they are there with Mobi header version 6, header size 0xE4!
|
||||
# 0.10 - Outputs unencrypted files as-is, so that when run as a Calibre
|
||||
# import filter it works when importing unencrypted files.
|
||||
# Also now handles encrypted files that don't need a specific PID.
|
||||
# 0.11 - use autoflushed stdout and proper return values
|
||||
# 0.12 - Fix for problems with metadata import as Calibre plugin, report errors
|
||||
# 0.13 - Formatting fixes: retabbed file, removed trailing whitespace
|
||||
# and extra blank lines, converted CR/LF pairs at ends of each line,
|
||||
# and other cosmetic fixes.
|
||||
# 0.14 - Working out when the extra data flags are present has been problematic
|
||||
# Versions 7 through 9 have tried to tweak the conditions, but have been
|
||||
# only partially successful. Closer examination of lots of sample
|
||||
# files reveals that a confusion has arisen because trailing data entries
|
||||
# are not encrypted, but it turns out that the multibyte entries
|
||||
# in utf8 file are encrypted. (Although neither kind gets compressed.)
|
||||
# This knowledge leads to a simplification of the test for the
|
||||
# trailing data byte flags - version 5 and higher AND header size >= 0xE4.
|
||||
# 0.15 - Now outputs 'heartbeat', and is also quicker for long files.
|
||||
# 0.16 - And reverts to 'done' not 'done.' at the end for unswindle compatibility.
|
||||
# 0.17 - added modifications to support its use as an imported python module
|
||||
# both inside calibre and also in other places (ie K4DeDRM tools)
|
||||
# 0.17a- disabled the standalone plugin feature since a plugin can not import
|
||||
# a plugin
|
||||
# 0.18 - It seems that multibyte entries aren't encrypted in a v7 file...
|
||||
# Removed the disabled Calibre plug-in code
|
||||
# Permit use of 8-digit PIDs
|
||||
# 0.19 - It seems that multibyte entries aren't encrypted in a v6 file either.
|
||||
# 0.20 - Correction: It seems that multibyte entries are encrypted in a v6 file.
|
||||
# 0.21 - Added support for multiple pids
|
||||
# 0.22 - revised structure to hold MobiBook as a class to allow an extended interface
|
||||
# 0.23 - fixed problem with older files with no EXTH section
|
||||
# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well
|
||||
# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption
|
||||
# 0.26 - Now enables Text-To-Speech flag and sets clipping limit to 100%
|
||||
# 0.27 - Correct pid metadata token generation to match that used by skindle (Thank You Bart!)
|
||||
# 0.28 - slight additional changes to metadata token generation (None -> '')
|
||||
# 0.29 - It seems that the ideas about when multibyte trailing characters were
|
||||
# included in the encryption were wrong. They are for DOC compressed
|
||||
# files, but they are not for HUFF/CDIC compress files!
|
||||
# 0.30 - Modified interface slightly to work better with new calibre plugin style
|
||||
# 0.31 - The multibyte encrytion info is true for version 7 files too.
|
||||
# 0.32 - Added support for "Print Replica" Kindle ebooks
|
||||
# 0.33 - Performance improvements for large files (concatenation)
|
||||
# 0.34 - Performance improvements in decryption (libalfcrypto)
|
||||
# 0.35 - add interface to get mobi_version
|
||||
# 0.36 - fixed problem with TEXtREAd and getBookTitle interface
|
||||
# 0.37 - Fixed double announcement for stand-alone operation
|
||||
|
||||
|
||||
__version__ = '0.37'
|
||||
|
||||
import sys
|
||||
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
def write(self, data):
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
|
||||
import os
|
||||
import struct
|
||||
import binascii
|
||||
from alfcrypto import Pukall_Cipher
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
#
|
||||
# MobiBook Utility Routines
|
||||
#
|
||||
|
||||
# Implementation of Pukall Cipher 1
|
||||
def PC1(key, src, decryption=True):
|
||||
return Pukall_Cipher().PC1(key,src,decryption)
|
||||
# sum1 = 0;
|
||||
# sum2 = 0;
|
||||
# keyXorVal = 0;
|
||||
# if len(key)!=16:
|
||||
# print "Bad key length!"
|
||||
# return None
|
||||
# wkey = []
|
||||
# for i in xrange(8):
|
||||
# wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
|
||||
# dst = ""
|
||||
# for i in xrange(len(src)):
|
||||
# temp1 = 0;
|
||||
# byteXorVal = 0;
|
||||
# for j in xrange(8):
|
||||
# temp1 ^= wkey[j]
|
||||
# sum2 = (sum2+j)*20021 + sum1
|
||||
# sum1 = (temp1*346)&0xFFFF
|
||||
# sum2 = (sum2+sum1)&0xFFFF
|
||||
# temp1 = (temp1*20021+1)&0xFFFF
|
||||
# byteXorVal ^= temp1 ^ sum2
|
||||
# curByte = ord(src[i])
|
||||
# if not decryption:
|
||||
# keyXorVal = curByte * 257;
|
||||
# curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
|
||||
# if decryption:
|
||||
# keyXorVal = curByte * 257;
|
||||
# for j in xrange(8):
|
||||
# wkey[j] ^= keyXorVal;
|
||||
# dst+=chr(curByte)
|
||||
# return dst
|
||||
|
||||
def checksumPid(s):
|
||||
letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
crc = (~binascii.crc32(s,-1))&0xFFFFFFFF
|
||||
crc = crc ^ (crc >> 16)
|
||||
res = s
|
||||
l = len(letters)
|
||||
for i in (0,1):
|
||||
b = crc & 0xff
|
||||
pos = (b // l) ^ (b % l)
|
||||
res += letters[pos%l]
|
||||
crc >>= 8
|
||||
return res
|
||||
|
||||
def getSizeOfTrailingDataEntries(ptr, size, flags):
|
||||
def getSizeOfTrailingDataEntry(ptr, size):
|
||||
bitpos, result = 0, 0
|
||||
if size <= 0:
|
||||
return result
|
||||
while True:
|
||||
v = ord(ptr[size-1])
|
||||
result |= (v & 0x7F) << bitpos
|
||||
bitpos += 7
|
||||
size -= 1
|
||||
if (v & 0x80) != 0 or (bitpos >= 28) or (size == 0):
|
||||
return result
|
||||
num = 0
|
||||
testflags = flags >> 1
|
||||
while testflags:
|
||||
if testflags & 1:
|
||||
num += getSizeOfTrailingDataEntry(ptr, size - num)
|
||||
testflags >>= 1
|
||||
# Check the low bit to see if there's multibyte data present.
|
||||
# if multibyte data is included in the encryped data, we'll
|
||||
# have already cleared this flag.
|
||||
if flags & 1:
|
||||
num += (ord(ptr[size - num - 1]) & 0x3) + 1
|
||||
return num
|
||||
|
||||
|
||||
|
||||
class MobiBook:
|
||||
def loadSection(self, section):
|
||||
if (section + 1 == self.num_sections):
|
||||
endoff = len(self.data_file)
|
||||
else:
|
||||
endoff = self.sections[section + 1][0]
|
||||
off = self.sections[section][0]
|
||||
return self.data_file[off:endoff]
|
||||
|
||||
def __init__(self, infile, announce = True):
|
||||
if announce:
|
||||
print ('MobiDeDrm v%(__version__)s. '
|
||||
'Copyright 2008-2012 The Dark Reverser et al.' % globals())
|
||||
|
||||
# initial sanity check on file
|
||||
self.data_file = file(infile, 'rb').read()
|
||||
self.mobi_data = ''
|
||||
self.header = self.data_file[0:78]
|
||||
if self.header[0x3C:0x3C+8] != 'BOOKMOBI' and self.header[0x3C:0x3C+8] != 'TEXtREAd':
|
||||
raise DrmException("invalid file format")
|
||||
self.magic = self.header[0x3C:0x3C+8]
|
||||
self.crypto_type = -1
|
||||
|
||||
# build up section offset and flag info
|
||||
self.num_sections, = struct.unpack('>H', self.header[76:78])
|
||||
self.sections = []
|
||||
for i in xrange(self.num_sections):
|
||||
offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', self.data_file[78+i*8:78+i*8+8])
|
||||
flags, val = a1, a2<<16|a3<<8|a4
|
||||
self.sections.append( (offset, flags, val) )
|
||||
|
||||
# parse information from section 0
|
||||
self.sect = self.loadSection(0)
|
||||
self.records, = struct.unpack('>H', self.sect[0x8:0x8+2])
|
||||
self.compression, = struct.unpack('>H', self.sect[0x0:0x0+2])
|
||||
|
||||
if self.magic == 'TEXtREAd':
|
||||
print "Book has format: ", self.magic
|
||||
self.extra_data_flags = 0
|
||||
self.mobi_length = 0
|
||||
self.mobi_codepage = 1252
|
||||
self.mobi_version = -1
|
||||
self.meta_array = {}
|
||||
return
|
||||
self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18])
|
||||
self.mobi_codepage, = struct.unpack('>L',self.sect[0x1c:0x20])
|
||||
self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C])
|
||||
print "MOBI header version = %d, length = %d" %(self.mobi_version, self.mobi_length)
|
||||
self.extra_data_flags = 0
|
||||
if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
|
||||
self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4])
|
||||
print "Extra Data Flags = %d" % self.extra_data_flags
|
||||
if (self.compression != 17480):
|
||||
# multibyte utf8 data is included in the encryption for PalmDoc compression
|
||||
# so clear that byte so that we leave it to be decrypted.
|
||||
self.extra_data_flags &= 0xFFFE
|
||||
|
||||
# if exth region exists parse it for metadata array
|
||||
self.meta_array = {}
|
||||
try:
|
||||
exth_flag, = struct.unpack('>L', self.sect[0x80:0x84])
|
||||
exth = 'NONE'
|
||||
if exth_flag & 0x40:
|
||||
exth = self.sect[16 + self.mobi_length:]
|
||||
if (len(exth) >= 4) and (exth[:4] == 'EXTH'):
|
||||
nitems, = struct.unpack('>I', exth[8:12])
|
||||
pos = 12
|
||||
for i in xrange(nitems):
|
||||
type, size = struct.unpack('>II', exth[pos: pos + 8])
|
||||
content = exth[pos + 8: pos + size]
|
||||
self.meta_array[type] = content
|
||||
# reset the text to speech flag and clipping limit, if present
|
||||
if type == 401 and size == 9:
|
||||
# set clipping limit to 100%
|
||||
self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8)
|
||||
elif type == 404 and size == 9:
|
||||
# make sure text to speech is enabled
|
||||
self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8)
|
||||
# print type, size, content, content.encode('hex')
|
||||
pos += size
|
||||
except:
|
||||
self.meta_array = {}
|
||||
pass
|
||||
self.print_replica = False
|
||||
|
||||
def getBookTitle(self):
|
||||
codec_map = {
|
||||
1252 : 'windows-1252',
|
||||
65001 : 'utf-8',
|
||||
}
|
||||
title = ''
|
||||
codec = 'windows-1252'
|
||||
if self.magic == 'BOOKMOBI':
|
||||
if 503 in self.meta_array:
|
||||
title = self.meta_array[503]
|
||||
else:
|
||||
toff, tlen = struct.unpack('>II', self.sect[0x54:0x5c])
|
||||
tend = toff + tlen
|
||||
title = self.sect[toff:tend]
|
||||
if self.mobi_codepage in codec_map.keys():
|
||||
codec = codec_map[self.mobi_codepage]
|
||||
if title == '':
|
||||
title = self.header[:32]
|
||||
title = title.split("\0")[0]
|
||||
return unicode(title, codec).encode('utf-8')
|
||||
|
||||
def getPIDMetaInfo(self):
|
||||
rec209 = ''
|
||||
token = ''
|
||||
if 209 in self.meta_array:
|
||||
rec209 = self.meta_array[209]
|
||||
data = rec209
|
||||
# The 209 data comes in five byte groups. Interpret the last four bytes
|
||||
# of each group as a big endian unsigned integer to get a key value
|
||||
# if that key exists in the meta_array, append its contents to the token
|
||||
for i in xrange(0,len(data),5):
|
||||
val, = struct.unpack('>I',data[i+1:i+5])
|
||||
sval = self.meta_array.get(val,'')
|
||||
token += sval
|
||||
return rec209, token
|
||||
|
||||
def patch(self, off, new):
|
||||
self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):]
|
||||
|
||||
def patchSection(self, section, new, in_off = 0):
|
||||
if (section + 1 == self.num_sections):
|
||||
endoff = len(self.data_file)
|
||||
else:
|
||||
endoff = self.sections[section + 1][0]
|
||||
off = self.sections[section][0]
|
||||
assert off + in_off + len(new) <= endoff
|
||||
self.patch(off + in_off, new)
|
||||
|
||||
def parseDRM(self, data, count, pidlist):
|
||||
found_key = None
|
||||
keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96"
|
||||
for pid in pidlist:
|
||||
bigpid = pid.ljust(16,'\0')
|
||||
temp_key = PC1(keyvec1, bigpid, False)
|
||||
temp_key_sum = sum(map(ord,temp_key)) & 0xff
|
||||
found_key = None
|
||||
for i in xrange(count):
|
||||
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
|
||||
if cksum == temp_key_sum:
|
||||
cookie = PC1(temp_key, cookie)
|
||||
ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
|
||||
if verification == ver and (flags & 0x1F) == 1:
|
||||
found_key = finalkey
|
||||
break
|
||||
if found_key != None:
|
||||
break
|
||||
if not found_key:
|
||||
# Then try the default encoding that doesn't require a PID
|
||||
pid = "00000000"
|
||||
temp_key = keyvec1
|
||||
temp_key_sum = sum(map(ord,temp_key)) & 0xff
|
||||
for i in xrange(count):
|
||||
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
|
||||
if cksum == temp_key_sum:
|
||||
cookie = PC1(temp_key, cookie)
|
||||
ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
|
||||
if verification == ver:
|
||||
found_key = finalkey
|
||||
break
|
||||
return [found_key,pid]
|
||||
|
||||
def getMobiFile(self, outpath):
|
||||
file(outpath,'wb').write(self.mobi_data)
|
||||
|
||||
def getMobiVersion(self):
|
||||
return self.mobi_version
|
||||
|
||||
def getPrintReplica(self):
|
||||
return self.print_replica
|
||||
|
||||
def processBook(self, pidlist):
|
||||
crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2])
|
||||
print 'Crypto Type is: ', crypto_type
|
||||
self.crypto_type = crypto_type
|
||||
if crypto_type == 0:
|
||||
print "This book is not encrypted."
|
||||
# we must still check for Print Replica
|
||||
self.print_replica = (self.loadSection(1)[0:4] == '%MOP')
|
||||
self.mobi_data = self.data_file
|
||||
return
|
||||
if crypto_type != 2 and crypto_type != 1:
|
||||
raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type)
|
||||
if 406 in self.meta_array:
|
||||
data406 = self.meta_array[406]
|
||||
val406, = struct.unpack('>Q',data406)
|
||||
if val406 != 0:
|
||||
raise DrmException("Cannot decode library or rented ebooks.")
|
||||
|
||||
goodpids = []
|
||||
for pid in pidlist:
|
||||
if len(pid)==10:
|
||||
if checksumPid(pid[0:-2]) != pid:
|
||||
print "Warning: PID " + pid + " has incorrect checksum, should have been "+checksumPid(pid[0:-2])
|
||||
goodpids.append(pid[0:-2])
|
||||
elif len(pid)==8:
|
||||
goodpids.append(pid)
|
||||
|
||||
if self.crypto_type == 1:
|
||||
t1_keyvec = "QDCVEPMU675RUBSZ"
|
||||
if self.magic == 'TEXtREAd':
|
||||
bookkey_data = self.sect[0x0E:0x0E+16]
|
||||
elif self.mobi_version < 0:
|
||||
bookkey_data = self.sect[0x90:0x90+16]
|
||||
else:
|
||||
bookkey_data = self.sect[self.mobi_length+16:self.mobi_length+32]
|
||||
pid = "00000000"
|
||||
found_key = PC1(t1_keyvec, bookkey_data)
|
||||
else :
|
||||
# calculate the keys
|
||||
drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16])
|
||||
if drm_count == 0:
|
||||
raise DrmException("Not yet initialised with PID. Must be opened with Mobipocket Reader first.")
|
||||
found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids)
|
||||
if not found_key:
|
||||
raise DrmException("No key found in " + str(len(goodpids)) + " keys tried. Please report this failure for help.")
|
||||
# kill the drm keys
|
||||
self.patchSection(0, "\0" * drm_size, drm_ptr)
|
||||
# kill the drm pointers
|
||||
self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8)
|
||||
|
||||
if pid=="00000000":
|
||||
print "File has default encryption, no specific PID."
|
||||
else:
|
||||
print "File is encoded with PID "+checksumPid(pid)+"."
|
||||
|
||||
# clear the crypto type
|
||||
self.patchSection(0, "\0" * 2, 0xC)
|
||||
|
||||
# decrypt sections
|
||||
print "Decrypting. Please wait . . .",
|
||||
mobidataList = []
|
||||
mobidataList.append(self.data_file[:self.sections[1][0]])
|
||||
for i in xrange(1, self.records+1):
|
||||
data = self.loadSection(i)
|
||||
extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags)
|
||||
if i%100 == 0:
|
||||
print ".",
|
||||
# print "record %d, extra_size %d" %(i,extra_size)
|
||||
decoded_data = PC1(found_key, data[0:len(data) - extra_size])
|
||||
if i==1:
|
||||
self.print_replica = (decoded_data[0:4] == '%MOP')
|
||||
mobidataList.append(decoded_data)
|
||||
if extra_size > 0:
|
||||
mobidataList.append(data[-extra_size:])
|
||||
if self.num_sections > self.records+1:
|
||||
mobidataList.append(self.data_file[self.sections[self.records+1][0]:])
|
||||
self.mobi_data = "".join(mobidataList)
|
||||
print "done"
|
||||
return
|
||||
|
||||
def getUnencryptedBook(infile,pid,announce=True):
|
||||
if not os.path.isfile(infile):
|
||||
raise DrmException('Input File Not Found')
|
||||
book = MobiBook(infile,announce)
|
||||
book.processBook([pid])
|
||||
return book.mobi_data
|
||||
|
||||
def getUnencryptedBookWithList(infile,pidlist,announce=True):
|
||||
if not os.path.isfile(infile):
|
||||
raise DrmException('Input File Not Found')
|
||||
book = MobiBook(infile, announce)
|
||||
book.processBook(pidlist)
|
||||
return book.mobi_data
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
print ('MobiDeDrm v%(__version__)s. '
|
||||
'Copyright 2008-2012 The Dark Reverser et al.' % globals())
|
||||
if len(argv)<3 or len(argv)>4:
|
||||
print "Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks"
|
||||
print "Usage:"
|
||||
print " %s <infile> <outfile> [<Comma separated list of PIDs to try>]" % sys.argv[0]
|
||||
return 1
|
||||
else:
|
||||
infile = argv[1]
|
||||
outfile = argv[2]
|
||||
if len(argv) is 4:
|
||||
pidlist = argv[3].split(',')
|
||||
else:
|
||||
pidlist = {}
|
||||
try:
|
||||
stripped_file = getUnencryptedBookWithList(infile, pidlist, False)
|
||||
file(outfile, 'wb').write(stripped_file)
|
||||
except DrmException, e:
|
||||
print "Error: %s" % e
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
|
@ -1,189 +0,0 @@
|
|||
# This is a python script. You need a Python interpreter to run it.
|
||||
# For example, ActiveState Python, which exists for windows.
|
||||
#
|
||||
# Big Thanks to Igor SKOCHINSKY for providing me with all his information
|
||||
# and source code relating to the inner workings of this compression scheme.
|
||||
# Without it, I wouldn't be able to solve this as easily.
|
||||
#
|
||||
# Changelog
|
||||
# 0.01 - Initial version
|
||||
# 0.02 - Fix issue with size computing
|
||||
# 0.03 - Fix issue with some files
|
||||
# 0.04 - make stdout self flushing and fix return values
|
||||
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
def write(self, data):
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
|
||||
|
||||
import struct
|
||||
|
||||
class BitReader:
|
||||
def __init__(self, data):
|
||||
self.data, self.pos, self.nbits = data + "\x00\x00\x00\x00", 0, len(data) * 8
|
||||
def peek(self, n):
|
||||
r, g = 0, 0
|
||||
while g < n:
|
||||
r, g = (r << 8) | ord(self.data[(self.pos+g)>>3]), g + 8 - ((self.pos+g) & 7)
|
||||
return (r >> (g - n)) & ((1 << n) - 1)
|
||||
def eat(self, n):
|
||||
self.pos += n
|
||||
return self.pos <= self.nbits
|
||||
def left(self):
|
||||
return self.nbits - self.pos
|
||||
|
||||
class HuffReader:
|
||||
def __init__(self, huffs):
|
||||
self.huffs = huffs
|
||||
h = huffs[0]
|
||||
if huffs[0][0:4] != 'HUFF' or huffs[0][4:8] != '\x00\x00\x00\x18':
|
||||
raise ValueError('invalid huff1 header')
|
||||
if huffs[1][0:4] != 'CDIC' or huffs[1][4:8] != '\x00\x00\x00\x10':
|
||||
raise ValueError('invalid huff2 header')
|
||||
self.entry_bits, = struct.unpack('>L', huffs[1][12:16])
|
||||
off1,off2 = struct.unpack('>LL', huffs[0][16:24])
|
||||
self.dict1 = struct.unpack('<256L', huffs[0][off1:off1+256*4])
|
||||
self.dict2 = struct.unpack('<64L', huffs[0][off2:off2+64*4])
|
||||
self.dicts = huffs[1:]
|
||||
self.r = ''
|
||||
|
||||
def _unpack(self, bits, depth = 0):
|
||||
if depth > 32:
|
||||
raise ValueError('corrupt file')
|
||||
while bits.left():
|
||||
dw = bits.peek(32)
|
||||
v = self.dict1[dw >> 24]
|
||||
codelen = v & 0x1F
|
||||
assert codelen != 0
|
||||
code = dw >> (32 - codelen)
|
||||
r = (v >> 8)
|
||||
if not (v & 0x80):
|
||||
while code < self.dict2[(codelen-1)*2]:
|
||||
codelen += 1
|
||||
code = dw >> (32 - codelen)
|
||||
r = self.dict2[(codelen-1)*2+1]
|
||||
r -= code
|
||||
assert codelen != 0
|
||||
if not bits.eat(codelen):
|
||||
return
|
||||
dicno = r >> self.entry_bits
|
||||
off1 = 16 + (r - (dicno << self.entry_bits)) * 2
|
||||
dic = self.dicts[dicno]
|
||||
off2 = 16 + ord(dic[off1]) * 256 + ord(dic[off1+1])
|
||||
blen = ord(dic[off2]) * 256 + ord(dic[off2+1])
|
||||
slice = dic[off2+2:off2+2+(blen&0x7fff)]
|
||||
if blen & 0x8000:
|
||||
self.r += slice
|
||||
else:
|
||||
self._unpack(BitReader(slice), depth + 1)
|
||||
|
||||
def unpack(self, data):
|
||||
self.r = ''
|
||||
self._unpack(BitReader(data))
|
||||
return self.r
|
||||
|
||||
class Sectionizer:
|
||||
def __init__(self, filename, ident):
|
||||
self.contents = file(filename, 'rb').read()
|
||||
self.header = self.contents[0:72]
|
||||
self.num_sections, = struct.unpack('>H', self.contents[76:78])
|
||||
if self.header[0x3C:0x3C+8] != ident:
|
||||
raise ValueError('Invalid file format')
|
||||
self.sections = []
|
||||
for i in xrange(self.num_sections):
|
||||
offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', self.contents[78+i*8:78+i*8+8])
|
||||
flags, val = a1, a2<<16|a3<<8|a4
|
||||
self.sections.append( (offset, flags, val) )
|
||||
def loadSection(self, section):
|
||||
if section + 1 == self.num_sections:
|
||||
end_off = len(self.contents)
|
||||
else:
|
||||
end_off = self.sections[section + 1][0]
|
||||
off = self.sections[section][0]
|
||||
return self.contents[off:end_off]
|
||||
|
||||
|
||||
def getSizeOfTrailingDataEntry(ptr, size):
|
||||
bitpos, result = 0, 0
|
||||
while True:
|
||||
v = ord(ptr[size-1])
|
||||
result |= (v & 0x7F) << bitpos
|
||||
bitpos += 7
|
||||
size -= 1
|
||||
if (v & 0x80) != 0 or (bitpos >= 28) or (size == 0):
|
||||
return result
|
||||
|
||||
def getSizeOfTrailingDataEntries(ptr, size, flags):
|
||||
num = 0
|
||||
flags >>= 1
|
||||
while flags:
|
||||
if flags & 1:
|
||||
num += getSizeOfTrailingDataEntry(ptr, size - num)
|
||||
flags >>= 1
|
||||
return num
|
||||
|
||||
def unpackBook(input_file):
|
||||
sect = Sectionizer(input_file, 'BOOKMOBI')
|
||||
|
||||
header = sect.loadSection(0)
|
||||
|
||||
crypto_type, = struct.unpack('>H', header[0xC:0xC+2])
|
||||
if crypto_type != 0:
|
||||
raise ValueError('The book is encrypted. Run mobidedrm first')
|
||||
|
||||
if header[0:2] != 'DH':
|
||||
raise ValueError('invalid compression type')
|
||||
|
||||
extra_flags, = struct.unpack('>L', header[0xF0:0xF4])
|
||||
records, = struct.unpack('>H', header[0x8:0x8+2])
|
||||
|
||||
huffoff,huffnum = struct.unpack('>LL', header[0x70:0x78])
|
||||
huffs = [sect.loadSection(i) for i in xrange(huffoff, huffoff+huffnum)]
|
||||
huff = HuffReader(huffs)
|
||||
|
||||
def decompressSection(nr):
|
||||
data = sect.loadSection(nr)
|
||||
trail_size = getSizeOfTrailingDataEntries(data, len(data), extra_flags)
|
||||
return huff.unpack(data[0:len(data)-trail_size])
|
||||
|
||||
r = ''
|
||||
for i in xrange(1, records+1):
|
||||
r += decompressSection(i)
|
||||
return r
|
||||
|
||||
def main(argv=sys.argv):
|
||||
print "MobiHuff v0.03"
|
||||
print " Copyright (c) 2008 The Dark Reverser <dark.reverser@googlemail.com>"
|
||||
if len(sys.argv)!=3:
|
||||
print ""
|
||||
print "Description:"
|
||||
print " Unpacks the new mobipocket huffdic compression."
|
||||
print " This program works with unencrypted files only."
|
||||
print "Usage:"
|
||||
print " mobihuff.py infile.mobi outfile.html"
|
||||
return 1
|
||||
else:
|
||||
infile = sys.argv[1]
|
||||
outfile = sys.argv[2]
|
||||
try:
|
||||
print "Decompressing...",
|
||||
result = unpackBook(infile)
|
||||
file(outfile, 'wb').write(result)
|
||||
print "done"
|
||||
except ValueError, e:
|
||||
print
|
||||
print "Error: %s" % e
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
|
@ -1,529 +0,0 @@
|
|||
#
|
||||
# $Id: prc.py,v 1.3 2001/12/27 08:48:02 rob Exp $
|
||||
#
|
||||
# Copyright 1998-2001 Rob Tillotson <rob@pyrite.org>
|
||||
# All Rights Reserved
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and
|
||||
# its documentation for any purpose and without fee or royalty is
|
||||
# hereby granted, provided that the above copyright notice appear in
|
||||
# all copies and that both the copyright notice and this permission
|
||||
# notice appear in supporting documentation or portions thereof,
|
||||
# including modifications, that you you make.
|
||||
#
|
||||
# THE AUTHOR ROB TILLOTSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
# SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
|
||||
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
||||
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE!
|
||||
#
|
||||
"""PRC/PDB file I/O in pure Python.
|
||||
|
||||
This module serves two purposes: one, it allows access to Palm OS(tm)
|
||||
database files on the desktop in pure Python without requiring
|
||||
pilot-link (hence, it may be useful for import/export utilities),
|
||||
and two, it caches the contents of the file in memory so it can
|
||||
be freely modified using an identical API to databases over a
|
||||
DLP connection.
|
||||
"""
|
||||
|
||||
__version__ = '$Id: prc.py,v 1.3 2001/12/27 08:48:02 rob Exp $'
|
||||
|
||||
__copyright__ = 'Copyright 1998-2001 Rob Tillotson <robt@debian.org>'
|
||||
|
||||
|
||||
# temporary hack until we get gettext support again
|
||||
def _(s): return s
|
||||
|
||||
#
|
||||
# DBInfo structure:
|
||||
#
|
||||
# int more
|
||||
# unsigned int flags
|
||||
# unsigned int miscflags
|
||||
# unsigned long type
|
||||
# unsigned long creator
|
||||
# unsigned int version
|
||||
# unsigned long modnum
|
||||
# time_t createDate, modifydate, backupdate
|
||||
# unsigned int index
|
||||
# char name[34]
|
||||
#
|
||||
#
|
||||
# DB Header:
|
||||
# 32 name
|
||||
# 2 flags
|
||||
# 2 version
|
||||
# 4 creation time
|
||||
# 4 modification time
|
||||
# 4 backup time
|
||||
# 4 modification number
|
||||
# 4 appinfo offset
|
||||
# 4 sortinfo offset
|
||||
# 4 type
|
||||
# 4 creator
|
||||
# 4 unique id seed (garbage?)
|
||||
# 4 next record list id (normally 0)
|
||||
# 2 num of records for this header
|
||||
# (maybe 2 more bytes)
|
||||
#
|
||||
# Resource entry header: (if low bit of attr = 1)
|
||||
# 4 type
|
||||
# 2 id
|
||||
# 4 offset
|
||||
#
|
||||
# record entry header: (if low bit of attr = 0)
|
||||
# 4 offset
|
||||
# 1 attributes
|
||||
# 3 unique id
|
||||
#
|
||||
# then 2 bytes of 0
|
||||
#
|
||||
# then appinfo then sortinfo
|
||||
#
|
||||
|
||||
import sys, os, stat, struct
|
||||
|
||||
PI_HDR_SIZE = 78
|
||||
PI_RESOURCE_ENT_SIZE = 10
|
||||
PI_RECORD_ENT_SIZE = 8
|
||||
|
||||
PILOT_TIME_DELTA = 2082844800L
|
||||
|
||||
flagResource = 0x0001
|
||||
flagReadOnly = 0x0002
|
||||
flagAppInfoDirty = 0x0004
|
||||
flagBackup = 0x0008
|
||||
flagOpen = 0x8000
|
||||
# 2.x
|
||||
flagNewer = 0x0010
|
||||
flagReset = 0x0020
|
||||
#
|
||||
flagExcludeFromSync = 0x0080
|
||||
|
||||
attrDeleted = 0x80
|
||||
attrDirty = 0x40
|
||||
attrBusy = 0x20
|
||||
attrSecret = 0x10
|
||||
attrArchived = 0x08
|
||||
|
||||
default_info = {
|
||||
'name': '',
|
||||
'type': 'DATA',
|
||||
'creator': ' ',
|
||||
'createDate': 0,
|
||||
'modifyDate': 0,
|
||||
'backupDate': 0,
|
||||
'modnum': 0,
|
||||
'version': 0,
|
||||
'flagReset': 0,
|
||||
'flagResource': 0,
|
||||
'flagNewer': 0,
|
||||
'flagExcludeFromSync': 0,
|
||||
'flagAppInfoDirty': 0,
|
||||
'flagReadOnly': 0,
|
||||
'flagBackup': 0,
|
||||
'flagOpen': 0,
|
||||
'more': 0,
|
||||
'index': 0
|
||||
}
|
||||
|
||||
def null_terminated(s):
|
||||
for x in range(0, len(s)):
|
||||
if s[x] == '\000': return s[:x]
|
||||
return s
|
||||
|
||||
def trim_null(s):
|
||||
return string.split(s, '\0')[0]
|
||||
|
||||
def pad_null(s, l):
|
||||
if len(s) > l - 1:
|
||||
s = s[:l-1]
|
||||
s = s + '\0'
|
||||
if len(s) < l: s = s + '\0' * (l - len(s))
|
||||
return s
|
||||
|
||||
#
|
||||
# new stuff
|
||||
|
||||
# Record object to be put in tree...
|
||||
class PRecord:
|
||||
def __init__(self, attr=0, id=0, category=0, raw=''):
|
||||
self.raw = raw
|
||||
self.id = id
|
||||
self.attr = attr
|
||||
self.category = category
|
||||
|
||||
# comparison and hashing are done by ID;
|
||||
# thus, the id value *may not be changed* once
|
||||
# the object is created.
|
||||
def __cmp__(self, obj):
|
||||
if type(obj) == type(0):
|
||||
return cmp(self.id, obj)
|
||||
else:
|
||||
return cmp(self.id, obj.id)
|
||||
|
||||
def __hash__(self):
|
||||
return self.id
|
||||
|
||||
class PResource:
|
||||
def __init__(self, typ=' ', id=0, raw=''):
|
||||
self.raw = raw
|
||||
self.id = id
|
||||
self.type = typ
|
||||
|
||||
def __cmp__(self, obj):
|
||||
if type(obj) == type(()):
|
||||
return cmp( (self.type, self.id), obj)
|
||||
else:
|
||||
return cmp( (self.type, self.id), (obj.type, obj.id) )
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.type, self.id))
|
||||
|
||||
|
||||
class PCache:
|
||||
def __init__(self):
|
||||
self.data = []
|
||||
self.appblock = ''
|
||||
self.sortblock = ''
|
||||
self.dirty = 0
|
||||
self.next = 0
|
||||
self.info = {}
|
||||
self.info.update(default_info)
|
||||
# if allow_zero_ids is 1, then this prc behaves appropriately
|
||||
# for a desktop database. That is, it never attempts to assign
|
||||
# an ID, and lets new records be inserted with an ID of zero.
|
||||
self.allow_zero_ids = 0
|
||||
|
||||
# pi-file API
|
||||
def getRecords(self): return len(self.data)
|
||||
def getAppBlock(self): return self.appblock and self.appblock or None
|
||||
def setAppBlock(self, raw):
|
||||
self.dirty = 1
|
||||
self.appblock = raw
|
||||
def getSortBlock(self): return self.sortblock and self.sortblock or None
|
||||
def setSortBlock(self, raw):
|
||||
self.dirty = 1
|
||||
self.appblock = raw
|
||||
def checkID(self, id): return id in self.data
|
||||
def getRecord(self, i):
|
||||
try: r = self.data[i]
|
||||
except: return None
|
||||
return r.raw, i, r.id, r.attr, r.category
|
||||
def getRecordByID(self, id):
|
||||
try:
|
||||
i = self.data.index(id)
|
||||
r = self.data[i]
|
||||
except: return None
|
||||
return r.raw, i, r.id, r.attr, r.category
|
||||
def getResource(self, i):
|
||||
try: r = self.data[i]
|
||||
except: return None
|
||||
return r.raw, r.type, r.id
|
||||
def getDBInfo(self): return self.info
|
||||
def setDBInfo(self, info):
|
||||
self.dirty = 1
|
||||
self.info = {}
|
||||
self.info.update(info)
|
||||
|
||||
def updateDBInfo(self, info):
|
||||
self.dirty = 1
|
||||
self.info.update(info)
|
||||
|
||||
def setRecord(self, attr, id, cat, data):
|
||||
if not self.allow_zero_ids and not id:
|
||||
if not len(self.data): id = 1
|
||||
else:
|
||||
xid = self.data[0].id + 1
|
||||
while xid in self.data: xid = xid + 1
|
||||
id = xid
|
||||
|
||||
r = PRecord(attr, id, cat, data)
|
||||
if id and id in self.data:
|
||||
self.data.remove(id)
|
||||
self.data.append(r)
|
||||
self.dirty = 1
|
||||
return id
|
||||
|
||||
def setRecordIdx(self, i, data):
|
||||
self.data[i].raw = data
|
||||
self.dirty = 1
|
||||
|
||||
def setResource(self, typ, id, data):
|
||||
if (typ, id) in self.data:
|
||||
self.data.remove((typ,id))
|
||||
r = PResource(typ, id, data)
|
||||
self.data.append(r)
|
||||
self.dirty = 1
|
||||
return id
|
||||
|
||||
def getNextRecord(self, cat):
|
||||
while self.next < len(self.data):
|
||||
r = self.data[self.next]
|
||||
i = self.next
|
||||
self.next = self.next + 1
|
||||
if r.category == cat:
|
||||
return r.raw, i, r.id, r.attr, r.category
|
||||
return ''
|
||||
|
||||
def getNextModRecord(self, cat=-1):
|
||||
while self.next < len(self.data):
|
||||
r = self.data[self.next]
|
||||
i = self.next
|
||||
self.next = self.next + 1
|
||||
if (r.attr & attrModified) and (cat < 0 or r.category == cat):
|
||||
return r.raw, i, r.id, r.attr, r.category
|
||||
|
||||
def getResourceByID(self, type, id):
|
||||
try: r = self.data[self.data.index((type,id))]
|
||||
except: return None
|
||||
return r.raw, r.type, r.id
|
||||
|
||||
def deleteRecord(self, id):
|
||||
if not id in self.data: return None
|
||||
self.data.remove(id)
|
||||
self.dirty = 1
|
||||
|
||||
def deleteRecords(self):
|
||||
self.data = []
|
||||
self.dirty = 1
|
||||
|
||||
def deleteResource(self, type, id):
|
||||
if not (type,id) in self.data: return None
|
||||
self.data.remove((type,id))
|
||||
self.dirty = 1
|
||||
|
||||
def deleteResources(self):
|
||||
self.data = []
|
||||
self.dirty = 1
|
||||
|
||||
def getRecordIDs(self, sort=0):
|
||||
m = map(lambda x: x.id, self.data)
|
||||
if sort: m.sort()
|
||||
return m
|
||||
|
||||
def moveCategory(self, frm, to):
|
||||
for r in self.data:
|
||||
if r.category == frm:
|
||||
r.category = to
|
||||
self.dirty = 1
|
||||
|
||||
def deleteCategory(self, cat):
|
||||
raise RuntimeError, _("unimplemented")
|
||||
|
||||
def purge(self):
|
||||
ndata = []
|
||||
# change to filter later
|
||||
for r in self.data:
|
||||
if (r.attr & attrDeleted):
|
||||
continue
|
||||
ndata.append(r)
|
||||
self.data = ndata
|
||||
self.dirty = 1
|
||||
|
||||
def resetNext(self):
|
||||
self.next = 0
|
||||
|
||||
def resetFlags(self):
|
||||
# special behavior for resources
|
||||
if not self.info.get('flagResource',0):
|
||||
# use map()
|
||||
for r in self.data:
|
||||
r.attr = r.attr & ~attrDirty
|
||||
self.dirty = 1
|
||||
|
||||
import pprint
|
||||
class File(PCache):
|
||||
def __init__(self, name=None, read=1, write=0, info={}):
|
||||
PCache.__init__(self)
|
||||
self.filename = name
|
||||
self.info.update(info)
|
||||
self.writeback = write
|
||||
self.isopen = 0
|
||||
|
||||
if read:
|
||||
self.load(name)
|
||||
self.isopen = 1
|
||||
|
||||
def close(self):
|
||||
if self.writeback and self.dirty:
|
||||
self.save(self.filename)
|
||||
self.isopen = 0
|
||||
|
||||
def __del__(self):
|
||||
if self.isopen: self.close()
|
||||
|
||||
def load(self, f):
|
||||
if type(f) == type(''): f = open(f, 'rb')
|
||||
|
||||
data = f.read()
|
||||
self.unpack(data)
|
||||
|
||||
def unpack(self, data):
|
||||
if len(data) < PI_HDR_SIZE: raise IOError, _("file too short")
|
||||
(name, flags, ver, ctime, mtime, btime, mnum, appinfo, sortinfo,
|
||||
typ, creator, uid, nextrec, numrec) \
|
||||
= struct.unpack('>32shhLLLlll4s4sllh', data[:PI_HDR_SIZE])
|
||||
|
||||
if nextrec or appinfo < 0 or sortinfo < 0 or numrec < 0:
|
||||
raise IOError, _("invalid database header")
|
||||
|
||||
self.info = {
|
||||
'name': null_terminated(name),
|
||||
'type': typ,
|
||||
'creator': creator,
|
||||
'createDate': ctime - PILOT_TIME_DELTA,
|
||||
'modifyDate': mtime - PILOT_TIME_DELTA,
|
||||
'backupDate': btime - PILOT_TIME_DELTA,
|
||||
'modnum': mnum,
|
||||
'version': ver,
|
||||
'flagReset': flags & flagReset,
|
||||
'flagResource': flags & flagResource,
|
||||
'flagNewer': flags & flagNewer,
|
||||
'flagExcludeFromSync': flags & flagExcludeFromSync,
|
||||
'flagAppInfoDirty': flags & flagAppInfoDirty,
|
||||
'flagReadOnly': flags & flagReadOnly,
|
||||
'flagBackup': flags & flagBackup,
|
||||
'flagOpen': flags & flagOpen,
|
||||
'more': 0,
|
||||
'index': 0
|
||||
}
|
||||
|
||||
rsrc = flags & flagResource
|
||||
if rsrc: s = PI_RESOURCE_ENT_SIZE
|
||||
else: s = PI_RECORD_ENT_SIZE
|
||||
|
||||
entries = []
|
||||
|
||||
pos = PI_HDR_SIZE
|
||||
for x in range(0,numrec):
|
||||
hstr = data[pos:pos+s]
|
||||
pos = pos + s
|
||||
if not hstr or len(hstr) < s:
|
||||
raise IOError, _("bad database header")
|
||||
|
||||
if rsrc:
|
||||
(typ, id, offset) = struct.unpack('>4shl', hstr)
|
||||
entries.append((offset, typ, id))
|
||||
else:
|
||||
(offset, auid) = struct.unpack('>ll', hstr)
|
||||
attr = (auid & 0xff000000) >> 24
|
||||
uid = auid & 0x00ffffff
|
||||
entries.append((offset, attr, uid))
|
||||
|
||||
offset = len(data)
|
||||
entries.reverse()
|
||||
for of, q, id in entries:
|
||||
size = offset - of
|
||||
if size < 0: raise IOError, _("bad pdb/prc record entry (size < 0)")
|
||||
d = data[of:offset]
|
||||
offset = of
|
||||
if len(d) != size: raise IOError, _("failed to read record")
|
||||
if rsrc:
|
||||
r = PResource(q, id, d)
|
||||
self.data.append(r)
|
||||
else:
|
||||
r = PRecord(q & 0xf0, id, q & 0x0f, d)
|
||||
self.data.append(r)
|
||||
self.data.reverse()
|
||||
|
||||
if sortinfo:
|
||||
sortinfo_size = offset - sortinfo
|
||||
offset = sortinfo
|
||||
else:
|
||||
sortinfo_size = 0
|
||||
|
||||
if appinfo:
|
||||
appinfo_size = offset - appinfo
|
||||
offset = appinfo
|
||||
else:
|
||||
appinfo_size = 0
|
||||
|
||||
if appinfo_size < 0 or sortinfo_size < 0:
|
||||
raise IOError, _("bad database header (appinfo or sortinfo size < 0)")
|
||||
|
||||
if appinfo_size:
|
||||
self.appblock = data[appinfo:appinfo+appinfo_size]
|
||||
if len(self.appblock) != appinfo_size:
|
||||
raise IOError, _("failed to read appinfo block")
|
||||
|
||||
if sortinfo_size:
|
||||
self.sortblock = data[sortinfo:sortinfo+sortinfo_size]
|
||||
if len(self.sortblock) != sortinfo_size:
|
||||
raise IOError, _("failed to read sortinfo block")
|
||||
|
||||
def save(self, f):
|
||||
"""Dump the cache to a file.
|
||||
"""
|
||||
if type(f) == type(''): f = open(f, 'wb')
|
||||
|
||||
# first, we need to precalculate the offsets.
|
||||
if self.info.get('flagResource'):
|
||||
entries_len = 10 * len(self.data)
|
||||
else: entries_len = 8 * len(self.data)
|
||||
|
||||
off = PI_HDR_SIZE + entries_len + 2
|
||||
if self.appblock:
|
||||
appinfo_offset = off
|
||||
off = off + len(self.appblock)
|
||||
else:
|
||||
appinfo_offset = 0
|
||||
if self.sortblock:
|
||||
sortinfo_offset = off
|
||||
off = off + len(self.sortblock)
|
||||
else:
|
||||
sortinfo_offset = 0
|
||||
|
||||
rec_offsets = []
|
||||
for x in self.data:
|
||||
rec_offsets.append(off)
|
||||
off = off + len(x.raw)
|
||||
|
||||
info = self.info
|
||||
flg = 0
|
||||
if info.get('flagResource',0): flg = flg | flagResource
|
||||
if info.get('flagReadOnly',0): flg = flg | flagReadOnly
|
||||
if info.get('flagAppInfoDirty',0): flg = flg | flagAppInfoDirty
|
||||
if info.get('flagBackup',0): flg = flg | flagBackup
|
||||
if info.get('flagOpen',0): flg = flg | flagOpen
|
||||
if info.get('flagNewer',0): flg = flg | flagNewer
|
||||
if info.get('flagReset',0): flg = flg | flagReset
|
||||
# excludefromsync doesn't actually get stored?
|
||||
hdr = struct.pack('>32shhLLLlll4s4sllh',
|
||||
pad_null(info.get('name',''), 32),
|
||||
flg,
|
||||
info.get('version',0),
|
||||
info.get('createDate',0L)+PILOT_TIME_DELTA,
|
||||
info.get('modifyDate',0L)+PILOT_TIME_DELTA,
|
||||
info.get('backupDate',0L)+PILOT_TIME_DELTA,
|
||||
info.get('modnum',0),
|
||||
appinfo_offset, # appinfo
|
||||
sortinfo_offset, # sortinfo
|
||||
info.get('type',' '),
|
||||
info.get('creator',' '),
|
||||
0, # uid???
|
||||
0, # nextrec???
|
||||
len(self.data))
|
||||
|
||||
f.write(hdr)
|
||||
|
||||
entries = []
|
||||
record_data = []
|
||||
rsrc = self.info.get('flagResource')
|
||||
for x, off in map(None, self.data, rec_offsets):
|
||||
if rsrc:
|
||||
record_data.append(x.raw)
|
||||
entries.append(struct.pack('>4shl', x.type, x.id, off))
|
||||
else:
|
||||
record_data.append(x.raw)
|
||||
a = ((x.attr | x.category) << 24) | x.id
|
||||
entries.append(struct.pack('>ll', off, a))
|
||||
|
||||
for x in entries: f.write(x)
|
||||
f.write('\0\0') # padding? dunno, it's always there.
|
||||
if self.appblock: f.write(self.appblock)
|
||||
if self.sortblock: f.write(self.sortblock)
|
||||
for x in record_data: f.write(x)
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue