Compare commits

...

37 Commits

Author SHA1 Message Date
apprenticeharper
07485be2c0 Add transparency to the obok logo 2015-09-03 08:06:33 +01:00
apprenticeharper
e5e269fbae Fixes for android key extraction 2015-09-03 07:51:10 +01:00
apprenticeharper
d54dc38c2d Fix for Kobo Desktop 3.17 2015-09-03 07:49:09 +01:00
apprenticeharper
3c322f3695 Second and last step to fix capitalisation issue 2015-09-01 07:08:46 +01:00
apprenticeharper
c112c28f58 First step to fix capitalisation issue 2015-09-01 07:06:13 +01:00
apprenticeharper
b8606cd182 Merge pull request #32 from eli-schwartz/master
Linux: Allow using ~ when specifying a wineprefix.
2015-09-01 06:57:48 +01:00
Eli Schwartz
f2190a6755 Linux: Allow using ~ when specifying a wineprefix. 2015-08-12 17:39:50 -04:00
apprenticeharper
33d8a63f61 Fix for plugin bug introduced in 6.3.1 for Kindle for PC 2015-08-12 18:35:21 +01:00
apprenticeharper
91b22c18c4 Update kobo plugin to 3.1.3 with Portuguese and Arabic translations. Include .mo files in git. 2015-08-05 06:59:01 +01:00
apprenticeharper
3317dc7330 Fixed plugin help file and updated readmes 2015-08-04 07:18:33 +01:00
apprenticeharper
aa866938f5 Updated plugin zip with 6.3.1 files 2015-08-02 11:10:57 +01:00
apprenticeharper
aa822de138 New approach to Android backup files. Changed version number to 6.3.1 2015-08-02 11:09:35 +01:00
apprenticeharper
f5e66d42a1 Android backup handling approach improved and implemented in Windows and plugin. Mac work to follow. 2015-07-29 18:11:19 +01:00
apprenticeharper
c16d767b00 Merge branch 'nook_url_support' 2015-07-13 18:02:48 +01:00
Apprentice Harper
9a8d5f74a6 Improvements to nook Study key retrieval, and addition of retrieval of nook keys via the internet. 2015-07-11 13:50:36 +01:00
Apprentice Harper
6be1323817 Fixed name of Kindle for Android help file 2015-04-14 07:02:18 +01:00
Apprentice Harper
9b77255212 Starting to move ignoblekeyfetch into all tools. 2015-04-13 07:45:43 +01:00
Apprentice Harper
46426a9eae The compressed plugin so far 2015-04-10 18:14:36 +01:00
Apprentice Harper
45ad3cedec Added in fetching B&N key via URL instead of generating from name & CC# 2015-04-10 18:12:39 +01:00
Apprentice Harper
d140b7e2dc Merge of bugfix 6.2.1 into master 2015-03-26 07:31:45 +00:00
Apprentice Harper
e729ae8904 Fix for Kindle problem in Mac app and non-ascii username problem in Windows (plugin and app). 2015-03-25 07:26:33 +00:00
Apprentice Harper
0837482686 changed for android support - in progress 2015-03-24 07:04:06 +00:00
Apprentice Harper
4c9aacd01e Added help file for Kindle for Android keys to plugin. Copied updated files to Mac and Windows applications. 2015-03-18 20:37:54 +00:00
Apprentice Harper
6b2672ff7c Fixes for the plugin and Android keys (help still needs adding) 2015-03-18 19:12:01 +00:00
Apprentice Alf
39c9d57b15 Merge branch 'master' of https://github.com/apprenticeharper/DeDRM_tools 2015-03-17 18:02:23 +00:00
Apprentice Alf
9c347ca42f removed unused file 2015-03-17 17:49:57 +00:00
Apprentice Alf
032fcfa422 Partial update of plugin to use androidkindlekey.py. Still needs more testing/tweaking in the preferences. 2015-03-17 17:49:30 +00:00
Apprentice Alf
35aaf20c8d Update the Macintosh AppleScript to use the new androidkindlekey.py 2015-03-17 17:48:25 +00:00
Apprentice Alf
b146e4b864 Update androidkindlekey.py to work with tools 2015-03-17 07:07:12 +00:00
Apprentice Alf
27d8f08b54 android.py name change to androidkindlekey.py 2015-03-17 07:05:00 +00:00
apprenticeharper
6db762bc40 Create README.md 2015-03-13 17:22:40 +00:00
Apprentice Alf
c7c34274e9 Obok plugin 3.1.2 unzipped 2015-03-13 07:18:16 +00:00
Apprentice Alf
cf922b6ba1 obok 3.1.1 plugin unzipped 2015-03-13 07:16:59 +00:00
Apprentice Harper
9d9c879413 tools v6.2.0
Updated for B&N new scheme, added obok plugin, and many minor fixes,
2015-03-09 07:41:07 +00:00
Apprentice Alf
c4fc10395b tools v6.1.0 2015-03-07 21:23:33 +00:00
Apprentice Alf
a30aace99c tools v6.0.9
obok added to other tools
2015-03-07 21:18:50 +00:00
Apprentice Alf
b1feca321d tools v6.0.8 2015-03-07 21:10:52 +00:00
141 changed files with 23439 additions and 10148 deletions

1
.gitignore vendored
View File

@@ -37,7 +37,6 @@ nosetests.xml
coverage.xml
# Translations
*.mo
*.pot
# Django stuff:

View File

@@ -1,24 +1,44 @@
{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fnil\fcharset0 Verdana;\f2\fnil\fcharset128 HiraKakuProN-W3;
}
{\rtf1\ansi\ansicpg1252\cocoartf1348\cocoasubrtf170
{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fnil\fcharset134 STHeitiSC-Light;}
{\colortbl;\red255\green255\blue255;}
\paperw11900\paperh16840\margl1440\margr1440\vieww18160\viewh16520\viewkind0
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\qc
\paperw11900\paperh16840\vieww12000\viewh15840\viewkind0
\deftab720
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720\qc
\f0\b\fs24 \cf0 DeDRM ReadMe
\b0 \
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720
\cf0 \
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\qj
\cf0 DeDRM is an application that packs all of the python dm-removal software into one easy to use program that remembers preferences and settings.\
It works without manual configuration with Kindle for Mac ebooks and Adobe Adept (including nook) ePub and PDF ebooks.\
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720\qj
\cf0 DeDRM is an application that packs all of the python dm removal software into one easy to use program that remembers preferences and settings.\
It works without manual configuration with Kindle for Mac ebooks, Adobe Digital Editions Adept ePub and PDF ebooks, and Barnes & Noble NOOK Study ebooks.\
\
To remove the DRM of Kindle ebooks from eInk Kindles, eReader pdb ebooks, Barnes & Noble ePubs, or Mobipocket ebooks, you must first run DeDRM application (by double-clicking it) and set some additional Preferences including:\
To remove the DRM of Kindle ebooks from eInk Kindles, Kindle for Android, other Barnes & Noble ePubs, eReader pdb ebooks, or Mobipocket ebooks, you must first run DeDRM application (by double-clicking it) and set some additional Preferences, depending on the origin of your ebook files:\
\
eInk Kindle (not Kindle Fire): 16 digit Serial Number\
Barnes & Noble ePub: Name and CC number or key file (bnepubkey.b64)\
eReader: Name and last 8 digits of CC number\
Mobipocket: 10 digit PID\
\b eInk Kindle (not Kindle Fire)
\b0 :
\b \
\b0 16 digit Serial Number, found in your Amazon account web pages.\
\b Android Kindle app
\b0 : \
backup.ab file retrieved with adb command-line tool using command\
adb backup com.amazon.kindle\
\b Barnes & Noble (not from NOOK Study)
\b0 : \
Your account email and password, so the key can be retrieved from the Barnes & Noble servers.\
An active internet connection is required for this.\
\b eReader
\b0 :\
Name and last 8 digits of CC number\
\b Mobipocket
\b0 :\
10 digit PID\
\
A final preference is the destination folder for the DRM-free copies of your ebooks that the application produces. This can be either the same folder as the original ebook, or a folder of your choice.\
\
@@ -27,7 +47,7 @@ Once these preferences have been set, you can drag and drop ebooks (or folders o
This program requires Mac OS X 10.4 or above. It will not work on Mac OS X 10.3 or earlier.\
\
\
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720
\b \cf0 Installation
\b0 \
@@ -35,7 +55,7 @@ Mac OS X 10.4
\i only
\i0 : You
\i must
\i0 first install Python 2.7.3 from http://python.org/. At the time of writing, the direct download link is http://www.python.org/ftp/python/2.7.3/python-2.7.3-macosx10.3.dmg.\
\i0 first install Python 2.7.3 or later 2.7.x version from {\field{\*\fldinst{HYPERLINK "http://python.org/"}}{\fldrslt http://python.org/}}. At the time of writing, the direct download link is {\field{\*\fldinst{HYPERLINK "http://www.python.org/ftp/python/2.7.3/python-2.7.3-macosx10.3.dmg"}}{\fldrslt http://www.python.org/ftp/python/2.7.3/python-2.7.3-macosx10.3.dmg}}.\
\
Mac OS X 10.5 and above: You do
\i not
@@ -45,7 +65,15 @@ Drag the DeDRM application from from the DeDRM_Application_Macintosh folder (the
\
\
\b Use
\b First Use for Mac OS X 10.9 and later\
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720
\b0 \cf0 The application is not signed, so the first time you run it you will need to change your security options, or hold down the option key when double-clicking on the icon, or control-click or right-button to get the contextual menu to open it.\
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720
\b \cf0 \
\
Use
\b0 \
1. To set the preferences, double-click the application and follow the instructions in the dialogs.\
2. Drag & Drop DRMed ebooks or folders of DRMed ebooks onto the application icon when it is not running.\
@@ -53,24 +81,27 @@ Drag the DeDRM application from from the DeDRM_Application_Macintosh folder (the
\
\b Troubleshooting\
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720
\b0 \cf0 A log is created on your desktop (DeDRM.log) containing detailed information from all the scripts. If you have any problems decrypting your ebooks, copy the contents of this log in a comment at Apprentice Alf's blog.\
http://apprenticealf.wordpress.com/\
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720
{\field{\*\fldinst{HYPERLINK "http://apprenticealf.wordpress.com/"}}{\fldrslt \cf0 http://apprenticealf.wordpress.com/}}\
\
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural
\b \cf0 Credits\
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural
\b Credits\
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720
\b0 \cf0 The mobidedrm and erdr2pml scripts were created by The Dark Reverser\
The i
\f1 \CocoaLigature0 gnobleepub, ignoblekeygen, ineptepub and adobe key scripts were created by
\f0 \CocoaLigature1 i
\f2 \CocoaLigature0 \uc0\u9829
\f1 cabbages\
The k4mobidedrm script and supporting scripts were written by some_updates with help from DiapDealer and Apprentice Alf, based on code by Bart Simpson (aka Skindle), CMBDTC and clarknova \
The alfcrypto library was created by some_updates\
The ePub encryption detection script was adapted by Apprentice Alf from a script by Paul Durrant\
The DeDRM all-in-one AppleScript was created by Apprentice Alf\
\b0 \cf0 The original inept and ignoble scripts were by i
\f1 \uc0\u9829
\f0 cabbages\
The original mobidedrm and erdr2pml scripts were by The Dark Reverser\
The original topaz DRM removal script was by CMBDTC\
The original topaz format conversion scripts were by some_updates, clarknova and Bart Simpson\
\
The alfcrypto library is by some_updates\
The ePub encryption detection script is by Apprentice Alf, adapted from a script by Paul Durrant\
The ignoblekey script is by Apprentice Harper\
The DeDRM AppleScript is by Apprentice Alf and Apprentice Harper, adapted from a script by Paul Durrant\
\
Many fixes, updates and enhancements to the scripts and applications have been made by many other people. For more details, see the comments in the individual scripts.\
}

View File

@@ -24,37 +24,43 @@
<key>CFBundleExecutable</key>
<string>droplet</string>
<key>CFBundleGetInfoString</key>
<string>DeDRM AppleScript 6.0.7. Written 20102013 by Apprentice Alf and others.</string>
<string>DeDRM AppleScript 6.3.4 Written 20102015 by Apprentice Alf et al.</string>
<key>CFBundleIconFile</key>
<string>DeDRM</string>
<key>CFBundleIdentifier</key>
<string>com.apple.ScriptEditor.id.707CCCD5-0C6C-4BEB-B67C-B6E866ADE85A</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0.7</string>
<string>6.0</string>
<key>CFBundleName</key>
<string>DeDRM</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>6.0.7</string>
<string>6.3.4</string>
<key>CFBundleSignature</key>
<string>dplt</string>
<key>LSRequiresCarbon</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 20102015 Apprentice Alf and Apprentice Harper</string>
<key>WindowState</key>
<dict>
<key>bundleDividerCollapsed</key>
<false/>
<key>bundlePositionOfDivider</key>
<real>1162</real>
<key>dividerCollapsed</key>
<true/>
<false/>
<key>eventLogLevel</key>
<integer>-1</integer>
<integer>0</integer>
<key>name</key>
<string>ScriptWindowState</string>
<key>positionOfDivider</key>
<real>0.0</real>
<real>652</real>
<key>savedFrame</key>
<string>85 100 1002 753 0 0 1440 878 </string>
<key>selectedTabView</key>
<string>event log</string>
<string>0 36 1680 991 0 0 1680 1027 </string>
<key>selectedTab</key>
<string>log</string>
</dict>
</dict>
</plist>

View File

@@ -22,13 +22,19 @@ li {margin-top: 0.5em}
<p>If you have upgraded from an earlier version of the plugin, any existing Barnes and Noble keys will have been automatically imported, so you might not need to do any more configuration. Continue reading for key generation and management instructions.</p>
<h3>Changes at Barnes & Noble</h3>
<p>In mid-2014, Barnes & Noble changed the way they generated encryption keys. Instead of deriving the key from the user's name and credit card number, they started generating a random key themselves, sending that key through to devices when they connected to the Barnes & Noble servers. This means that most users will now find that no combination of their name and CC# will work in decrypting their recently downloaded ebooks.</p>
<p>Someone commenting at Apprentice Alf's blog detailed a way to retrieve a new account key using the account's email address and password. This method has now been incorporated into the plugin.
<h3>Creating New Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering the necessary data to generate a new key.</p>
<ul>
<li><span class="bold">Unique Key Name:</span> this is a unique name you choose to help you identify the key. This name will show in the list of configured keys. Choose something that will help you remember the data (name, cc#) it was created with.</li>
<li><span class="bold">Your Name:</span> This is the name used by Barnes and Noble to generate your encryption key. Seemingly at random, Barnes and Noble choose one of three places from which to take this name. Most commonly, its your name as set in your Barnes &amp; Noble account, My Account page, directly under PERSONAL INFORMATION. Sometimes it is the the name used in the default shipping address, and sometimes its the name listed for the active credit card. If these names are different in your Barnes and Noble account preferences, I suggest creating one key for each version of your name. This name will not be stored anywhere on your computer or in calibre. It will only be used in the creation of the one-way hash/key thats stored in the preferences.</li>
<li><span class="bold">Credit Card#:</span> this is the default credit card number that was on file with Barnes and Noble at the time of download of the ebook to be de-DRMed. Just enter the 16 (15 for American Express) digits. As with the name, this number will not be stored anywhere on your computer or in calibre. It will only be used in the creation of the one-way hash/key thats stored in the preferences.</li>
<li><span class="bold">Unique Key Name:</span> this is a unique name you choose to help you identify the key. This name will show in the list of configured keys. Choose something that will help you remember the data (account email address) it was created with.</li>
<li><span class="bold">B&N/nook account email address:</span> This is the default email address for your Barnes and Noble/nook account. This email will not be stored anywhere on your computer or in calibre. It will only be used to fetch the account key that from the B&N server, and it is that key that will be stored in the preferences.</li>
<li><span class="bold">B&N/nook account password:</span> this is the password for your Barnes and Noble/nook account. As with the email address, this will not be stored anywhere on your computer or in calibre. It will only be used to fetch the key from the B&N server.</li>
</ul>
<p>Click the OK button to create and store the generated key. Or Cancel if you dont want to create a key.</p>
@@ -48,10 +54,15 @@ li {margin-top: 0.5em}
<h3>Importing Existing Keyfiles:</h3>
<p>At the bottom-left of the plugins customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing .b64 key files. Key files might come from being exported from this or older plugins, or may have been generated using the original i♥cabbages script.</p>
<p>At the bottom-left of the plugins customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing .b64 key files. Key files might come from being exported from this or older plugins, or may have been generated using the original i♥cabbages script, or you may have made it by following the instructions above.</p>
<p>Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.</p>
<h3>NOOK Study</h3>
<p>Books downloaded through NOOK Study may or may not use the key found using the above method. If a book is not decrypted successfully with any of the keys, the plugin will attempt to recover keys from the NOOK Study log file and use them.</p>
</body>
</html>

View File

@@ -1,4 +1,4 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
@@ -27,7 +27,7 @@ li {margin-top: 0.5em}
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering a new Kindle serial number.</p>
<ul>
<li><span class="bold">Eink Kindle Serial Number:</span> this is the unique serial number of your device. It usually starts with a B or a 9 and is sixteen characters long. For a reference of where to find serial numbers and their ranges, please refere to this <a href="http://wiki.mobileread.com/wiki/Kindle_serial_numbers">mobileread wiki page.</a></li>
<li><span class="bold">Eink Kindle Serial Number:</span> this is the unique serial number of your device. It usually starts with a B or a 9 and is sixteen characters long. For a reference of where to find serial numbers and their ranges, please refer to this <a href="http://wiki.mobileread.com/wiki/Kindle_serial_numbers">mobileread wiki page.</a></li>
</ul>
<p>Click the OK button to save the serial number. Or Cancel if you didnt want to enter a serial number.</p>

View File

@@ -17,7 +17,7 @@ p {margin-top: 0}
<body>
<h1>DeDRM Plugin <span class="version">(v6.0.0)</span></h1>
<h1>DeDRM Plugin <span class="version">(v6.3.0)</span></h1>
<p>This plugin removes DRM from ebooks when they are imported into calibre. If you already have DRMed ebooks in your calibre library, you will need to remove them and import them again.</p>

View File

@@ -0,0 +1,61 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Managing Kindle for Android Keys</title>
<style type="text/css">
span.version {font-size: 50%}
span.bold {font-weight: bold}
h3 {margin-bottom: 0}
p {margin-top: 0}
li {margin-top: 0.5em}
</style>
</head>
<body>
<h1>Managing Kindle for Android Keys</h1>
<p>Amazon's Kindle for Android application uses an internal key equivalent to an eInk Kindle's serial number. Extracting that key is a little tricky, but worth it, as it then allows the DRM to be removed from any Kindle ebooks that have been downloaded to that Android device.</p>
<p>Please note that it is not currently known whether the same applies to the Kindle application on the Kindle Fire and Fire HD.</p>
<h3>Getting the Kindle for Android backup file</h3>
<p>Obtain and install adb (Android Debug Bridge) on your computer. Details of how to do this are beyond the scope of this help file, but there are plenty of on-line guides.</p>
<p>Enable developer mode on your Android device. Again, look for an on-line guide for your device.</p>
<p>Once you have adb installed and your device in developer mode, connect your device to your computer with a USB cable and then open up a command line (Terminal on Mac OS X and cmd.exe on Windows) and enter "adb backup com.amazon.kindle" (without the quotation marks!) and press return. A file "backup.ab" should be created in your home directory.
<h3>Adding a Kindle for Android Key</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog with two main controls.
<ul>
<li><span class="bold">Choose backup file:</span> click this button and you will be prompted to find the backup.ab file you created earlier. Once selected the file will be processed to extract the decryption key, and if successful the file name will be displayed to the right of the button.</li>
<li><span class="bold">Unique Key Name:</span> this is a unique name you choose to help you identify the key. This name will show in the list of Kindle for Android keys. Enter a name that will help you remember which device this key came from.</li>
</ul>
<p>Click the OK button to store the Kindle for Android key for the current list of Kindle for Android keys. Or click Cancel if you dont want to store the key.</p>
<p>New keys are checked against the current list of keys before being added, and duplicates are discarded.</p>
<h3>Deleting Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted key in the list. You will be prompted once to be sure thats what you truly mean to do. Once gone, its permanently gone.</p>
<h3>Renaming Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will prompt you to enter a new name for the highlighted key in the list. Enter the new name for the key and click the OK button to use the new name, or Cancel to revert to the old name.</p>
<h3>Exporting Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a computers hard-drive. Use this button to export the highlighted key to a file (with a .k4a' file name extension). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.</p>
<h3>Importing Existing Keyfiles:</h3>
<p>At the bottom-left of the plugins customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import any .k4a file you obtained by using the androidkindlekey.py script manually, or by exporting from another copy of calibre.</p>
</body>
</html>

View File

@@ -31,14 +31,29 @@ __docformat__ = 'restructuredtext en'
# 6.0.3 - Fixes for Kindle for Mac and Windows non-ascii user names
# 6.0.4 - Fixes for stand-alone scripts and applications
# and pdb files in plugin and initial conversion of prefs.
# 6.0.5 - Fix a key issue
# 6.0.6 - Fix up an incorrect function call
# 6.0.7 - Error handling for incomplete PDF metadata
# 6.0.8 - Fixes a Wine key issue and topaz support
# 6.0.9 - Ported to work with newer versions of Calibre (moved to Qt5). Still supports older Qt4 versions.
# 6.1.0 - Fixed multiple books import problem and PDF import with no key problem
# 6.2.0 - Support for getting B&N key from nook Study log. Fix for UTF-8 filenames in Adobe ePubs.
# Fix for not copying needed files. Fix for getting default Adobe key for PDFs
# 6.2.1 - Fix for non-ascii Windows user names
# 6.2.2 - Added URL method for B&N/nook books
# 6.3.0 - Added in Kindle for Android serial number solution
# 6.3.1 - Version number bump for clarity
# 6.3.2 - Fixed Kindle for Android help file
# 6.3.3 - Bug fix for Kindle for PC support
# 6.3.4 - Fixes for Kindle for Android, Linux, and Kobo 3.17
"""
Decrypt DRMed ebooks.
"""
PLUGIN_NAME = u"DeDRM"
PLUGIN_VERSION_TUPLE = (6, 0, 7)
PLUGIN_VERSION_TUPLE = (6, 3, 4)
PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE])
# Include an html helpfile in the plugin's zipfile with the following name.
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
@@ -79,37 +94,27 @@ class DeDRM(FileTypePlugin):
name = PLUGIN_NAME
description = u"Removes DRM from Amazon Kindle, Adobe Adept (including Kobo), Barnes & Noble, Mobipocket and eReader ebooks. Credit given to i♥cabbages and The Dark Reverser for the original stand-alone scripts."
supported_platforms = ['linux', 'osx', 'windows']
author = u"DiapDealer, Apprentice Alf, The Dark Reverser and i♥cabbages"
author = u"Apprentice Alf, Aprentice Harper, The Dark Reverser 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','pdf','pdb','prc','mobi','azw','azw1','azw3','azw4','tpz'])
file_types = set(['epub','pdf','pdb','prc','mobi','pobi','azw','azw1','azw3','azw4','tpz'])
on_import = True
priority = 600
def initialize(self):
# convert old preferences, if necessary.
try:
from calibre_plugins.dedrm.prefs import convertprefs
convertprefs()
except:
traceback.print_exc()
def initialize(self):
"""
Dynamic modules can't be imported/loaded from a zipfile... so this routine
runs whenever the plugin gets initialized. This will extract the appropriate
Dynamic modules can't be imported/loaded from a zipfile.
So this routine will extract the appropriate
library for the target OS and copy it to the 'alfcrypto' subdirectory of
calibre's configuration directory. That 'alfcrypto' directory is then
inserted into the syspath (as the very first entry) in the run function
so the CDLL stuff will work in the alfcrypto.py script.
The extraction only happens once per version of the plugin
Also perform upgrade of preferences once per version
"""
try:
if iswindows:
names = [u"alfcrypto.dll",u"alfcrypto64.dll"]
elif isosx:
names = [u"libalfcrypto.dylib"]
else:
names = [u"libalfcrypto32.so",u"libalfcrypto64.so",u"kindlekey.py",u"adobekey.py",u"subasyncio.py"]
lib_dict = self.load_resources(names)
self.pluginsdir = os.path.join(config_dir,u"plugins")
if not os.path.exists(self.pluginsdir):
os.mkdir(self.pluginsdir)
@@ -122,9 +127,38 @@ class DeDRM(FileTypePlugin):
self.alfdir = os.path.join(self.maindir,u"libraryfiles")
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)
open(file_path,'wb').write(data)
# only continue if we've never run this version of the plugin before
self.verdir = os.path.join(self.maindir,PLUGIN_VERSION)
if not os.path.exists(self.verdir):
if iswindows:
names = [u"alfcrypto.dll",u"alfcrypto64.dll"]
elif isosx:
names = [u"libalfcrypto.dylib"]
else:
names = [u"libalfcrypto32.so",u"libalfcrypto64.so",u"kindlekey.py",u"adobekey.py",u"subasyncio.py"]
lib_dict = self.load_resources(names)
print u"{0} v{1}: Copying needed library files from plugin's zip".format(PLUGIN_NAME, PLUGIN_VERSION)
for entry, data in lib_dict.items():
file_path = os.path.join(self.alfdir, entry)
try:
os.remove(file_path)
except:
pass
try:
open(file_path,'wb').write(data)
except:
print u"{0} v{1}: Exception when copying needed library files".format(PLUGIN_NAME, PLUGIN_VERSION)
traceback.print_exc()
pass
# convert old preferences, if necessary.
from calibre_plugins.dedrm.prefs import convertprefs
convertprefs()
# mark that this version has been initialized
os.mkdir(self.verdir)
except Exception, e:
traceback.print_exc()
raise
@@ -162,7 +196,12 @@ class DeDRM(FileTypePlugin):
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)
try:
result = ignobleepub.decryptBook(userkey, inf.name, of.name)
except:
print u"{0} v{1}: Exception when trying to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
result = 1
of.close()
@@ -173,8 +212,69 @@ class DeDRM(FileTypePlugin):
print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime)
# perhaps we should see if we can get a key from a log file
print u"{0} v{1}: Looking for new NOOK Study Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
# get the default NOOK Study keys
defaultkeys = []
try:
if iswindows or isosx:
from calibre_plugins.dedrm.ignoblekey import nookkeys
defaultkeys = nookkeys()
else: # linux
from wineutils import WineGetKeys
scriptpath = os.path.join(self.alfdir,u"ignoblekey.py")
defaultkeys = WineGetKeys(scriptpath, u".b64",dedrmprefs['adobewineprefix'])
except:
print u"{0} v{1}: Exception when getting default NOOK Study Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
newkeys = []
for keyvalue in defaultkeys:
if keyvalue not in dedrmprefs['bandnkeys'].values():
newkeys.append(keyvalue)
if len(newkeys) > 0:
try:
for i,userkey in enumerate(newkeys):
print u"{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)
of = self.temporary_file(u".epub")
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
try:
result = ignobleepub.decryptBook(userkey, inf.name, of.name)
except:
print u"{0} v{1}: Exception when trying to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
result = 1
of.close()
if result == 0:
# Decryption was a success
# Store the new successful key in the defaults
print u"{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)
try:
dedrmprefs.addnamedvaluetoprefs('bandnkeys','nook_Study_key',keyvalue)
dedrmprefs.writeprefs()
print u"{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
except:
print u"{0} v{1}: Exception saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
# Return the modified PersistentTemporary file to calibre.
return of.name
print u"{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
except Exception, e:
pass
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt{2} after {3:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook),time.time()-self.starttime))
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
# import the Adobe Adept ePub handler
import calibre_plugins.dedrm.ineptepub as ineptepub
@@ -192,6 +292,8 @@ class DeDRM(FileTypePlugin):
try:
result = ineptepub.decryptBook(userkey, inf.name, of.name)
except:
print u"{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
result = 1
of.close()
@@ -222,6 +324,7 @@ class DeDRM(FileTypePlugin):
self.default_key = defaultkeys[0]
except:
print u"{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
self.default_key = u""
@@ -240,6 +343,8 @@ class DeDRM(FileTypePlugin):
try:
result = ineptepub.decryptBook(userkey, inf.name, of.name)
except:
print u"{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
result = 1
of.close()
@@ -251,7 +356,9 @@ class DeDRM(FileTypePlugin):
try:
dedrmprefs.addnamedvaluetoprefs('adeptkeys','default_key',keyvalue.encode('hex'))
dedrmprefs.writeprefs()
print u"{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
except:
print u"{0} v{1}: Exception when saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
# Return the modified PersistentTemporary file to calibre.
return of.name
@@ -262,12 +369,12 @@ class DeDRM(FileTypePlugin):
# Something went wrong with decryption.
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt{2} after {3:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook),time.time()-self.starttime))
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
# Not a Barnes & Noble nor an Adobe Adept
# Import the fixed epub.
print u"{0} v{1}: “{2}” is neither an Adobe Adept nor a Barnes & Noble encrypted ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))
return inf.name
raise DeDRMError(u"{0} v{1}: Couldn't decrypt after {2:.1f} seconds. DRM free perhaps?".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
def PDFDecrypt(self,path_to_ebook):
import calibre_plugins.dedrm.prefs as prefs
@@ -285,6 +392,8 @@ class DeDRM(FileTypePlugin):
try:
result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name)
except:
print u"{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
result = 1
of.close()
@@ -294,67 +403,73 @@ class DeDRM(FileTypePlugin):
# Return the modified PersistentTemporary file to calibre.
return of.name
# perhaps we need to get a new default ADE key
print u"{0} v{1}: Looking for new default Adobe Digital Editions Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime)
# get the default Adobe keys
defaultkeys = []
# perhaps we need to get a new default ADE key
print u"{0} v{1}: Looking for new default Adobe Digital Editions Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
# get the default Adobe keys
defaultkeys = []
try:
if iswindows or isosx:
import calibre_plugins.dedrm.adobekey as adobe
from calibre_plugins.dedrm.adobekey import adeptkeys
try:
defaultkeys = adobe.adeptkeys()
except:
pass
else:
# linux
try:
from wineutils import WineGetKeys
defaultkeys = adeptkeys()
else: # linux
from wineutils import WineGetKeys
scriptpath = os.path.join(self.alfdir,u"adobekey.py")
defaultkeys = self.WineGetKeys(scriptpath, u".der",dedrmprefs['adobewineprefix'])
except:
pass
scriptpath = os.path.join(self.alfdir,u"adobekey.py")
defaultkeys = WineGetKeys(scriptpath, u".der",dedrmprefs['adobewineprefix'])
newkeys = []
for keyvalue in defaultkeys:
if keyvalue.encode('hex') not in dedrmprefs['adeptkeys'].values():
newkeys.append(keyvalue)
self.default_key = defaultkeys[0]
except:
print u"{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
self.default_key = u""
if len(newkeys) > 0:
try:
for i,userkey in enumerate(newkeys):
print u"{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)
of = self.temporary_file(u".pdf")
newkeys = []
for keyvalue in defaultkeys:
if keyvalue.encode('hex') not in dedrmprefs['adeptkeys'].values():
newkeys.append(keyvalue)
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
if len(newkeys) > 0:
try:
for i,userkey in enumerate(newkeys):
print u"{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)
of = self.temporary_file(u".pdf")
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
try:
result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name)
except:
print u"{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
result = 1
of.close()
if result == 0:
# Decryption was a success
# Store the new successful key in the defaults
print u"{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)
try:
result = ineptepdf.decryptBook(userkey, inf.name, of.name)
dedrmprefs.addnamedvaluetoprefs('adeptkeys','default_key',keyvalue.encode('hex'))
dedrmprefs.writeprefs()
print u"{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
except:
result = 1
print u"{0} v{1}: Exception when saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
# Return the modified PersistentTemporary file to calibre.
return of.name
of.close()
if result == 0:
# Decryption was a success
# Store the new successful key in the defaults
print u"{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)
try:
dedrmprefs.addnamedvaluetoprefs('adeptkeys','default_key',keyvalue.encode('hex'))
dedrmprefs.writeprefs()
except:
traceback.print_exc()
# Return the modified PersistentTemporary file to calibre.
return of.name
print u"{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
except Exception, e:
pass
print u"{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
except Exception, e:
pass
# Something went wrong with decryption.
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt{2} after {3:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook),time.time()-self.starttime))
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
def KindleMobiDecrypt(self,path_to_ebook):
@@ -371,10 +486,15 @@ class DeDRM(FileTypePlugin):
dedrmprefs = prefs.DeDRM_Prefs()
pids = dedrmprefs['pids']
serials = dedrmprefs['serials']
for android_serials_list in dedrmprefs['androidkeys'].values():
#print android_serials_list
serials.extend(android_serials_list)
#print serials
androidFiles = []
kindleDatabases = dedrmprefs['kindlekeys'].items()
try:
book = k4mobidedrm.GetDecryptedBook(path_to_ebook,kindleDatabases,serials,pids,self.starttime)
book = k4mobidedrm.GetDecryptedBook(path_to_ebook,kindleDatabases,androidFiles,serials,pids,self.starttime)
except Exception, e:
decoded = False
# perhaps we need to get a new default Kindle for Mac/PC key
@@ -391,8 +511,10 @@ class DeDRM(FileTypePlugin):
from wineutils import WineGetKeys
scriptpath = os.path.join(self.alfdir,u"kindlekey.py")
defaultkeys = self.WineGetKeys(scriptpath, u".k4i",dedrmprefs['kindlewineprefix'])
defaultkeys = WineGetKeys(scriptpath, u".k4i",dedrmprefs['kindlewineprefix'])
except:
print u"{0} v{1}: Exception when getting default Kindle Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
pass
newkeys = {}
@@ -403,7 +525,7 @@ class DeDRM(FileTypePlugin):
if len(newkeys) > 0:
print u"{0} v{1}: Found {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), u"key" if len(newkeys)==1 else u"keys")
try:
book = k4mobidedrm.GetDecryptedBook(path_to_ebook,newkeys.items(),[],[],self.starttime)
book = k4mobidedrm.GetDecryptedBook(path_to_ebook,newkeys.items(),[],[],[],self.starttime)
decoded = True
# store the new successful keys in the defaults
print u"{0} v{1}: Saving {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), u"key" if len(newkeys)==1 else u"keys")
@@ -415,8 +537,7 @@ class DeDRM(FileTypePlugin):
if not decoded:
#if you reached here then no luck raise and exception
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
traceback.print_exc()
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt “{4}” after {3:.1f} seconds with error: {2}\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0],time.time()-self.starttime,os.path.basename(path_to_ebook)))
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
of = self.temporary_file(book.getBookExtension())
book.getFile(of.name)
@@ -445,12 +566,13 @@ class DeDRM(FileTypePlugin):
# Decryption was successful return the modified PersistentTemporary
# file to Calibre's import process.
if result == 0:
print u"{0} v{1}: Successfully decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime)
return of.name
print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime)
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt{2} after {3:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook),time.time()-self.starttime))
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
def run(self, path_to_ebook):
@@ -463,7 +585,7 @@ class DeDRM(FileTypePlugin):
self.starttime = time.time()
booktype = os.path.splitext(path_to_ebook)[1].lower()[1:]
if booktype in ['prc','mobi','azw','azw1','azw3','azw4','tpz']:
if booktype in ['prc','mobi','pobi','azw','azw1','azw3','azw4','tpz']:
# Kindle/Mobipocket
decrypted_ebook = self.KindleMobiDecrypt(path_to_ebook)
elif booktype == 'pdb':
@@ -480,7 +602,7 @@ class DeDRM(FileTypePlugin):
else:
print u"Unknown booktype {0}. Passing back to calibre unchanged".format(booktype)
return path_to_ebook
print u"{0} v{1}: Successfully decrypted book after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
print u"{0} v{1}: Finished after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
return decrypted_ebook
def is_customizable(self):

View File

@@ -0,0 +1,75 @@
import sys
import Tkinter
import Tkconstants
class ActivityBar(Tkinter.Frame):
def __init__(self, master, length=300, height=20, barwidth=15, interval=50, bg='white', fillcolor='orchid1',\
bd=2, relief=Tkconstants.GROOVE, *args, **kw):
Tkinter.Frame.__init__(self, master, bg=bg, width=length, height=height, *args, **kw)
self._master = master
self._interval = interval
self._maximum = length
self._startx = 0
self._barwidth = barwidth
self._bardiv = length / barwidth
if self._bardiv < 10:
self._bardiv = 10
stopx = self._startx + self._barwidth
if stopx > self._maximum:
stopx = self._maximum
# self._canv = Tkinter.Canvas(self, bg=self['bg'], width=self['width'], height=self['height'],\
# highlightthickness=0, relief='flat', bd=0)
self._canv = Tkinter.Canvas(self, bg=self['bg'], width=self['width'], height=self['height'],\
highlightthickness=0, relief=relief, bd=bd)
self._canv.pack(fill='both', expand=1)
self._rect = self._canv.create_rectangle(0, 0, self._canv.winfo_reqwidth(), self._canv.winfo_reqheight(), fill=fillcolor, width=0)
self._set()
self.bind('<Configure>', self._update_coords)
self._running = False
def _update_coords(self, event):
'''Updates the position of the rectangle inside the canvas when the size of
the widget gets changed.'''
# looks like we have to call update_idletasks() twice to make sure
# to get the results we expect
self._canv.update_idletasks()
self._maximum = self._canv.winfo_width()
self._startx = 0
self._barwidth = self._maximum / self._bardiv
if self._barwidth < 2:
self._barwidth = 2
stopx = self._startx + self._barwidth
if stopx > self._maximum:
stopx = self._maximum
self._canv.coords(self._rect, 0, 0, stopx, self._canv.winfo_height())
self._canv.update_idletasks()
def _set(self):
if self._startx < 0:
self._startx = 0
if self._startx > self._maximum:
self._startx = self._startx % self._maximum
stopx = self._startx + self._barwidth
if stopx > self._maximum:
stopx = self._maximum
self._canv.coords(self._rect, self._startx, 0, stopx, self._canv.winfo_height())
self._canv.update_idletasks()
def start(self):
self._running = True
self.after(self._interval, self._step)
def stop(self):
self._running = False
self._set()
def _step(self):
if self._running:
stepsize = self._barwidth / 4
if stepsize < 2:
stepsize = 2
self._startx += stepsize
self._set()
self.after(self._interval, self._step)

View File

@@ -0,0 +1,468 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import with_statement
# androidkindlekey.py
# Copyright © 2013-15 by Thom and Apprentice Harper
# Some portions Copyright © 2010-15 by some_updates and Apprentice Alf
#
# Revision history:
# 1.0 - AmazonSecureStorage.xml decryption to serial number
# 1.1 - map_data_storage.db decryption to serial number
# 1.2 - Changed to be callable from AppleScript by returning only serial number
# - and changed name to androidkindlekey.py
# - and added in unicode command line support
# 1.3 - added in TkInter interface, output to a file
# 1.4 - Fix some problems identified by Aldo Bleeker
# 1.5 - Fix another problem identified by Aldo Bleeker
"""
Retrieve Kindle for Android Serial Number.
"""
__license__ = 'GPL v3'
__version__ = '1.5'
import os
import sys
import traceback
import getopt
import tempfile
import zlib
import tarfile
from hashlib import md5
from cStringIO import StringIO
from binascii import a2b_hex, b2a_hex
# Routines common to Mac and PC
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
# encoded using "replace" before writing them.
class SafeUnbuffered:
def __init__(self, stream):
self.stream = stream
self.encoding = stream.encoding
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
try:
from calibre.constants import iswindows, isosx
except:
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
def unicode_argv():
if iswindows:
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
# as a list of Unicode strings and encode them as utf-8
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"kindlekey.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
class DrmException(Exception):
pass
STORAGE = u"backup.ab"
STORAGE1 = u"AmazonSecureStorage.xml"
STORAGE2 = u"map_data_storage.db"
class AndroidObfuscation(object):
'''AndroidObfuscation
For the key, it's written in java, and run in android dalvikvm
'''
key = a2b_hex('0176e04c9408b1702d90be333fd53523')
def encrypt(self, plaintext):
cipher = self._get_cipher()
padding = len(self.key) - len(plaintext) % len(self.key)
plaintext += chr(padding) * padding
return b2a_hex(cipher.encrypt(plaintext))
def decrypt(self, ciphertext):
cipher = self._get_cipher()
plaintext = cipher.decrypt(a2b_hex(ciphertext))
return plaintext[:-ord(plaintext[-1])]
def _get_cipher(self):
try:
from Crypto.Cipher import AES
return AES.new(self.key)
except ImportError:
from aescbc import AES, noPadding
return AES(self.key, padding=noPadding())
class AndroidObfuscationV2(AndroidObfuscation):
'''AndroidObfuscationV2
'''
count = 503
password = 'Thomsun was here!'
def __init__(self, salt):
key = self.password + salt
for _ in range(self.count):
key = md5(key).digest()
self.key = key[:8]
self.iv = key[8:16]
def _get_cipher(self):
try :
from Crypto.Cipher import DES
return DES.new(self.key, DES.MODE_CBC, self.iv)
except ImportError:
from python_des import Des, CBC
return Des(self.key, CBC, self.iv)
def parse_preference(path):
''' parse android's shared preference xml '''
storage = {}
read = open(path)
for line in read:
line = line.strip()
# <string name="key">value</string>
if line.startswith('<string name="'):
index = line.find('"', 14)
key = line[14:index]
value = line[index+2:-9]
storage[key] = value
read.close()
return storage
def get_serials1(path=STORAGE1):
''' get serials from android's shared preference xml '''
if not os.path.isfile(path):
return []
storage = parse_preference(path)
salt = storage.get('AmazonSaltKey')
if salt and len(salt) == 16:
obfuscation = AndroidObfuscationV2(a2b_hex(salt))
else:
obfuscation = AndroidObfuscation()
def get_value(key):
encrypted_key = obfuscation.encrypt(key)
encrypted_value = storage.get(encrypted_key)
if encrypted_value:
return obfuscation.decrypt(encrypted_value)
return ''
# also see getK4Pids in kgenpids.py
try:
dsnid = get_value('DsnId')
except:
sys.stderr.write('cannot get DsnId\n')
return []
try:
tokens = set(get_value('kindle.account.tokens').split(','))
except:
return []
serials = []
if dsnid:
serials.append(dsnid)
for token in tokens:
if token:
serials.append('%s%s' % (dsnid, token))
serials.append(token)
return serials
def get_serials2(path=STORAGE2):
''' get serials from android's sql database '''
if not os.path.isfile(path):
return []
import sqlite3
connection = sqlite3.connect(path)
cursor = connection.cursor()
cursor.execute('''select userdata_value from userdata where userdata_key like '%/%token.device.deviceserialname%' ''')
userdata_keys = cursor.fetchall()
dsns = []
for userdata_row in userdata_keys:
try:
if userdata_row and userdata_row[0]:
userdata_utf8 = userdata_row[0].encode('utf8')
if len(userdata_utf8) > 0:
dsns.append(userdata_utf8)
except:
print "Error getting one of the device serial name keys"
traceback.print_exc()
pass
dsns = list(set(dsns))
cursor.execute('''select userdata_value from userdata where userdata_key like '%/%kindle.account.tokens%' ''')
userdata_keys = cursor.fetchall()
tokens = []
for userdata_row in userdata_keys:
try:
if userdata_row and userdata_row[0]:
userdata_utf8 = userdata_row[0].encode('utf8')
if len(userdata_utf8) > 0:
tokens.append(userdata_utf8)
except:
print "Error getting one of the account token keys"
traceback.print_exc()
pass
tokens = list(set(tokens))
serials = []
for x in dsns:
serials.append(x)
for y in tokens:
serials.append('%s%s' % (x, y))
for y in tokens:
serials.append(y)
return serials
def get_serials(path=STORAGE):
'''get serials from files in from android backup.ab
backup.ab can be get using adb command:
shell> adb backup com.amazon.kindle
or from individual files if they're passed.
'''
if not os.path.isfile(path):
return []
basename = os.path.basename(path)
if basename == STORAGE1:
return get_serials1(path)
elif basename == STORAGE2:
return get_serials2(path)
output = None
try :
read = open(path, 'rb')
head = read.read(24)
if head[:14] == 'ANDROID BACKUP':
output = StringIO(zlib.decompress(read.read()))
except Exception:
pass
finally:
read.close()
if not output:
return []
serials = []
tar = tarfile.open(fileobj=output)
for member in tar.getmembers():
if member.name.strip().endswith(STORAGE1):
write = tempfile.NamedTemporaryFile(mode='wb', delete=False)
write.write(tar.extractfile(member).read())
write.close()
write_path = os.path.abspath(write.name)
serials.extend(get_serials1(write_path))
os.remove(write_path)
elif member.name.strip().endswith(STORAGE2):
write = tempfile.NamedTemporaryFile(mode='wb', delete=False)
write.write(tar.extractfile(member).read())
write.close()
write_path = os.path.abspath(write.name)
serials.extend(get_serials2(write_path))
os.remove(write_path)
return list(set(serials))
__all__ = [ 'get_serials', 'getkey']
# procedure for CLI and GUI interfaces
# returns single or multiple keys (one per line) in the specified file
def getkey(outfile, inpath):
keys = get_serials(inpath)
if len(keys) > 0:
with file(outfile, 'w') as keyfileout:
for key in keys:
keyfileout.write(key)
keyfileout.write("\n")
return True
return False
def usage(progname):
print u"Decrypts the serial number(s) of Kindle For Android from Android backup or file"
print u"Get backup.ab file using adb backup com.amazon.kindle for Android 4.0+."
print u"Otherwise extract AmazonSecureStorage.xml from /data/data/com.amazon.kindle/shared_prefs/AmazonSecureStorage.xml"
print u"Or map_data_storage.db from /data/data/com.amazon.kindle/databases/map_data_storage.db"
print u""
print u"Usage:"
print u" {0:s} [-h] [-b <backup.ab>] [<outfile.k4a>]".format(progname)
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv()
progname = os.path.basename(argv[0])
print u"{0} v{1}\nCopyright © 2010-2015 Thom, some_updates, Apprentice Alf and Apprentice Harper".format(progname,__version__)
try:
opts, args = getopt.getopt(argv[1:], "hb:")
except getopt.GetoptError, err:
usage(progname)
print u"\nError in options or arguments: {0}".format(err.args[0])
return 2
inpath = ""
for o, a in opts:
if o == "-h":
usage(progname)
return 0
if o == "-b":
inpath = a
if len(args) > 1:
usage(progname)
return 2
if len(args) == 1:
# save to the specified file or directory
outfile = args[0]
if not os.path.isabs(outfile):
outfile = os.path.join(os.path.dirname(argv[0]),outfile)
outfile = os.path.abspath(outfile)
if os.path.isdir(outfile):
outfile = os.path.join(os.path.dirname(argv[0]),"androidkindlekey.k4a")
else:
# save to the same directory as the script
outfile = os.path.join(os.path.dirname(argv[0]),"androidkindlekey.k4a")
# make sure the outpath is OK
outfile = os.path.realpath(os.path.normpath(outfile))
if not os.path.isfile(inpath):
usage(progname)
print u"\n{0:s} file not found".format(inpath)
return 2
if getkey(outfile, inpath):
print u"\nSaved Kindle for Android key to {0}".format(outfile)
else:
print u"\nCould not retrieve Kindle for Android key."
return 0
def gui_main():
try:
import Tkinter
import Tkconstants
import tkMessageBox
import tkFileDialog
except:
print "Tkinter not installed"
return cli_main()
class DecryptionDialog(Tkinter.Frame):
def __init__(self, root):
Tkinter.Frame.__init__(self, root, border=5)
self.status = Tkinter.Label(self, text=u"Select backup.ab file")
self.status.pack(fill=Tkconstants.X, expand=1)
body = Tkinter.Frame(self)
body.pack(fill=Tkconstants.X, expand=1)
sticky = Tkconstants.E + Tkconstants.W
body.grid_columnconfigure(1, weight=2)
Tkinter.Label(body, text=u"Backup file").grid(row=0, column=0)
self.keypath = Tkinter.Entry(body, width=40)
self.keypath.grid(row=0, column=1, sticky=sticky)
self.keypath.insert(2, u"backup.ab")
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
button.grid(row=0, column=2)
buttons = Tkinter.Frame(self)
buttons.pack()
button2 = Tkinter.Button(
buttons, text=u"Extract", width=10, command=self.generate)
button2.pack(side=Tkconstants.LEFT)
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
button3 = Tkinter.Button(
buttons, text=u"Quit", width=10, command=self.quit)
button3.pack(side=Tkconstants.RIGHT)
def get_keypath(self):
keypath = tkFileDialog.askopenfilename(
parent=None, title=u"Select backup.ab file",
defaultextension=u".ab",
filetypes=[('adb backup com.amazon.kindle', '.ab'),
('All Files', '.*')])
if keypath:
keypath = os.path.normpath(keypath)
self.keypath.delete(0, Tkconstants.END)
self.keypath.insert(0, keypath)
return
def generate(self):
inpath = self.keypath.get()
self.status['text'] = u"Getting key..."
try:
keys = get_serials(inpath)
keycount = 0
for key in keys:
while True:
keycount += 1
outfile = os.path.join(progpath,u"kindlekey{0:d}.k4a".format(keycount))
if not os.path.exists(outfile):
break
with file(outfile, 'w') as keyfileout:
keyfileout.write(key)
success = True
tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile))
except Exception, e:
self.status['text'] = u"Error: {0}".format(e.args[0])
return
self.status['text'] = u"Select backup.ab file"
argv=unicode_argv()
progpath, progname = os.path.split(argv[0])
root = Tkinter.Tk()
root.title(u"Kindle for Android Key Extraction v.{0}".format(__version__))
root.resizable(True, False)
root.minsize(300, 0)
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
root.mainloop()
return 0
if __name__ == '__main__':
if len(sys.argv) > 1:
sys.exit(cli_main())
sys.exit(gui_main())

View File

@@ -0,0 +1,88 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys, os
import locale
import codecs
# get sys.argv arguments and encode them into utf-8
def unicode_argv():
if sys.platform.startswith('win'):
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'.
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"DeDRM.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
def add_cp65001_codec():
try:
codecs.lookup('cp65001')
except LookupError:
codecs.register(
lambda name: name == 'cp65001' and codecs.lookup('utf-8') or None)
return
def set_utf8_default_encoding():
if sys.getdefaultencoding() == 'utf-8':
return
# Regenerate setdefaultencoding.
reload(sys)
sys.setdefaultencoding('utf-8')
for attr in dir(locale):
if attr[0:3] != 'LC_':
continue
aref = getattr(locale, attr)
try:
locale.setlocale(aref, '')
except locale.Error:
continue
try:
lang = locale.getlocale(aref)[0]
except (TypeError, ValueError):
continue
if lang:
try:
locale.setlocale(aref, (lang, 'UTF-8'))
except locale.Error:
os.environ[attr] = lang + '.UTF-8'
try:
locale.setlocale(locale.LC_ALL, '')
except locale.Error:
pass
return

View File

@@ -0,0 +1,211 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
# to work around tk_chooseDirectory not properly returning unicode paths on Windows
# need to use a dialog that can be hacked up to actually return full unicode paths
# originally based on AskFolder from EasyDialogs for Windows but modified to fix it
# to actually use unicode for path
# The original license for EasyDialogs is as follows
#
# Copyright (c) 2003-2005 Jimmy Retzlaff
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
"""
AskFolder(...) -- Ask the user to select a folder Windows specific
"""
import os
import ctypes
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
import ctypes.wintypes as wintypes
__all__ = ['AskFolder']
# Load required Windows DLLs
ole32 = ctypes.windll.ole32
shell32 = ctypes.windll.shell32
user32 = ctypes.windll.user32
# Windows Constants
BFFM_INITIALIZED = 1
BFFM_SETOKTEXT = 1129
BFFM_SETSELECTIONA = 1126
BFFM_SETSELECTIONW = 1127
BIF_EDITBOX = 16
BS_DEFPUSHBUTTON = 1
CB_ADDSTRING = 323
CB_GETCURSEL = 327
CB_SETCURSEL = 334
CDM_SETCONTROLTEXT = 1128
EM_GETLINECOUNT = 186
EM_GETMARGINS = 212
EM_POSFROMCHAR = 214
EM_SETSEL = 177
GWL_STYLE = -16
IDC_STATIC = -1
IDCANCEL = 2
IDNO = 7
IDOK = 1
IDYES = 6
MAX_PATH = 260
OFN_ALLOWMULTISELECT = 512
OFN_ENABLEHOOK = 32
OFN_ENABLESIZING = 8388608
OFN_ENABLETEMPLATEHANDLE = 128
OFN_EXPLORER = 524288
OFN_OVERWRITEPROMPT = 2
OPENFILENAME_SIZE_VERSION_400 = 76
PBM_GETPOS = 1032
PBM_SETMARQUEE = 1034
PBM_SETPOS = 1026
PBM_SETRANGE = 1025
PBM_SETRANGE32 = 1030
PBS_MARQUEE = 8
PM_REMOVE = 1
SW_HIDE = 0
SW_SHOW = 5
SW_SHOWNORMAL = 1
SWP_NOACTIVATE = 16
SWP_NOMOVE = 2
SWP_NOSIZE = 1
SWP_NOZORDER = 4
VER_PLATFORM_WIN32_NT = 2
WM_COMMAND = 273
WM_GETTEXT = 13
WM_GETTEXTLENGTH = 14
WM_INITDIALOG = 272
WM_NOTIFY = 78
# Windows function prototypes
BrowseCallbackProc = ctypes.WINFUNCTYPE(ctypes.c_int, wintypes.HWND, ctypes.c_uint, wintypes.LPARAM, wintypes.LPARAM)
# Windows types
LPCTSTR = ctypes.c_char_p
LPTSTR = ctypes.c_char_p
LPVOID = ctypes.c_voidp
TCHAR = ctypes.c_char
class BROWSEINFO(ctypes.Structure):
_fields_ = [
("hwndOwner", wintypes.HWND),
("pidlRoot", LPVOID),
("pszDisplayName", LPTSTR),
("lpszTitle", LPCTSTR),
("ulFlags", ctypes.c_uint),
("lpfn", BrowseCallbackProc),
("lParam", wintypes.LPARAM),
("iImage", ctypes.c_int)
]
# Utilities
def CenterWindow(hwnd):
desktopRect = GetWindowRect(user32.GetDesktopWindow())
myRect = GetWindowRect(hwnd)
x = width(desktopRect) // 2 - width(myRect) // 2
y = height(desktopRect) // 2 - height(myRect) // 2
user32.SetWindowPos(hwnd, 0,
desktopRect.left + x,
desktopRect.top + y,
0, 0,
SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER
)
def GetWindowRect(hwnd):
rect = wintypes.RECT()
user32.GetWindowRect(hwnd, ctypes.byref(rect))
return rect
def width(rect):
return rect.right-rect.left
def height(rect):
return rect.bottom-rect.top
def AskFolder(
message=None,
version=None,
defaultLocation=None,
location=None,
windowTitle=None,
actionButtonLabel=None,
cancelButtonLabel=None,
multiple=None):
"""Display a dialog asking the user for select a folder.
modified to use unicode strings as much as possible
returns unicode path
"""
def BrowseCallback(hwnd, uMsg, lParam, lpData):
if uMsg == BFFM_INITIALIZED:
if actionButtonLabel:
label = unicode(actionButtonLabel, errors='replace')
user32.SendMessageW(hwnd, BFFM_SETOKTEXT, 0, label)
if cancelButtonLabel:
label = unicode(cancelButtonLabel, errors='replace')
cancelButton = user32.GetDlgItem(hwnd, IDCANCEL)
if cancelButton:
user32.SetWindowTextW(cancelButton, label)
if windowTitle:
title = unicode(windowTitle, erros='replace')
user32.SetWindowTextW(hwnd, title)
if defaultLocation:
user32.SendMessageW(hwnd, BFFM_SETSELECTIONW, 1, defaultLocation.replace('/', '\\'))
if location:
x, y = location
desktopRect = wintypes.RECT()
user32.GetWindowRect(0, ctypes.byref(desktopRect))
user32.SetWindowPos(hwnd, 0,
desktopRect.left + x,
desktopRect.top + y, 0, 0,
SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER)
else:
CenterWindow(hwnd)
return 0
# This next line is needed to prevent gc of the callback
callback = BrowseCallbackProc(BrowseCallback)
browseInfo = BROWSEINFO()
browseInfo.pszDisplayName = ctypes.c_char_p('\0' * (MAX_PATH+1))
browseInfo.lpszTitle = message
browseInfo.lpfn = callback
pidl = shell32.SHBrowseForFolder(ctypes.byref(browseInfo))
if not pidl:
result = None
else:
path = LPCWSTR(u" " * (MAX_PATH+1))
shell32.SHGetPathFromIDListW(pidl, path)
ole32.CoTaskMemFree(pidl)
result = path.value
return result

View File

@@ -9,16 +9,24 @@ __license__ = 'GPL v3'
import os, traceback, json
# PyQT4 modules (part of calibre).
from PyQt4.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit,
try:
from PyQt5.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit,
QGroupBox, QPushButton, QListWidget, QListWidgetItem,
QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl, QString)
from PyQt4 import QtGui
QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl)
except ImportError:
from PyQt4.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit,
QGroupBox, QPushButton, QListWidget, QListWidgetItem,
QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl)
try:
from PyQt5 import Qt as QtGui
except ImportError:
from PyQt4 import QtGui
from zipfile import ZipFile
# calibre modules and constants.
from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url,
choose_dir, choose_files)
choose_dir, choose_files, choose_save_file)
from calibre.utils.config import dynamic, config_dir, JSONConfig
from calibre.constants import iswindows, isosx
@@ -28,6 +36,7 @@ from calibre_plugins.dedrm.__init__ import RESOURCE_NAME as help_file_name
from calibre_plugins.dedrm.utilities import uStrCmp
import calibre_plugins.dedrm.prefs as prefs
import calibre_plugins.dedrm.androidkindlekey as androidkindlekey
class ConfigWidget(QWidget):
def __init__(self, plugin_path, alfdir):
@@ -45,6 +54,7 @@ class ConfigWidget(QWidget):
self.tempdedrmprefs['adeptkeys'] = self.dedrmprefs['adeptkeys'].copy()
self.tempdedrmprefs['ereaderkeys'] = self.dedrmprefs['ereaderkeys'].copy()
self.tempdedrmprefs['kindlekeys'] = self.dedrmprefs['kindlekeys'].copy()
self.tempdedrmprefs['androidkeys'] = self.dedrmprefs['androidkeys'].copy()
self.tempdedrmprefs['pids'] = list(self.dedrmprefs['pids'])
self.tempdedrmprefs['serials'] = list(self.dedrmprefs['serials'])
self.tempdedrmprefs['adobewineprefix'] = self.dedrmprefs['adobewineprefix']
@@ -75,6 +85,10 @@ class ConfigWidget(QWidget):
self.bandn_button.setToolTip(_(u"Click to manage keys for Barnes and Noble ebooks"))
self.bandn_button.setText(u"Barnes and Noble ebooks")
self.bandn_button.clicked.connect(self.bandn_keys)
self.kindle_android_button = QtGui.QPushButton(self)
self.kindle_android_button.setToolTip(_(u"Click to manage keys for Kindle for Android ebooks"))
self.kindle_android_button.setText(u"Kindle for Android ebooks")
self.kindle_android_button.clicked.connect(self.kindle_android)
self.kindle_serial_button = QtGui.QPushButton(self)
self.kindle_serial_button.setToolTip(_(u"Click to manage eInk Kindle serial numbers for Kindle ebooks"))
self.kindle_serial_button.setText(u"eInk Kindle ebooks")
@@ -96,6 +110,7 @@ class ConfigWidget(QWidget):
self.ereader_button.setText(u"eReader ebooks")
self.ereader_button.clicked.connect(self.ereader_keys)
button_layout.addWidget(self.kindle_serial_button)
button_layout.addWidget(self.kindle_android_button)
button_layout.addWidget(self.bandn_button)
button_layout.addWidget(self.mobi_button)
button_layout.addWidget(self.ereader_button)
@@ -107,6 +122,10 @@ class ConfigWidget(QWidget):
def kindle_serials(self):
d = ManageKeysDialog(self,u"EInk Kindle Serial Number",self.tempdedrmprefs['serials'], AddSerialDialog)
d.exec_()
def kindle_android(self):
d = ManageKeysDialog(self,u"Kindle for Android Key",self.tempdedrmprefs['androidkeys'], AddAndroidDialog, 'k4a')
d.exec_()
def kindle_keys(self):
if isosx or iswindows:
@@ -154,6 +173,7 @@ class ConfigWidget(QWidget):
self.dedrmprefs.set('adeptkeys', self.tempdedrmprefs['adeptkeys'])
self.dedrmprefs.set('ereaderkeys', self.tempdedrmprefs['ereaderkeys'])
self.dedrmprefs.set('kindlekeys', self.tempdedrmprefs['kindlekeys'])
self.dedrmprefs.set('androidkeys', self.tempdedrmprefs['androidkeys'])
self.dedrmprefs.set('pids', self.tempdedrmprefs['pids'])
self.dedrmprefs.set('serials', self.tempdedrmprefs['serials'])
self.dedrmprefs.set('adobewineprefix', self.tempdedrmprefs['adobewineprefix'])
@@ -180,6 +200,7 @@ class ManageKeysDialog(QDialog):
self.import_key = (keyfile_ext != u"")
self.binary_file = (keyfile_ext == u"der")
self.json_file = (keyfile_ext == u"k4i")
self.android_file = (keyfile_ext == u"k4a")
self.wineprefix = wineprefix
self.setWindowTitle("{0} {1}: Manage {2}s".format(PLUGIN_NAME, PLUGIN_VERSION, self.key_type_name))
@@ -211,8 +232,8 @@ class ManageKeysDialog(QDialog):
button_layout = QVBoxLayout()
keys_group_box_layout.addLayout(button_layout)
self._add_key_button = QtGui.QToolButton(self)
self._add_key_button.setToolTip(u"Create new {0}".format(self.key_type_name))
self._add_key_button.setIcon(QIcon(I('plus.png')))
self._add_key_button.setToolTip(u"Create new {0}".format(self.key_type_name))
self._add_key_button.clicked.connect(self.add_key)
button_layout.addWidget(self._add_key_button)
@@ -267,7 +288,7 @@ class ManageKeysDialog(QDialog):
def getwineprefix(self):
if self.wineprefix is not None:
return unicode(self.wp_lineedit.text().toUtf8(), 'utf8').strip()
return unicode(self.wp_lineedit.text()).strip()
return u""
def populate_list(self):
@@ -316,7 +337,7 @@ class ManageKeysDialog(QDialog):
if d.result() != d.Accepted:
# rename cancelled or moot.
return
keyname = unicode(self.listy.currentItem().text().toUtf8(),'utf8')
keyname = unicode(self.listy.currentItem().text())
if not question_dialog(self, "{0} {1}: Confirm Rename".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to rename the {2} named <strong>{0}</strong> to <strong>{1}</strong>?".format(keyname,d.key_name,self.key_type_name), show_copy_button=False, default_yes=False):
return
self.plugin_keys[d.key_name] = self.plugin_keys[keyname]
@@ -328,7 +349,7 @@ class ManageKeysDialog(QDialog):
def delete_key(self):
if not self.listy.currentItem():
return
keyname = unicode(self.listy.currentItem().text().toUtf8(), 'utf8')
keyname = unicode(self.listy.currentItem().text())
if not question_dialog(self, "{0} {1}: Confirm Delete".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to delete the {1} <strong>{0}</strong>?".format(keyname, self.key_type_name), show_copy_button=False, default_yes=False):
return
if type(self.plugin_keys) == dict:
@@ -352,9 +373,10 @@ class ManageKeysDialog(QDialog):
open_url(QUrl(url))
def migrate_files(self):
dynamic[PLUGIN_NAME + u"config_dir"] = config_dir
files = choose_files(self, PLUGIN_NAME + u"config_dir",
u"Select {0} files to import".format(self.key_type_name), [(u"{0} files".format(self.key_type_name), [self.keyfile_ext])], False)
unique_dlg_name = PLUGIN_NAME + u"import {0} keys".format(self.key_type_name).replace(' ', '_') #takes care of automatically remembering last directory
caption = u"Select {0} files to import".format(self.key_type_name)
filters = [(u"{0} files".format(self.key_type_name), [self.keyfile_ext])]
files = choose_files(self, unique_dlg_name, caption, filters, all_files=False)
counter = 0
skipped = 0
if files:
@@ -368,6 +390,9 @@ class ManageKeysDialog(QDialog):
new_key_value = new_key_value.encode('hex')
elif self.json_file:
new_key_value = json.loads(new_key_value)
elif self.android_file:
# convert to list of the keys in the string
new_key_value = new_key_value.splitlines()
match = False
for key in self.plugin_keys.keys():
if uStrCmp(new_key_name, key, True):
@@ -386,7 +411,7 @@ class ManageKeysDialog(QDialog):
else:
counter += 1
self.plugin_keys[new_key_name] = new_key_value
msg = u""
if counter+skipped > 1:
if counter > 0:
@@ -408,21 +433,22 @@ class ManageKeysDialog(QDialog):
r = error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
_(errmsg), show=True, show_copy_button=False)
return
filter = QString(u"{0} Files (*.{1})".format(self.key_type_name, self.keyfile_ext))
keyname = unicode(self.listy.currentItem().text().toUtf8(), 'utf8')
if dynamic.get(PLUGIN_NAME + 'save_dir'):
defaultname = os.path.join(dynamic.get(PLUGIN_NAME + 'save_dir'), u"{0}.{1}".format(keyname , self.keyfile_ext))
else:
defaultname = os.path.join(os.path.expanduser('~'), u"{0}.{1}".format(keyname , self.keyfile_ext))
filename = unicode(QtGui.QFileDialog.getSaveFileName(self, u"Save {0} File as...".format(self.key_type_name), defaultname,
u"{0} Files (*.{1})".format(self.key_type_name,self.keyfile_ext), filter))
keyname = unicode(self.listy.currentItem().text())
unique_dlg_name = PLUGIN_NAME + u"export {0} keys".format(self.key_type_name).replace(' ', '_') #takes care of automatically remembering last directory
caption = u"Save {0} File as...".format(self.key_type_name)
filters = [(u"{0} Files".format(self.key_type_name), [u"{0}".format(self.keyfile_ext)])]
defaultname = u"{0}.{1}".format(keyname, self.keyfile_ext)
filename = choose_save_file(self, unique_dlg_name, caption, filters, all_files=False, initial_filename=defaultname)
if filename:
dynamic[PLUGIN_NAME + 'save_dir'] = os.path.split(filename)[0]
with file(filename, 'w') as fname:
with file(filename, 'wb') as fname:
if self.binary_file:
fname.write(self.plugin_keys[keyname].decode('hex'))
elif self.json_file:
fname.write(json.dumps(self.plugin_keys[keyname]))
elif self.android_file:
for key in self.plugin_keys[keyname]:
fname.write(key)
fname.write("\n")
else:
fname.write(self.plugin_keys[keyname])
@@ -458,7 +484,7 @@ class RenameKeyDialog(QDialog):
self.resize(self.sizeHint())
def accept(self):
if self.key_ledit.text().isEmpty() or unicode(self.key_ledit.text()).isspace():
if not unicode(self.key_ledit.text()) or unicode(self.key_ledit.text()).isspace():
errmsg = u"Key name field cannot be empty!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
_(errmsg), show=True, show_copy_button=False)
@@ -479,7 +505,7 @@ class RenameKeyDialog(QDialog):
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
@@ -509,20 +535,17 @@ class AddBandNKeyDialog(QDialog):
u"<p>It should be something that will help you remember " +
u"what personal information was used to create it."))
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
name_group = QHBoxLayout()
data_group_box_layout.addLayout(name_group)
name_group.addWidget(QLabel(u"Your Name:", self))
name_group.addWidget(QLabel(u"B&N/nook account email address:", self))
self.name_ledit = QLineEdit(u"", self)
self.name_ledit.setToolTip(_(u"<p>Enter your name as it appears in your B&N " +
u"account or on your credit card.</p>" +
self.name_ledit.setToolTip(_(u"<p>Enter your email address as it appears in your B&N " +
u"account.</p>" +
u"<p>It will only be used to generate this " +
u"one-time key and won\'t be stored anywhere " +
u"key and won\'t be stored anywhere " +
u"in calibre or on your computer.</p>" +
u"<p>(ex: Jonathan Smith)"))
u"<p>eg: apprenticeharper@gmail.com</p>"))
name_group.addWidget(self.name_ledit)
name_disclaimer_label = QLabel(_(u"(Will not be saved in configuration data)"), self)
name_disclaimer_label.setAlignment(Qt.AlignHCenter)
@@ -530,13 +553,12 @@ class AddBandNKeyDialog(QDialog):
ccn_group = QHBoxLayout()
data_group_box_layout.addLayout(ccn_group)
ccn_group.addWidget(QLabel(u"Credit Card#:", self))
ccn_group.addWidget(QLabel(u"B&N/nook account password:", self))
self.cc_ledit = QLineEdit(u"", self)
self.cc_ledit.setToolTip(_(u"<p>Enter the full credit card number on record " +
u"in your B&N account.</p>" +
u"<p>No spaces or dashes... just the numbers. " +
u"This number will only be used to generate this " +
u"one-time key and won\'t be stored anywhere in " +
self.cc_ledit.setToolTip(_(u"<p>Enter the password " +
u"for your B&N account.</p>" +
u"<p>The password will only be used to generate this " +
u"key and won\'t be stored anywhere in " +
u"calibre or on your computer."))
ccn_group.addWidget(self.cc_ledit)
ccn_disclaimer_label = QLabel(_('(Will not be saved in configuration data)'), self)
@@ -553,29 +575,26 @@ class AddBandNKeyDialog(QDialog):
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
@property
def key_value(self):
from calibre_plugins.dedrm.ignoblekeygen import generate_key as generate_bandn_key
return generate_bandn_key(self.user_name,self.cc_number)
from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key
return fetch_bandn_key(self.user_name,self.cc_number)
@property
def user_name(self):
return unicode(self.name_ledit.text().toUtf8(), 'utf8').strip().lower().replace(' ','')
return unicode(self.name_ledit.text()).strip().lower().replace(' ','')
@property
def cc_number(self):
return unicode(self.cc_ledit.text().toUtf8(), 'utf8').strip().replace(' ', '').replace('-','')
return unicode(self.cc_ledit.text()).strip()
def accept(self):
if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace():
errmsg = u"All fields are required!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if not self.cc_number.isdigit():
errmsg = u"Numbers only in the credit card number field!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) < 4:
errmsg = u"Key name must be at <i>least</i> 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
@@ -600,9 +619,6 @@ class AddEReaderDialog(QDialog):
self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(u"<p>Enter an identifying name for this new key.\nIt should be something that will help you remember what personal information was used to create it.")
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
name_group = QHBoxLayout()
data_group_box_layout.addLayout(name_group)
@@ -634,7 +650,7 @@ class AddEReaderDialog(QDialog):
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
@property
def key_value(self):
@@ -643,11 +659,11 @@ class AddEReaderDialog(QDialog):
@property
def user_name(self):
return unicode(self.name_ledit.text().toUtf8(), 'utf8').strip().lower().replace(' ','')
return unicode(self.name_ledit.text()).strip().lower().replace(' ','')
@property
def cc_number(self):
return unicode(self.cc_ledit.text().toUtf8(), 'utf8').strip().replace(' ', '').replace('-','')
return unicode(self.cc_ledit.text()).strip().replace(' ', '').replace('-','')
def accept(self):
@@ -701,9 +717,7 @@ class AddAdeptDialog(QDialog):
self.key_ledit = QLineEdit(u"default_key", self)
self.key_ledit.setToolTip(u"<p>Enter an identifying name for the current default Adobe Digital Editions key.")
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
self.button_box.accepted.connect(self.accept)
else:
default_key_error = QLabel(u"The default encryption key for Adobe Digital Editions could not be found.", self)
@@ -719,7 +733,7 @@ class AddAdeptDialog(QDialog):
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
@property
def key_value(self):
@@ -774,15 +788,14 @@ class AddKindleDialog(QDialog):
self.key_ledit = QLineEdit(u"default_key", self)
self.key_ledit.setToolTip(u"<p>Enter an identifying name for the current default Kindle for Mac/PC key.")
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
self.button_box.accepted.connect(self.accept)
else:
default_key_error = QLabel(u"The default encryption key for Kindle for Mac/PC could not be found.", self)
default_key_error.setAlignment(Qt.AlignHCenter)
layout.addWidget(default_key_error)
# if no default, bot buttons do the same
# if no default, both buttons do the same
self.button_box.accepted.connect(self.reject)
self.button_box.rejected.connect(self.reject)
@@ -792,7 +805,7 @@ class AddKindleDialog(QDialog):
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
@property
def key_value(self):
@@ -828,9 +841,6 @@ class AddSerialDialog(QDialog):
self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(u"Enter an eInk Kindle serial number. EInk Kindle serial numbers are 16 characters long and usually start with a 'B' or a '9'. Kindle Serial Numbers are case-sensitive, so be sure to enter the upper and lower case letters unchanged.")
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
@@ -841,11 +851,11 @@ class AddSerialDialog(QDialog):
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
@property
def key_value(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
def accept(self):
if len(self.key_name) == 0 or self.key_name.isspace():
@@ -857,6 +867,89 @@ class AddSerialDialog(QDialog):
QDialog.accept(self)
class AddAndroidDialog(QDialog):
def __init__(self, parent=None,):
QDialog.__init__(self, parent)
self.parent = parent
self.setWindowTitle(u"{0} {1}: Add new Kindle for Android Key".format(PLUGIN_NAME, PLUGIN_VERSION))
layout = QVBoxLayout(self)
self.setLayout(layout)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
data_group_box = QGroupBox(u"", self)
layout.addWidget(data_group_box)
data_group_box_layout = QVBoxLayout()
data_group_box.setLayout(data_group_box_layout)
file_group = QHBoxLayout()
data_group_box_layout.addLayout(file_group)
add_btn = QPushButton(u"Choose Backup File", self)
add_btn.setToolTip(u"Import Kindle for Android backup file.")
add_btn.clicked.connect(self.get_android_file)
file_group.addWidget(add_btn)
self.selected_file_name = QLabel(u"",self)
self.selected_file_name.setAlignment(Qt.AlignHCenter)
file_group.addWidget(self.selected_file_name)
key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel(u"Unique Key Name:", self))
self.key_ledit = QLineEdit(u"", self)
self.key_ledit.setToolTip(u"<p>Enter an identifying name for the Android for Kindle key.")
key_group.addWidget(self.key_ledit)
#key_label = QLabel(_(''), self)
#key_label.setAlignment(Qt.AlignHCenter)
#data_group_box_layout.addWidget(key_label)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
layout.addWidget(self.button_box)
self.resize(self.sizeHint())
@property
def key_name(self):
return unicode(self.key_ledit.text()).strip()
@property
def file_name(self):
return unicode(self.selected_file_name.text()).strip()
@property
def key_value(self):
return self.serials_from_file
def get_android_file(self):
unique_dlg_name = PLUGIN_NAME + u"Import Kindle for Android backup file" #takes care of automatically remembering last directory
caption = u"Select Kindle for Android backup file to add"
filters = [(u"Kindle for Android backup files", ['db','ab','xml'])]
files = choose_files(self, unique_dlg_name, caption, filters, all_files=False)
self.serials_from_file = []
file_name = u""
if files:
# find the first selected file that yields some serial numbers
for filename in files:
fpath = os.path.join(config_dir, filename)
self.filename = os.path.basename(filename)
file_serials = androidkindlekey.get_serials(fpath)
if len(file_serials)>0:
file_name = os.path.basename(self.filename)
self.serials_from_file.extend(file_serials)
self.selected_file_name.setText(file_name)
def accept(self):
if len(self.file_name) == 0 or len(self.key_value) == 0:
errmsg = u"Please choose a Kindle for Android backup file."
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) == 0 or self.key_name.isspace():
errmsg = u"Please enter a key name."
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) < 4:
errmsg = u"Key name must be at <i>least</i> 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
QDialog.accept(self)
class AddPIDDialog(QDialog):
def __init__(self, parent=None,):
QDialog.__init__(self, parent)
@@ -876,9 +969,6 @@ class AddPIDDialog(QDialog):
self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(u"Enter a Mobipocket PID. Mobipocket PIDs are 8 or 10 characters long. Mobipocket PIDs are case-sensitive, so be sure to enter the upper and lower case letters unchanged.")
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
@@ -889,11 +979,11 @@ class AddPIDDialog(QDialog):
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
@property
def key_value(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
def accept(self):
if len(self.key_name) == 0 or self.key_name.isspace():

View File

@@ -1,4 +1,4 @@
{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370
{\rtf1\ansi\ansicpg1252\cocoartf1348\cocoasubrtf170
{\fonttbl}
{\colortbl;\red255\green255\blue255;}
}

View File

@@ -458,7 +458,11 @@ class DocParser(object):
(wtype, num) = pdesc[j]
if wtype == 'ocr' :
word = self.ocrtext[num]
try:
word = self.ocrtext[num]
except:
word = ""
sep = ' '
if handle_links:

View File

@@ -0,0 +1,336 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import with_statement
# ignoblekey.py
# Copyright © 2015 Apprentice Alf and Apprentice Harper
# Based on kindlekey.py, Copyright © 2010-2013 by some_updates and Apprentice Alf
# Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/>
# Revision history:
# 1.0 - Initial release
# 1.1 - remove duplicates and return last key as single key
"""
Get Barnes & Noble EPUB user key from nook Studio log file
"""
__license__ = 'GPL v3'
__version__ = "1.1"
import sys
import os
import hashlib
import getopt
import re
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
# encoded using "replace" before writing them.
class SafeUnbuffered:
def __init__(self, stream):
self.stream = stream
self.encoding = stream.encoding
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
try:
from calibre.constants import iswindows, isosx
except:
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
def unicode_argv():
if iswindows:
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
# as a list of Unicode strings and encode them as utf-8
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"ignoblekey.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
class DrmException(Exception):
pass
# Locate all of the nookStudy/nook for PC/Mac log file and return as list
def getNookLogFiles():
logFiles = []
found = False
if iswindows:
import _winreg as winreg
# some 64 bit machines do not have the proper registry key for some reason
# or the python interface to the 32 vs 64 bit registry is broken
paths = set()
if 'LOCALAPPDATA' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
if os.path.isdir(path):
paths.add(path)
if 'USERPROFILE' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
path = winreg.ExpandEnvironmentStrings(u"%USERPROFILE%")+u"\\AppData\\Local"
if os.path.isdir(path):
paths.add(path)
path = winreg.ExpandEnvironmentStrings(u"%USERPROFILE%")+u"\\AppData\\Roaming"
if os.path.isdir(path):
paths.add(path)
# User Shell Folders show take precedent over Shell Folders if present
try:
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\")
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
if os.path.isdir(path):
paths.add(path)
except WindowsError:
pass
try:
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\")
path = winreg.QueryValueEx(regkey, 'AppData')[0]
if os.path.isdir(path):
paths.add(path)
except WindowsError:
pass
try:
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
if os.path.isdir(path):
paths.add(path)
except WindowsError:
pass
try:
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
path = winreg.QueryValueEx(regkey, 'AppData')[0]
if os.path.isdir(path):
paths.add(path)
except WindowsError:
pass
for path in paths:
# look for nookStudy log file
logpath = path +'\\Barnes & Noble\\NOOKstudy\\logs\\BNClientLog.txt'
if os.path.isfile(logpath):
found = True
print('Found nookStudy log file: ' + logpath.encode('ascii','ignore'))
logFiles.append(logpath)
else:
home = os.getenv('HOME')
# check for BNClientLog.txt in various locations
testpath = home + '/Library/Application Support/Barnes & Noble/DesktopReader/logs/BNClientLog.txt'
if os.path.isfile(testpath):
logFiles.append(testpath)
print('Found nookStudy log file: ' + testpath)
found = True
testpath = home + '/Library/Application Support/Barnes & Noble/DesktopReader/indices/BNClientLog.txt'
if os.path.isfile(testpath):
logFiles.append(testpath)
print('Found nookStudy log file: ' + testpath)
found = True
testpath = home + '/Library/Application Support/Barnes & Noble/BNDesktopReader/logs/BNClientLog.txt'
if os.path.isfile(testpath):
logFiles.append(testpath)
print('Found nookStudy log file: ' + testpath)
found = True
testpath = home + '/Library/Application Support/Barnes & Noble/BNDesktopReader/indices/BNClientLog.txt'
if os.path.isfile(testpath):
logFiles.append(testpath)
print('Found nookStudy log file: ' + testpath)
found = True
if not found:
print('No nook Study log files have been found.')
return logFiles
# Extract CCHash key(s) from log file
def getKeysFromLog(kLogFile):
keys = []
regex = re.compile("ccHash: \"(.{28})\"");
for line in open(kLogFile):
for m in regex.findall(line):
keys.append(m)
return keys
# interface for calibre plugin
def nookkeys(files = []):
keys = []
if files == []:
files = getNookLogFiles()
for file in files:
fileKeys = getKeysFromLog(file)
if fileKeys:
print u"Found {0} keys in the Nook Study log files".format(len(fileKeys))
keys.extend(fileKeys)
return list(set(keys))
# interface for Python DeDRM
# returns single key or multiple keys, depending on path or file passed in
def getkey(outpath, files=[]):
keys = nookkeys(files)
if len(keys) > 0:
if not os.path.isdir(outpath):
outfile = outpath
with file(outfile, 'w') as keyfileout:
keyfileout.write(keys[-1])
print u"Saved a key to {0}".format(outfile)
else:
keycount = 0
for key in keys:
while True:
keycount += 1
outfile = os.path.join(outpath,u"nookkey{0:d}.b64".format(keycount))
if not os.path.exists(outfile):
break
with file(outfile, 'w') as keyfileout:
keyfileout.write(key)
print u"Saved a key to {0}".format(outfile)
return True
return False
def usage(progname):
print u"Finds the nook Study encryption keys."
print u"Keys are saved to the current directory, or a specified output directory."
print u"If a file name is passed instead of a directory, only the first key is saved, in that file."
print u"Usage:"
print u" {0:s} [-h] [-k <logFile>] [<outpath>]".format(progname)
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv()
progname = os.path.basename(argv[0])
print u"{0} v{1}\nCopyright © 2015 Apprentice Alf".format(progname,__version__)
try:
opts, args = getopt.getopt(argv[1:], "hk:")
except getopt.GetoptError, err:
print u"Error in options or arguments: {0}".format(err.args[0])
usage(progname)
sys.exit(2)
files = []
for o, a in opts:
if o == "-h":
usage(progname)
sys.exit(0)
if o == "-k":
files = [a]
if len(args) > 1:
usage(progname)
sys.exit(2)
if len(args) == 1:
# save to the specified file or directory
outpath = args[0]
if not os.path.isabs(outpath):
outpath = os.path.abspath(outpath)
else:
# save to the same directory as the script
outpath = os.path.dirname(argv[0])
# make sure the outpath is the
outpath = os.path.realpath(os.path.normpath(outpath))
if not getkey(outpath, files):
print u"Could not retrieve nook Study key."
return 0
def gui_main():
try:
import Tkinter
import Tkconstants
import tkMessageBox
import traceback
except:
return cli_main()
class ExceptionDialog(Tkinter.Frame):
def __init__(self, root, text):
Tkinter.Frame.__init__(self, root, border=5)
label = Tkinter.Label(self, text=u"Unexpected error:",
anchor=Tkconstants.W, justify=Tkconstants.LEFT)
label.pack(fill=Tkconstants.X, expand=0)
self.text = Tkinter.Text(self)
self.text.pack(fill=Tkconstants.BOTH, expand=1)
self.text.insert(Tkconstants.END, text)
argv=unicode_argv()
root = Tkinter.Tk()
root.withdraw()
progpath, progname = os.path.split(argv[0])
success = False
try:
keys = nookkeys()
keycount = 0
for key in keys:
print key
while True:
keycount += 1
outfile = os.path.join(progpath,u"nookkey{0:d}.b64".format(keycount))
if not os.path.exists(outfile):
break
with file(outfile, 'w') as keyfileout:
keyfileout.write(key)
success = True
tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile))
except DrmException, e:
tkMessageBox.showerror(progname, u"Error: {0}".format(str(e)))
except Exception:
root.wm_state('normal')
root.title(progname)
text = traceback.format_exc()
ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1)
root.mainloop()
if not success:
return 1
return 0
if __name__ == '__main__':
if len(sys.argv) > 1:
sys.exit(cli_main())
sys.exit(gui_main())

View File

@@ -0,0 +1,258 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import with_statement
# ignoblekeyfetch.pyw, version 1.1
# Copyright © 2015 Apprentice Harper
# Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/>
# Based on discoveries by "Nobody You Know"
# Code partly based on ignoblekeygen.py by several people.
# Windows users: Before running this program, you must first install Python.
# We recommend ActiveState Python 2.7.X for Windows from
# http://www.activestate.com/activepython/downloads.
# Then save this script file as ignoblekeyfetch.pyw and double-click on it to run it.
#
# Mac OS X users: Save this script file as ignoblekeyfetch.pyw. You can run this
# program from the command line (python ignoblekeyfetch.pyw) or by double-clicking
# it when it has been associated with PythonLauncher.
# Revision history:
# 1.0 - Initial version
# 1.1 - Try second URL if first one fails
"""
Fetch Barnes & Noble EPUB user key from B&N servers using email and password
"""
__license__ = 'GPL v3'
__version__ = "1.1"
import sys
import os
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
# encoded using "replace" before writing them.
class SafeUnbuffered:
def __init__(self, stream):
self.stream = stream
self.encoding = stream.encoding
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
try:
from calibre.constants import iswindows, isosx
except:
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
def unicode_argv():
if iswindows:
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
# as a list of Unicode strings and encode them as utf-8
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"ignoblekeyfetch.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
class IGNOBLEError(Exception):
pass
def fetch_key(email, password):
# change email and password to utf-8 if unicode
if type(email)==unicode:
email = email.encode('utf-8')
if type(password)==unicode:
password = password.encode('utf-8')
import random
random = "%030x" % random.randrange(16**30)
import urllib, urllib2, re
# try the URL from nook for PC
fetch_url = "https://cart4.barnesandnoble.com/services/service.aspx?Version=2&acctPassword="
fetch_url += urllib.quote(password,'')+"&devID=PC_BN_2.5.6.9575_"+random+"&emailAddress="
fetch_url += urllib.quote(email,"")+"&outFormat=5&schema=1&service=1&stage=deviceHashB"
#print fetch_url
found = ''
try:
req = urllib2.Request(fetch_url)
response = urllib2.urlopen(req)
the_page = response.read()
#print the_page
found = re.search('ccHash>(.+?)</ccHash', the_page).group(1)
except:
found = ''
if len(found)!=28:
# try the URL from android devices
fetch_url = "https://cart4.barnesandnoble.com/services/service.aspx?Version=2&acctPassword="
fetch_url += urllib.quote(password,'')+"&devID=hobbes_9.3.50818_"+random+"&emailAddress="
fetch_url += urllib.quote(email,"")+"&outFormat=5&schema=1&service=1&stage=deviceHashB"
#print fetch_url
found = ''
try:
req = urllib2.Request(fetch_url)
response = urllib2.urlopen(req)
the_page = response.read()
#print the_page
found = re.search('ccHash>(.+?)</ccHash', the_page).group(1)
except:
found = ''
return found
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv()
progname = os.path.basename(argv[0])
if len(argv) != 4:
print u"usage: {0} <email> <password> <keyfileout.b64>".format(progname)
return 1
email, password, keypath = argv[1:]
userkey = fetch_key(email, password)
if len(userkey) == 28:
open(keypath,'wb').write(userkey)
return 0
print u"Failed to fetch key."
return 1
def gui_main():
try:
import Tkinter
import tkFileDialog
import Tkconstants
import tkMessageBox
import traceback
except:
return cli_main()
class DecryptionDialog(Tkinter.Frame):
def __init__(self, root):
Tkinter.Frame.__init__(self, root, border=5)
self.status = Tkinter.Label(self, text=u"Enter parameters")
self.status.pack(fill=Tkconstants.X, expand=1)
body = Tkinter.Frame(self)
body.pack(fill=Tkconstants.X, expand=1)
sticky = Tkconstants.E + Tkconstants.W
body.grid_columnconfigure(1, weight=2)
Tkinter.Label(body, text=u"Account email address").grid(row=0)
self.name = Tkinter.Entry(body, width=40)
self.name.grid(row=0, column=1, sticky=sticky)
Tkinter.Label(body, text=u"Account password").grid(row=1)
self.ccn = Tkinter.Entry(body, width=40)
self.ccn.grid(row=1, column=1, sticky=sticky)
Tkinter.Label(body, text=u"Output file").grid(row=2)
self.keypath = Tkinter.Entry(body, width=40)
self.keypath.grid(row=2, column=1, sticky=sticky)
self.keypath.insert(2, u"bnepubkey.b64")
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
button.grid(row=2, column=2)
buttons = Tkinter.Frame(self)
buttons.pack()
botton = Tkinter.Button(
buttons, text=u"Fetch", width=10, command=self.generate)
botton.pack(side=Tkconstants.LEFT)
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
button = Tkinter.Button(
buttons, text=u"Quit", width=10, command=self.quit)
button.pack(side=Tkconstants.RIGHT)
def get_keypath(self):
keypath = tkFileDialog.asksaveasfilename(
parent=None, title=u"Select B&N ePub key file to produce",
defaultextension=u".b64",
filetypes=[('base64-encoded files', '.b64'),
('All Files', '.*')])
if keypath:
keypath = os.path.normpath(keypath)
self.keypath.delete(0, Tkconstants.END)
self.keypath.insert(0, keypath)
return
def generate(self):
email = self.name.get()
password = self.ccn.get()
keypath = self.keypath.get()
if not email:
self.status['text'] = u"Email address not given"
return
if not password:
self.status['text'] = u"Account password not given"
return
if not keypath:
self.status['text'] = u"Output keyfile path not set"
return
self.status['text'] = u"Fetching..."
try:
userkey = fetch_key(email, password)
except Exception, e:
self.status['text'] = u"Error: {0}".format(e.args[0])
return
if len(userkey) == 28:
open(keypath,'wb').write(userkey)
self.status['text'] = u"Keyfile fetched successfully"
else:
self.status['text'] = u"Keyfile fetch failed."
root = Tkinter.Tk()
root.title(u"Barnes & Noble ePub Keyfile Fetch v.{0}".format(__version__))
root.resizable(True, False)
root.minsize(300, 0)
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
root.mainloop()
return 0
if __name__ == '__main__':
if len(sys.argv) > 1:
sys.exit(cli_main())
sys.exit(gui_main())

View File

@@ -33,13 +33,14 @@ from __future__ import with_statement
# 2.5 - Additional improvement for unicode and plugin support
# 2.6 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 2.7 - Work if TkInter is missing
# 2.8 - Fix bug in stand-alone use (import tkFileDialog)
"""
Generate Barnes & Noble EPUB user key from name and credit card number.
"""
__license__ = 'GPL v3'
__version__ = "2.7"
__version__ = "2.8"
import sys
import os
@@ -240,6 +241,7 @@ def gui_main():
import Tkinter
import Tkconstants
import tkMessageBox
import tkFileDialog
import traceback
except:
return cli_main()

View File

@@ -37,13 +37,14 @@ from __future__ import with_statement
# 5.9 - Fixed to retain zip file metadata (e.g. file modification date)
# 6.0 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 6.1 - Work if TkInter is missing
# 6.2 - Handle UTF-8 file names inside an ePub, fix by Jose Luis
"""
Decrypt Adobe Digital Editions encrypted ePub books.
"""
__license__ = 'GPL v3'
__version__ = "6.1"
__version__ = "6.2"
import sys
import os
@@ -366,7 +367,7 @@ class Decryptor(object):
return bytes
def decrypt(self, path, data):
if path in self._encrypted:
if path.encode('utf-8') in self._encrypted:
data = self._aes.decrypt(data)[16:]
data = data[:-ord(data[-1])]
data = self.decompress(data)

View File

@@ -3,15 +3,15 @@
from __future__ import with_statement
# ignobleepub.pyw, version 3.6
# Copyright © 2009-2012 by DiapDealer et al.
# k4mobidedrm.py, version 5.3
# Copyright © 2009-2015 by ApprenticeHarper et al.
# engine to remove drm from Kindle for Mac and Kindle for PC books
# engine to remove drm from Kindle and Mobipocket ebooks
# for personal use for archiving and converting your ebooks
# PLEASE DO NOT PIRATE EBOOKS!
# We want all authors and publishers, and eBook stores to live
# 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
@@ -55,8 +55,9 @@ from __future__ import with_statement
# - tweaked GetDecryptedBook interface to leave passed parameters unchanged
# 5.1 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 5.2 - Fixed error in command line processing of unicode arguments
# 5.3 - Changed Android support to allow passing of backup .ab files
__version__ = '5.2'
__version__ = '5.3'
import sys, os, re
@@ -80,10 +81,12 @@ if inCalibre:
from calibre_plugins.dedrm import mobidedrm
from calibre_plugins.dedrm import topazextract
from calibre_plugins.dedrm import kgenpids
from calibre_plugins.dedrm import androidkindlekey
else:
import mobidedrm
import topazextract
import kgenpids
import androidkindlekey
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
@@ -185,7 +188,7 @@ def unescape(text):
return text # leave as is
return re.sub(u"&#?\w+;", fixup, text)
def GetDecryptedBook(infile, kDatabases, serials, pids, starttime = time.time()):
def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime = time.time()):
# handle the obvious cases at the beginning
if not os.path.isfile(infile):
raise DrmException(u"Input file does not exist.")
@@ -205,9 +208,14 @@ def GetDecryptedBook(infile, kDatabases, serials, pids, starttime = time.time())
# copy list of pids
totalpids = list(pids)
# extend PID list with book-specific PIDs
# extend list of serials with serials from android databases
for aFile in androidFiles:
serials.extend(androidkindlekey.get_serials(aFile))
# extend PID list with book-specific PIDs from seriala and kDatabases
md1, md2 = mb.getPIDMetaInfo()
totalpids.extend(kgenpids.getPidList(md1, md2, serials, kDatabases))
# remove any duplicates
totalpid = list(set(totalpids))
print u"Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(totalpids))
try:
@@ -221,7 +229,7 @@ def GetDecryptedBook(infile, kDatabases, serials, pids, starttime = time.time())
# kDatabaseFiles is a list of files created by kindlekey
def decryptBook(infile, outdir, kDatabaseFiles, serials, pids):
def decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids):
starttime = time.time()
kDatabases = []
for dbfile in kDatabaseFiles:
@@ -237,7 +245,7 @@ def decryptBook(infile, outdir, kDatabaseFiles, serials, pids):
try:
book = GetDecryptedBook(infile, kDatabases, serials, pids, starttime)
book = GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime)
except Exception, e:
print u"Error decrypting book after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime)
traceback.print_exc()
@@ -252,7 +260,7 @@ def decryptBook(infile, outdir, kDatabaseFiles, serials, pids):
# avoid excessively long file names
if len(outfilename)>150:
outfilename = outfilename[:150]
outfilename = outfilename[:99]+"--"+outfilename[-49:]
outfilename = outfilename+u"_nodrm"
outfile = os.path.join(outdir, outfilename + book.getBookExtension())
@@ -273,7 +281,7 @@ def decryptBook(infile, outdir, kDatabaseFiles, serials, pids):
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.k4i>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] <infile> <outdir>".format(progname)
print u" {0} [-k <kindle.k4i>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] [ -a <AmazonSecureStorage.xml|backup.ab> ] <infile> <outdir>".format(progname)
#
# Main
@@ -284,7 +292,7 @@ def cli_main():
print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2013 The Dark Reverser et al.".format(__version__)
try:
opts, args = getopt.getopt(argv[1:], "k:p:s:")
opts, args = getopt.getopt(argv[1:], "k:p:s:a:")
except getopt.GetoptError, err:
print u"Error in options or arguments: {0}".format(err.args[0])
usage(progname)
@@ -296,6 +304,7 @@ def cli_main():
infile = args[0]
outdir = args[1]
kDatabaseFiles = []
androidFiles = []
serials = []
pids = []
@@ -312,11 +321,15 @@ def cli_main():
if a == None :
raise DrmException("Invalid parameter for -s")
serials = a.split(',')
if o == '-a':
if a == None:
raise DrmException("Invalid parameter for -a")
androidFiles.append(a)
# try with built in Kindle Info files if not on Linux
k4 = not sys.platform.startswith('linux')
return decryptBook(infile, outdir, kDatabaseFiles, serials, pids)
return decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids)
if __name__ == '__main__':

View File

@@ -2,6 +2,13 @@
# -*- coding: utf-8 -*-
from __future__ import with_statement
# kgenpids.py
# Copyright © 2010-2015 by some_updates, Apprentice Alf and Apprentice Harper
# Revision history:
# 2.0 - Fix for non-ascii Windows user names
import sys
import os, csv
import binascii
@@ -164,7 +171,7 @@ def getKindlePids(rec209, token, serialnum):
pids=[]
if isinstance(serialnum,unicode):
serialnum = serialnum.encode('ascii')
serialnum = serialnum.encode('utf-8')
# Compute book PID
pidHash = SHA1(serialnum+rec209+token)
@@ -190,16 +197,16 @@ def getK4Pids(rec209, token, kindleDatabase):
try:
# Get the Mazama Random number
MazamaRandomNumber = (kindleDatabase[1])['MazamaRandomNumber'].decode('hex').encode('ascii')
MazamaRandomNumber = (kindleDatabase[1])['MazamaRandomNumber'].decode('hex')
# Get the kindle account token
kindleAccountToken = (kindleDatabase[1])['kindle.account.tokens'].decode('hex').encode('ascii')
kindleAccountToken = (kindleDatabase[1])['kindle.account.tokens'].decode('hex')
# Get the IDString used to decode the Kindle Info file
IDString = (kindleDatabase[1])['IDString'].decode('hex').encode('ascii')
IDString = (kindleDatabase[1])['IDString'].decode('hex')
# Get the UserName stored when the Kindle Info file was decoded
UserName = (kindleDatabase[1])['UserName'].decode('hex').encode('ascii')
UserName = (kindleDatabase[1])['UserName'].decode('hex')
except KeyError:
print u"Keys not found in the database {0}.".format(kindleDatabase[0])

View File

@@ -4,9 +4,7 @@
from __future__ import with_statement
# kindlekey.py
# Copyright © 2010-2013 by some_updates and Apprentice Alf
#
# Currently requires alfcrypto.py which requires the alfcrypto library
# Copyright © 2010-2015 by some_updates, Apprentice Alf and Apprentice Harper
# Revision history:
# 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc.
@@ -19,6 +17,8 @@ from __future__ import with_statement
# 1.6 - Fixed a problem getting the disk serial numbers
# 1.7 - Work if TkInter is missing
# 1.8 - Fixes for Kindle for Mac, and non-ascii in Windows user names
# 1.9 - Fixes for Unicode in Windows user names
# 2.0 - Added comments and extra fix for non-ascii Windows user names
"""
@@ -26,7 +26,7 @@ Retrieve Kindle for PC/Mac user key.
"""
__license__ = 'GPL v3'
__version__ = '1.8'
__version__ = '1.9'
import sys, os, re
from struct import pack, unpack, unpack_from
@@ -884,6 +884,7 @@ if iswindows:
return "AlternateUserName"
buffer = create_unicode_buffer(len(buffer) * 2)
size.value = len(buffer)
# return low byte of the unicode value of each character of the username
return buffer.value.encode('utf-16-le')[::2]
return GetUserName
GetUserName = GetUserName()
@@ -907,18 +908,34 @@ if iswindows:
return CryptUnprotectData
CryptUnprotectData = CryptUnprotectData()
# Returns Environmental Variables that contain unicode
def getEnvironmentVariable(name):
import ctypes
name = unicode(name) # make sure string argument is unicode
n = ctypes.windll.kernel32.GetEnvironmentVariableW(name, None, 0)
if n == 0:
return None
buf = ctypes.create_unicode_buffer(u'\0'*n)
ctypes.windll.kernel32.GetEnvironmentVariableW(name, buf, n)
return buf.value
# Locate all of the kindle-info style files and return as list
def getKindleInfoFiles():
kInfoFiles = []
# some 64 bit machines do not have the proper registry key for some reason
# or the pythonn interface to the 32 vs 64 bit registry is broken
# or the python interface to the 32 vs 64 bit registry is broken
path = ""
if 'LOCALAPPDATA' in os.environ.keys():
path = os.environ['LOCALAPPDATA']
# Python 2.x does not return unicode env. Use Python 3.x
path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
# this is just another alternative.
# path = getEnvironmentVariable('LOCALAPPDATA')
if not os.path.isdir(path):
path = ""
else:
# User Shell Folders show take precedent over Shell Folders if present
try:
# this will still break
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\")
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
if not os.path.isdir(path):
@@ -937,13 +954,14 @@ if iswindows:
if path == "":
print ('Could not find the folder in which to look for kinfoFiles.')
else:
print('searching for kinfoFiles in ' + path)
# Probably not the best. To Fix (shouldn't ignore in encoding) or use utf-8
print(u'searching for kinfoFiles in ' + path.encode('ascii', 'ignore'))
# look for (K4PC 1.9.0 and later) .kinf2011 file
kinfopath = path +'\\Amazon\\Kindle\\storage\\.kinf2011'
if os.path.isfile(kinfopath):
found = True
print('Found K4PC 1.9+ kinf2011 file: ' + kinfopath)
print('Found K4PC 1.9+ kinf2011 file: ' + kinfopath.encode('ascii','ignore'))
kInfoFiles.append(kinfopath)
# look for (K4PC 1.6.0 and later) rainier.2.1.1.kinf file
@@ -1142,11 +1160,11 @@ if iswindows:
cleartext = CryptUnprotectData(encryptedValue, entropy, 1)
DB[keyname] = cleartext
if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB:
print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(GetIDString(), GetUserName().decode("latin-1"))
if 'kindle.account.tokens' in DB:
# store values used in decryption
DB['IDString'] = GetIDString()
DB['UserName'] = GetUserName()
print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(GetIDString(), GetUserName().encode('hex'))
else:
DB = {}
return DB
@@ -1758,7 +1776,7 @@ elif isosx:
break
except:
pass
if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB:
if 'kindle.account.tokens' in DB:
# store values used in decryption
print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString, GetUserName())
DB['IDString'] = IDString

View File

@@ -156,6 +156,8 @@ def PC1(key, src, decryption=True):
return Pukall_Cipher().PC1(key,src,decryption)
except NameError:
pass
except TypeError:
pass
# use slow python version, since Pukall_Cipher didn't load
sum1 = 0;

View File

@@ -23,6 +23,7 @@ class DeDRM_Prefs():
self.dedrmprefs.defaults['adeptkeys'] = {}
self.dedrmprefs.defaults['ereaderkeys'] = {}
self.dedrmprefs.defaults['kindlekeys'] = {}
self.dedrmprefs.defaults['androidkeys'] = {}
self.dedrmprefs.defaults['pids'] = []
self.dedrmprefs.defaults['serials'] = []
self.dedrmprefs.defaults['adobewineprefix'] = ""
@@ -40,6 +41,8 @@ class DeDRM_Prefs():
self.dedrmprefs['ereaderkeys'] = {}
if self.dedrmprefs['kindlekeys'] == {}:
self.dedrmprefs['kindlekeys'] = {}
if self.dedrmprefs['androidkeys'] == {}:
self.dedrmprefs['androidkeys'] = {}
if self.dedrmprefs['pids'] == []:
self.dedrmprefs['pids'] = []
if self.dedrmprefs['serials'] == []:

View File

@@ -166,8 +166,30 @@ def decryptk4mobi(infile, outdir, rscpath):
for filename in files:
dpath = os.path.join(rscpath,filename)
kDatabaseFiles.append(dpath)
androidFiles = []
files = os.listdir(rscpath)
filefilter = re.compile("\.ab$", re.IGNORECASE)
files = filter(filefilter.search, files)
if files:
for filename in files:
dpath = os.path.join(rscpath,filename)
androidFiles.append(dpath)
files = os.listdir(rscpath)
filefilter = re.compile("\.db$", re.IGNORECASE)
files = filter(filefilter.search, files)
if files:
for filename in files:
dpath = os.path.join(rscpath,filename)
androidFiles.append(dpath)
files = os.listdir(rscpath)
filefilter = re.compile("\.xml$", re.IGNORECASE)
files = filter(filefilter.search, files)
if files:
for filename in files:
dpath = os.path.join(rscpath,filename)
androidFiles.append(dpath)
try:
rv = k4mobidedrm.decryptBook(infile, outdir, kDatabaseFiles, serialnums, pidnums)
rv = k4mobidedrm.decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serialnums, pidnums)
except Exception, e:
errlog += traceback.format_exc()
errlog += str(e)

View File

@@ -0,0 +1,27 @@
#!/usr/bin/env python
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
import Tkinter
import Tkconstants
# basic scrolled text widget
class ScrolledText(Tkinter.Text):
def __init__(self, master=None, **kw):
self.frame = Tkinter.Frame(master)
self.vbar = Tkinter.Scrollbar(self.frame)
self.vbar.pack(side=Tkconstants.RIGHT, fill=Tkconstants.Y)
kw.update({'yscrollcommand': self.vbar.set})
Tkinter.Text.__init__(self, self.frame, **kw)
self.pack(side=Tkconstants.LEFT, fill=Tkconstants.BOTH, expand=True)
self.vbar['command'] = self.yview
# Copy geometry methods of self.frame without overriding Text
# methods = hack!
text_meths = vars(Tkinter.Text).keys()
methods = vars(Tkinter.Pack).keys() + vars(Tkinter.Grid).keys() + vars(Tkinter.Place).keys()
methods = set(methods).difference(text_meths)
for m in methods:
if m[0] != '_' and m != 'config' and m != 'configure':
setattr(self, m, getattr(self.frame, m))
def __str__(self):
return str(self.frame)

View File

@@ -0,0 +1,77 @@
#!/usr/bin/env python
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
import sys
import os, os.path
import shutil
class SimplePrefsError(Exception):
pass
class SimplePrefs(object):
def __init__(self, target, description):
self.prefs = {}
self.key2file={}
self.file2key={}
for keyfilemap in description:
[key, filename] = keyfilemap
self.key2file[key] = filename
self.file2key[filename] = key
self.target = target + 'Prefs'
if sys.platform.startswith('win'):
import _winreg as winreg
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
prefdir = path + os.sep + self.target
elif sys.platform.startswith('darwin'):
home = os.getenv('HOME')
prefdir = os.path.join(home,'Library','Preferences','org.' + self.target)
else:
# linux and various flavors of unix
home = os.getenv('HOME')
prefdir = os.path.join(home,'.' + self.target)
if not os.path.exists(prefdir):
os.makedirs(prefdir)
self.prefdir = prefdir
self.prefs['dir'] = self.prefdir
self._loadPreferences()
def _loadPreferences(self):
filenames = os.listdir(self.prefdir)
for filename in filenames:
if filename in self.file2key:
key = self.file2key[filename]
filepath = os.path.join(self.prefdir,filename)
if os.path.isfile(filepath):
try :
data = file(filepath,'rb').read()
self.prefs[key] = data
except Exception, e:
pass
def getPreferences(self):
return self.prefs
def setPreferences(self, newprefs={}):
if 'dir' not in newprefs:
raise SimplePrefsError('Error: Attempt to Set Preferences in unspecified directory')
if newprefs['dir'] != self.prefs['dir']:
raise SimplePrefsError('Error: Attempt to Set Preferences in unspecified directory')
for key in newprefs:
if key != 'dir':
if key in self.key2file:
filename = self.key2file[key]
filepath = os.path.join(self.prefdir,filename)
data = newprefs[key]
if data != None:
data = str(data)
if data == None or data == '':
if os.path.exists(filepath):
os.remove(filepath)
else:
try:
file(filepath,'wb').write(data)
except Exception, e:
pass
self.prefs = newprefs
return

View File

@@ -178,7 +178,12 @@ class DocParser(object):
if val == "":
val = 0
if not ((attr == 'hang') and (int(val) == 0)) :
if not ((attr == 'hang') and (int(val) == 0)):
try:
f = float(val)
except:
print "Warning: unrecognised val, ignoring"
val = 0
pv = float(val)/scale
cssargs[attr] = (self.attr_val_map[attr], pv)
keep = True

View File

@@ -0,0 +1,148 @@
#!/usr/bin/env python
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
import os, sys
import signal
import threading
import subprocess
from subprocess import Popen, PIPE, STDOUT
# **heavily** chopped up and modfied version of asyncproc.py
# to make it actually work on Windows as well as Mac/Linux
# For the original see:
# "http://www.lysator.liu.se/~bellman/download/"
# author is "Thomas Bellman <bellman@lysator.liu.se>"
# available under GPL version 3 or Later
# create an asynchronous subprocess whose output can be collected in
# a non-blocking manner
# What a mess! Have to use threads just to get non-blocking io
# in a cross-platform manner
# luckily all thread use is hidden within this class
class Process(object):
def __init__(self, *params, **kwparams):
if len(params) <= 3:
kwparams.setdefault('stdin', subprocess.PIPE)
if len(params) <= 4:
kwparams.setdefault('stdout', subprocess.PIPE)
if len(params) <= 5:
kwparams.setdefault('stderr', subprocess.PIPE)
self.__pending_input = []
self.__collected_outdata = []
self.__collected_errdata = []
self.__exitstatus = None
self.__lock = threading.Lock()
self.__inputsem = threading.Semaphore(0)
self.__quit = False
self.__process = subprocess.Popen(*params, **kwparams)
if self.__process.stdin:
self.__stdin_thread = threading.Thread(
name="stdin-thread",
target=self.__feeder, args=(self.__pending_input,
self.__process.stdin))
self.__stdin_thread.setDaemon(True)
self.__stdin_thread.start()
if self.__process.stdout:
self.__stdout_thread = threading.Thread(
name="stdout-thread",
target=self.__reader, args=(self.__collected_outdata,
self.__process.stdout))
self.__stdout_thread.setDaemon(True)
self.__stdout_thread.start()
if self.__process.stderr:
self.__stderr_thread = threading.Thread(
name="stderr-thread",
target=self.__reader, args=(self.__collected_errdata,
self.__process.stderr))
self.__stderr_thread.setDaemon(True)
self.__stderr_thread.start()
def pid(self):
return self.__process.pid
def kill(self, signal):
self.__process.send_signal(signal)
# check on subprocess (pass in 'nowait') to act like poll
def wait(self, flag):
if flag.lower() == 'nowait':
rc = self.__process.poll()
else:
rc = self.__process.wait()
if rc != None:
if self.__process.stdin:
self.closeinput()
if self.__process.stdout:
self.__stdout_thread.join()
if self.__process.stderr:
self.__stderr_thread.join()
return self.__process.returncode
def terminate(self):
if self.__process.stdin:
self.closeinput()
self.__process.terminate()
# thread gets data from subprocess stdout
def __reader(self, collector, source):
while True:
data = os.read(source.fileno(), 65536)
self.__lock.acquire()
collector.append(data)
self.__lock.release()
if data == "":
source.close()
break
return
# thread feeds data to subprocess stdin
def __feeder(self, pending, drain):
while True:
self.__inputsem.acquire()
self.__lock.acquire()
if not pending and self.__quit:
drain.close()
self.__lock.release()
break
data = pending.pop(0)
self.__lock.release()
drain.write(data)
# non-blocking read of data from subprocess stdout
def read(self):
self.__lock.acquire()
outdata = "".join(self.__collected_outdata)
del self.__collected_outdata[:]
self.__lock.release()
return outdata
# non-blocking read of data from subprocess stderr
def readerr(self):
self.__lock.acquire()
errdata = "".join(self.__collected_errdata)
del self.__collected_errdata[:]
self.__lock.release()
return errdata
# non-blocking write to stdin of subprocess
def write(self, data):
if self.__process.stdin is None:
raise ValueError("Writing to process with stdin not a pipe")
self.__lock.acquire()
self.__pending_input.append(data)
self.__inputsem.release()
self.__lock.release()
# close stdinput of subprocess
def closeinput(self):
self.__lock.acquire()
self.__quit = True
self.__inputsem.release()
self.__lock.release()

View File

@@ -356,7 +356,7 @@ class TopazBook:
self.setBookKey(bookKey)
self.createBookDirectory()
self.extractFiles()
self.extractFiles()
print u"Successfully Extracted Topaz contents"
if inCalibre:
from calibre_plugins.dedrm import genbook

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# DeDRM.pyw, version 6.0.1
# Copyright 2010-2013 some_updates and Apprentice Alf
# DeDRM.pyw
# Copyright 2010-2015 some_updates, Apprentice Alf and Apprentice Harper
# Revision history:
# 6.0.0 - Release along with unified plugin
@@ -11,8 +11,16 @@
# 6.0.3 - Fix for Windows non-ascii user names
# 6.0.4 - Fix for other potential unicode problems
# 6.0.5 - Fix typo
# 6.2.0 - Update to match plugin and AppleScript
# 6.2.1 - Fix for non-ascii user names
# 6.2.2 - Added URL method for B&N/nook books
# 6.3.0 - Add in Android support
# 6.3.1 - Version bump for clarity
# 6.3.2 - Version bump to match plugin
# 6.3.3 - Version bump to match plugin
# 6.3.4 - Version bump to match plugin
__version__ = '6.0.7'
__version__ = '6.3.4'
import sys
import os, os.path
@@ -104,6 +112,14 @@ class MainApp(Tk):
except:
traceback.print_exc()
pass
bnepubkeyfile = os.path.join(prefdir,'bnepubkey.b64')
if not os.path.exists(bnepubkeyfile):
import ignoblekey
try:
ignoblekey.getkey(bnepubkeyfile)
except:
traceback.print_exc()
pass
return prefs
def setPreferences(self, newprefs):
@@ -120,8 +136,14 @@ class MainApp(Tk):
nfile = os.path.join(prefdir,fname)
if os.path.isfile(dfile):
shutil.copyfile(dfile,nfile)
if 'kinfofile' in newprefs:
dfile = newprefs['kinfofile']
if 'kindlefile' in newprefs:
dfile = newprefs['kindlefile']
fname = os.path.basename(dfile)
nfile = os.path.join(prefdir,fname)
if os.path.isfile(dfile):
shutil.copyfile(dfile,nfile)
if 'androidfile' in newprefs:
dfile = newprefs['androidfile']
fname = os.path.basename(dfile)
nfile = os.path.join(prefdir,fname)
if os.path.isfile(dfile):
@@ -150,82 +172,106 @@ class PrefsDialog(Toplevel):
sticky = Tkconstants.E + Tkconstants.W
body.grid_columnconfigure(1, weight=2)
Tkinter.Label(body, text='Adobe Key file (adeptkey.der)').grid(row=0, sticky=Tkconstants.E)
cur_row = 0
Tkinter.Label(body, text='Adobe Key file (adeptkey.der)').grid(row=cur_row, sticky=Tkconstants.E)
self.adkpath = Tkinter.Entry(body, width=50)
self.adkpath.grid(row=0, column=1, sticky=sticky)
self.adkpath.grid(row=cur_row, column=1, sticky=sticky)
prefdir = self.prefs_array['dir']
keyfile = os.path.join(prefdir,'adeptkey.der')
if os.path.isfile(keyfile):
path = keyfile
self.adkpath.insert(0, path)
self.adkpath.insert(cur_row, path)
button = Tkinter.Button(body, text="...", command=self.get_adkpath)
button.grid(row=0, column=2)
button.grid(row=cur_row, column=2)
Tkinter.Label(body, text='Kindle Key file (kindlekey.k4i)').grid(row=1, sticky=Tkconstants.E)
cur_row = cur_row + 1
Tkinter.Label(body, text='Kindle Key file (kindlekey.k4i)').grid(row=cur_row, sticky=Tkconstants.E)
self.kkpath = Tkinter.Entry(body, width=50)
self.kkpath.grid(row=1, column=1, sticky=sticky)
self.kkpath.grid(row=cur_row, column=1, sticky=sticky)
prefdir = self.prefs_array['dir']
keyfile = os.path.join(prefdir,'kindlekey.k4i')
if os.path.isfile(keyfile):
path = keyfile
self.kkpath.insert(1, path)
self.kkpath.insert(0, path)
button = Tkinter.Button(body, text="...", command=self.get_kkpath)
button.grid(row=1, column=2)
button.grid(row=cur_row, column=2)
Tkinter.Label(body, text='Barnes and Noble Key file (bnepubkey.b64)').grid(row=2, sticky=Tkconstants.E)
cur_row = cur_row + 1
Tkinter.Label(body, text='Android Kindle backup file (backup.ab)').grid(row=cur_row, sticky=Tkconstants.E)
self.akkpath = Tkinter.Entry(body, width=50)
self.akkpath.grid(row=cur_row, column=1, sticky=sticky)
prefdir = self.prefs_array['dir']
keyfile = os.path.join(prefdir,'backup.ab')
if os.path.isfile(keyfile):
path = keyfile
self.akkpath.insert(0, path)
button = Tkinter.Button(body, text="...", command=self.get_akkpath)
button.grid(row=cur_row, column=2)
cur_row = cur_row + 1
Tkinter.Label(body, text='Barnes and Noble Key file (bnepubkey.b64)').grid(row=cur_row, sticky=Tkconstants.E)
self.bnkpath = Tkinter.Entry(body, width=50)
self.bnkpath.grid(row=2, column=1, sticky=sticky)
self.bnkpath.grid(row=cur_row, column=1, sticky=sticky)
prefdir = self.prefs_array['dir']
keyfile = os.path.join(prefdir,'bnepubkey.b64')
if os.path.isfile(keyfile):
path = keyfile
self.bnkpath.insert(2, path)
self.bnkpath.insert(0, path)
button = Tkinter.Button(body, text="...", command=self.get_bnkpath)
button.grid(row=2, column=2)
button.grid(row=cur_row, column=2)
Tkinter.Label(body, text='Mobipocket PID list\n(8 or 10 characters, comma separated)').grid(row=3, sticky=Tkconstants.E)
cur_row = cur_row + 1
Tkinter.Label(body, text='Mobipocket PID list\n(8 or 10 characters, comma separated)').grid(row=cur_row, sticky=Tkconstants.E)
self.pidnums = Tkinter.StringVar()
self.pidinfo = Tkinter.Entry(body, width=50, textvariable=self.pidnums)
if 'pids' in self.prefs_array:
self.pidnums.set(self.prefs_array['pids'])
self.pidinfo.grid(row=3, column=1, sticky=sticky)
self.pidinfo.grid(row=cur_row, column=1, sticky=sticky)
Tkinter.Label(body, text='eInk Kindle Serial Number list\n(16 characters, comma separated)').grid(row=4, sticky=Tkconstants.E)
cur_row = cur_row + 1
Tkinter.Label(body, text='eInk Kindle Serial Number list\n(16 characters, comma separated)').grid(row=cur_row, sticky=Tkconstants.E)
self.sernums = Tkinter.StringVar()
self.serinfo = Tkinter.Entry(body, width=50, textvariable=self.sernums)
if 'serials' in self.prefs_array:
self.sernums.set(self.prefs_array['serials'])
self.serinfo.grid(row=4, column=1, sticky=sticky)
self.serinfo.grid(row=cur_row, column=1, sticky=sticky)
Tkinter.Label(body, text='eReader data list\n(name:last 8 digits on credit card, comma separated)').grid(row=5, sticky=Tkconstants.E)
cur_row = cur_row + 1
Tkinter.Label(body, text='eReader data list\n(name:last 8 digits on credit card, comma separated)').grid(row=cur_row, sticky=Tkconstants.E)
self.sdrmnums = Tkinter.StringVar()
self.sdrminfo = Tkinter.Entry(body, width=50, textvariable=self.sdrmnums)
if 'sdrms' in self.prefs_array:
self.sdrmnums.set(self.prefs_array['sdrms'])
self.sdrminfo.grid(row=5, column=1, sticky=sticky)
self.sdrminfo.grid(row=cur_row, column=1, sticky=sticky)
Tkinter.Label(body, text="Output Folder (if blank, use input ebook's folder)").grid(row=6, sticky=Tkconstants.E)
cur_row = cur_row + 1
Tkinter.Label(body, text="Output Folder (if blank, use input ebook's folder)").grid(row=cur_row, sticky=Tkconstants.E)
self.outpath = Tkinter.Entry(body, width=50)
self.outpath.grid(row=6, column=1, sticky=sticky)
self.outpath.grid(row=cur_row, column=1, sticky=sticky)
if 'outdir' in self.prefs_array:
dpath = self.prefs_array['outdir']
self.outpath.insert(0, dpath)
button = Tkinter.Button(body, text="...", command=self.get_outpath)
button.grid(row=6, column=2)
button.grid(row=cur_row, column=2)
Tkinter.Label(body, text='').grid(row=7, column=0, columnspan=2, sticky=Tkconstants.N)
cur_row = cur_row + 1
Tkinter.Label(body, text='').grid(row=cur_row, column=0, columnspan=2, sticky=Tkconstants.N)
Tkinter.Label(body, text='Alternatively Process an eBook').grid(row=8, column=0, columnspan=2, sticky=Tkconstants.N)
cur_row = cur_row + 1
Tkinter.Label(body, text='Alternatively Process an eBook').grid(row=cur_row, column=0, columnspan=2, sticky=Tkconstants.N)
Tkinter.Label(body, text='Select an eBook to Process*').grid(row=9, sticky=Tkconstants.E)
cur_row = cur_row + 1
Tkinter.Label(body, text='Select an eBook to Process*').grid(row=cur_row, sticky=Tkconstants.E)
self.bookpath = Tkinter.Entry(body, width=50)
self.bookpath.grid(row=9, column=1, sticky=sticky)
self.bookpath.grid(row=cur_row, column=1, sticky=sticky)
button = Tkinter.Button(body, text="...", command=self.get_bookpath)
button.grid(row=9, column=2)
button.grid(row=cur_row, column=2)
Tkinter.Label(body, font=("Helvetica", "10", "italic"), text='*To DeDRM multiple ebooks simultaneously, set your preferences and quit.\nThen drag and drop ebooks or folders onto the DeDRM_Drop_Target').grid(row=10, column=1, sticky=Tkconstants.E)
cur_row = cur_row + 1
Tkinter.Label(body, font=("Helvetica", "10", "italic"), text='*To DeDRM multiple ebooks simultaneously, set your preferences and quit.\nThen drag and drop ebooks or folders onto the DeDRM_Drop_Target').grid(row=cur_row, column=1, sticky=Tkconstants.E)
Tkinter.Label(body, text='').grid(row=11, column=0, columnspan=2, sticky=Tkconstants.E)
cur_row = cur_row + 1
Tkinter.Label(body, text='').grid(row=cur_row, column=0, columnspan=2, sticky=Tkconstants.E)
buttons = Tkinter.Frame(self)
buttons.pack()
@@ -295,6 +341,16 @@ class PrefsDialog(Toplevel):
self.kkpath.insert(0, kkpath)
return
def get_akkpath(self):
cpath = self.akkpath.get()
akkpath = tkFileDialog.askopenfilename(initialdir = cpath, parent=None, title='Select Android for Kindle backup file',
defaultextension='.ab', filetypes=[('Kindle for Android backup file', '.ab'), ('All Files', '.*')])
if akkpath:
akkpath = os.path.normpath(akkpath)
self.akkpath.delete(0, Tkconstants.END)
self.akkpath.insert(0, akkpath)
return
def get_bnkpath(self):
cpath = self.bnkpath.get()
bnkpath = tkFileDialog.askopenfilename(initialdir = cpath, parent=None, title='Select Barnes and Noble Key file',
@@ -347,6 +403,9 @@ class PrefsDialog(Toplevel):
kkpath = self.kkpath.get()
if os.path.dirname(kkpath) != prefdir:
new_prefs['kindlefile'] = kkpath
akkpath = self.akkpath.get()
if os.path.dirname(akkpath) != prefdir:
new_prefs['androidfile'] = akkpath
self.master.setPreferences(new_prefs)
# and update internal copies
self.prefs_array['pids'] = new_prefs['pids']
@@ -527,7 +586,7 @@ class ConvDialog(Toplevel):
self.p2 = Process(target=processPDB, args=(q, infile, outdir, rscpath))
self.p2.start()
return 0
if ext in ['.azw', '.azw1', '.azw3', '.azw4', '.prc', '.mobi', '.tpz']:
if ext in ['.azw', '.azw1', '.azw3', '.azw4', '.prc', '.mobi', '.pobi', '.tpz']:
self.p2 = Process(target=processK4MOBI,args=(q, infile, outdir, rscpath))
self.p2.start()
return 0

View File

@@ -22,13 +22,19 @@ li {margin-top: 0.5em}
<p>If you have upgraded from an earlier version of the plugin, any existing Barnes and Noble keys will have been automatically imported, so you might not need to do any more configuration. Continue reading for key generation and management instructions.</p>
<h3>Changes at Barnes & Noble</h3>
<p>In mid-2014, Barnes & Noble changed the way they generated encryption keys. Instead of deriving the key from the user's name and credit card number, they started generating a random key themselves, sending that key through to devices when they connected to the Barnes & Noble servers. This means that most users will now find that no combination of their name and CC# will work in decrypting their recently downloaded ebooks.</p>
<p>Someone commenting at Apprentice Alf's blog detailed a way to retrieve a new account key using the account's email address and password. This method has now been incorporated into the plugin.
<h3>Creating New Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering the necessary data to generate a new key.</p>
<ul>
<li><span class="bold">Unique Key Name:</span> this is a unique name you choose to help you identify the key. This name will show in the list of configured keys. Choose something that will help you remember the data (name, cc#) it was created with.</li>
<li><span class="bold">Your Name:</span> This is the name used by Barnes and Noble to generate your encryption key. Seemingly at random, Barnes and Noble choose one of three places from which to take this name. Most commonly, its your name as set in your Barnes &amp; Noble account, My Account page, directly under PERSONAL INFORMATION. Sometimes it is the the name used in the default shipping address, and sometimes its the name listed for the active credit card. If these names are different in your Barnes and Noble account preferences, I suggest creating one key for each version of your name. This name will not be stored anywhere on your computer or in calibre. It will only be used in the creation of the one-way hash/key thats stored in the preferences.</li>
<li><span class="bold">Credit Card#:</span> this is the default credit card number that was on file with Barnes and Noble at the time of download of the ebook to be de-DRMed. Just enter the 16 (15 for American Express) digits. As with the name, this number will not be stored anywhere on your computer or in calibre. It will only be used in the creation of the one-way hash/key thats stored in the preferences.</li>
<li><span class="bold">Unique Key Name:</span> this is a unique name you choose to help you identify the key. This name will show in the list of configured keys. Choose something that will help you remember the data (account email address) it was created with.</li>
<li><span class="bold">B&N/nook account email address:</span> This is the default email address for your Barnes and Noble/nook account. This email will not be stored anywhere on your computer or in calibre. It will only be used to fetch the account key that from the B&N server, and it is that key that will be stored in the preferences.</li>
<li><span class="bold">B&N/nook account password:</span> this is the password for your Barnes and Noble/nook account. As with the email address, this will not be stored anywhere on your computer or in calibre. It will only be used to fetch the key from the B&N server.</li>
</ul>
<p>Click the OK button to create and store the generated key. Or Cancel if you dont want to create a key.</p>
@@ -48,10 +54,15 @@ li {margin-top: 0.5em}
<h3>Importing Existing Keyfiles:</h3>
<p>At the bottom-left of the plugins customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing .b64 key files. Key files might come from being exported from this or older plugins, or may have been generated using the original i♥cabbages script.</p>
<p>At the bottom-left of the plugins customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing .b64 key files. Key files might come from being exported from this or older plugins, or may have been generated using the original i♥cabbages script, or you may have made it by following the instructions above.</p>
<p>Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.</p>
<h3>NOOK Study</h3>
<p>Books downloaded through NOOK Study may or may not use the key found using the above method. If a book is not decrypted successfully with any of the keys, the plugin will attempt to recover keys from the NOOK Study log file and use them.</p>
</body>
</html>

View File

@@ -1,4 +1,4 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
@@ -27,7 +27,7 @@ li {margin-top: 0.5em}
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering a new Kindle serial number.</p>
<ul>
<li><span class="bold">Eink Kindle Serial Number:</span> this is the unique serial number of your device. It usually starts with a B or a 9 and is sixteen characters long. For a reference of where to find serial numbers and their ranges, please refere to this <a href="http://wiki.mobileread.com/wiki/Kindle_serial_numbers">mobileread wiki page.</a></li>
<li><span class="bold">Eink Kindle Serial Number:</span> this is the unique serial number of your device. It usually starts with a B or a 9 and is sixteen characters long. For a reference of where to find serial numbers and their ranges, please refer to this <a href="http://wiki.mobileread.com/wiki/Kindle_serial_numbers">mobileread wiki page.</a></li>
</ul>
<p>Click the OK button to save the serial number. Or Cancel if you didnt want to enter a serial number.</p>

View File

@@ -17,7 +17,7 @@ p {margin-top: 0}
<body>
<h1>DeDRM Plugin <span class="version">(v6.0.0)</span></h1>
<h1>DeDRM Plugin <span class="version">(v6.3.0)</span></h1>
<p>This plugin removes DRM from ebooks when they are imported into calibre. If you already have DRMed ebooks in your calibre library, you will need to remove them and import them again.</p>

View File

@@ -0,0 +1,61 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Managing Kindle for Android Keys</title>
<style type="text/css">
span.version {font-size: 50%}
span.bold {font-weight: bold}
h3 {margin-bottom: 0}
p {margin-top: 0}
li {margin-top: 0.5em}
</style>
</head>
<body>
<h1>Managing Kindle for Android Keys</h1>
<p>Amazon's Kindle for Android application uses an internal key equivalent to an eInk Kindle's serial number. Extracting that key is a little tricky, but worth it, as it then allows the DRM to be removed from any Kindle ebooks that have been downloaded to that Android device.</p>
<p>Please note that it is not currently known whether the same applies to the Kindle application on the Kindle Fire and Fire HD.</p>
<h3>Getting the Kindle for Android backup file</h3>
<p>Obtain and install adb (Android Debug Bridge) on your computer. Details of how to do this are beyond the scope of this help file, but there are plenty of on-line guides.</p>
<p>Enable developer mode on your Android device. Again, look for an on-line guide for your device.</p>
<p>Once you have adb installed and your device in developer mode, connect your device to your computer with a USB cable and then open up a command line (Terminal on Mac OS X and cmd.exe on Windows) and enter "adb backup com.amazon.kindle" (without the quotation marks!) and press return. A file "backup.ab" should be created in your home directory.
<h3>Adding a Kindle for Android Key</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog with two main controls.
<ul>
<li><span class="bold">Choose backup file:</span> click this button and you will be prompted to find the backup.ab file you created earlier. Once selected the file will be processed to extract the decryption key, and if successful the file name will be displayed to the right of the button.</li>
<li><span class="bold">Unique Key Name:</span> this is a unique name you choose to help you identify the key. This name will show in the list of Kindle for Android keys. Enter a name that will help you remember which device this key came from.</li>
</ul>
<p>Click the OK button to store the Kindle for Android key for the current list of Kindle for Android keys. Or click Cancel if you dont want to store the key.</p>
<p>New keys are checked against the current list of keys before being added, and duplicates are discarded.</p>
<h3>Deleting Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted key in the list. You will be prompted once to be sure thats what you truly mean to do. Once gone, its permanently gone.</p>
<h3>Renaming Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will prompt you to enter a new name for the highlighted key in the list. Enter the new name for the key and click the OK button to use the new name, or Cancel to revert to the old name.</p>
<h3>Exporting Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a computers hard-drive. Use this button to export the highlighted key to a file (with a .k4a' file name extension). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.</p>
<h3>Importing Existing Keyfiles:</h3>
<p>At the bottom-left of the plugins customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import any .k4a file you obtained by using the androidkindlekey.py script manually, or by exporting from another copy of calibre.</p>
</body>
</html>

View File

@@ -31,14 +31,29 @@ __docformat__ = 'restructuredtext en'
# 6.0.3 - Fixes for Kindle for Mac and Windows non-ascii user names
# 6.0.4 - Fixes for stand-alone scripts and applications
# and pdb files in plugin and initial conversion of prefs.
# 6.0.5 - Fix a key issue
# 6.0.6 - Fix up an incorrect function call
# 6.0.7 - Error handling for incomplete PDF metadata
# 6.0.8 - Fixes a Wine key issue and topaz support
# 6.0.9 - Ported to work with newer versions of Calibre (moved to Qt5). Still supports older Qt4 versions.
# 6.1.0 - Fixed multiple books import problem and PDF import with no key problem
# 6.2.0 - Support for getting B&N key from nook Study log. Fix for UTF-8 filenames in Adobe ePubs.
# Fix for not copying needed files. Fix for getting default Adobe key for PDFs
# 6.2.1 - Fix for non-ascii Windows user names
# 6.2.2 - Added URL method for B&N/nook books
# 6.3.0 - Added in Kindle for Android serial number solution
# 6.3.1 - Version number bump for clarity
# 6.3.2 - Fixed Kindle for Android help file
# 6.3.3 - Bug fix for Kindle for PC support
# 6.3.4 - Fixes for Kindle for Android, Linux, and Kobo 3.17
"""
Decrypt DRMed ebooks.
"""
PLUGIN_NAME = u"DeDRM"
PLUGIN_VERSION_TUPLE = (6, 0, 7)
PLUGIN_VERSION_TUPLE = (6, 3, 4)
PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE])
# Include an html helpfile in the plugin's zipfile with the following name.
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
@@ -79,37 +94,27 @@ class DeDRM(FileTypePlugin):
name = PLUGIN_NAME
description = u"Removes DRM from Amazon Kindle, Adobe Adept (including Kobo), Barnes & Noble, Mobipocket and eReader ebooks. Credit given to i♥cabbages and The Dark Reverser for the original stand-alone scripts."
supported_platforms = ['linux', 'osx', 'windows']
author = u"DiapDealer, Apprentice Alf, The Dark Reverser and i♥cabbages"
author = u"Apprentice Alf, Aprentice Harper, The Dark Reverser 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','pdf','pdb','prc','mobi','azw','azw1','azw3','azw4','tpz'])
file_types = set(['epub','pdf','pdb','prc','mobi','pobi','azw','azw1','azw3','azw4','tpz'])
on_import = True
priority = 600
def initialize(self):
# convert old preferences, if necessary.
try:
from calibre_plugins.dedrm.prefs import convertprefs
convertprefs()
except:
traceback.print_exc()
def initialize(self):
"""
Dynamic modules can't be imported/loaded from a zipfile... so this routine
runs whenever the plugin gets initialized. This will extract the appropriate
Dynamic modules can't be imported/loaded from a zipfile.
So this routine will extract the appropriate
library for the target OS and copy it to the 'alfcrypto' subdirectory of
calibre's configuration directory. That 'alfcrypto' directory is then
inserted into the syspath (as the very first entry) in the run function
so the CDLL stuff will work in the alfcrypto.py script.
The extraction only happens once per version of the plugin
Also perform upgrade of preferences once per version
"""
try:
if iswindows:
names = [u"alfcrypto.dll",u"alfcrypto64.dll"]
elif isosx:
names = [u"libalfcrypto.dylib"]
else:
names = [u"libalfcrypto32.so",u"libalfcrypto64.so",u"kindlekey.py",u"adobekey.py",u"subasyncio.py"]
lib_dict = self.load_resources(names)
self.pluginsdir = os.path.join(config_dir,u"plugins")
if not os.path.exists(self.pluginsdir):
os.mkdir(self.pluginsdir)
@@ -122,9 +127,38 @@ class DeDRM(FileTypePlugin):
self.alfdir = os.path.join(self.maindir,u"libraryfiles")
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)
open(file_path,'wb').write(data)
# only continue if we've never run this version of the plugin before
self.verdir = os.path.join(self.maindir,PLUGIN_VERSION)
if not os.path.exists(self.verdir):
if iswindows:
names = [u"alfcrypto.dll",u"alfcrypto64.dll"]
elif isosx:
names = [u"libalfcrypto.dylib"]
else:
names = [u"libalfcrypto32.so",u"libalfcrypto64.so",u"kindlekey.py",u"adobekey.py",u"subasyncio.py"]
lib_dict = self.load_resources(names)
print u"{0} v{1}: Copying needed library files from plugin's zip".format(PLUGIN_NAME, PLUGIN_VERSION)
for entry, data in lib_dict.items():
file_path = os.path.join(self.alfdir, entry)
try:
os.remove(file_path)
except:
pass
try:
open(file_path,'wb').write(data)
except:
print u"{0} v{1}: Exception when copying needed library files".format(PLUGIN_NAME, PLUGIN_VERSION)
traceback.print_exc()
pass
# convert old preferences, if necessary.
from calibre_plugins.dedrm.prefs import convertprefs
convertprefs()
# mark that this version has been initialized
os.mkdir(self.verdir)
except Exception, e:
traceback.print_exc()
raise
@@ -162,7 +196,12 @@ class DeDRM(FileTypePlugin):
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)
try:
result = ignobleepub.decryptBook(userkey, inf.name, of.name)
except:
print u"{0} v{1}: Exception when trying to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
result = 1
of.close()
@@ -173,8 +212,69 @@ class DeDRM(FileTypePlugin):
print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime)
# perhaps we should see if we can get a key from a log file
print u"{0} v{1}: Looking for new NOOK Study Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
# get the default NOOK Study keys
defaultkeys = []
try:
if iswindows or isosx:
from calibre_plugins.dedrm.ignoblekey import nookkeys
defaultkeys = nookkeys()
else: # linux
from wineutils import WineGetKeys
scriptpath = os.path.join(self.alfdir,u"ignoblekey.py")
defaultkeys = WineGetKeys(scriptpath, u".b64",dedrmprefs['adobewineprefix'])
except:
print u"{0} v{1}: Exception when getting default NOOK Study Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
newkeys = []
for keyvalue in defaultkeys:
if keyvalue not in dedrmprefs['bandnkeys'].values():
newkeys.append(keyvalue)
if len(newkeys) > 0:
try:
for i,userkey in enumerate(newkeys):
print u"{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)
of = self.temporary_file(u".epub")
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
try:
result = ignobleepub.decryptBook(userkey, inf.name, of.name)
except:
print u"{0} v{1}: Exception when trying to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
result = 1
of.close()
if result == 0:
# Decryption was a success
# Store the new successful key in the defaults
print u"{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)
try:
dedrmprefs.addnamedvaluetoprefs('bandnkeys','nook_Study_key',keyvalue)
dedrmprefs.writeprefs()
print u"{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
except:
print u"{0} v{1}: Exception saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
# Return the modified PersistentTemporary file to calibre.
return of.name
print u"{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
except Exception, e:
pass
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt{2} after {3:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook),time.time()-self.starttime))
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
# import the Adobe Adept ePub handler
import calibre_plugins.dedrm.ineptepub as ineptepub
@@ -192,6 +292,8 @@ class DeDRM(FileTypePlugin):
try:
result = ineptepub.decryptBook(userkey, inf.name, of.name)
except:
print u"{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
result = 1
of.close()
@@ -222,6 +324,7 @@ class DeDRM(FileTypePlugin):
self.default_key = defaultkeys[0]
except:
print u"{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
self.default_key = u""
@@ -240,6 +343,8 @@ class DeDRM(FileTypePlugin):
try:
result = ineptepub.decryptBook(userkey, inf.name, of.name)
except:
print u"{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
result = 1
of.close()
@@ -251,7 +356,9 @@ class DeDRM(FileTypePlugin):
try:
dedrmprefs.addnamedvaluetoprefs('adeptkeys','default_key',keyvalue.encode('hex'))
dedrmprefs.writeprefs()
print u"{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
except:
print u"{0} v{1}: Exception when saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
# Return the modified PersistentTemporary file to calibre.
return of.name
@@ -262,12 +369,12 @@ class DeDRM(FileTypePlugin):
# Something went wrong with decryption.
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt{2} after {3:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook),time.time()-self.starttime))
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
# Not a Barnes & Noble nor an Adobe Adept
# Import the fixed epub.
print u"{0} v{1}: “{2}” is neither an Adobe Adept nor a Barnes & Noble encrypted ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))
return inf.name
raise DeDRMError(u"{0} v{1}: Couldn't decrypt after {2:.1f} seconds. DRM free perhaps?".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
def PDFDecrypt(self,path_to_ebook):
import calibre_plugins.dedrm.prefs as prefs
@@ -285,6 +392,8 @@ class DeDRM(FileTypePlugin):
try:
result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name)
except:
print u"{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
result = 1
of.close()
@@ -294,67 +403,73 @@ class DeDRM(FileTypePlugin):
# Return the modified PersistentTemporary file to calibre.
return of.name
# perhaps we need to get a new default ADE key
print u"{0} v{1}: Looking for new default Adobe Digital Editions Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime)
# get the default Adobe keys
defaultkeys = []
# perhaps we need to get a new default ADE key
print u"{0} v{1}: Looking for new default Adobe Digital Editions Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
# get the default Adobe keys
defaultkeys = []
try:
if iswindows or isosx:
import calibre_plugins.dedrm.adobekey as adobe
from calibre_plugins.dedrm.adobekey import adeptkeys
try:
defaultkeys = adobe.adeptkeys()
except:
pass
else:
# linux
try:
from wineutils import WineGetKeys
defaultkeys = adeptkeys()
else: # linux
from wineutils import WineGetKeys
scriptpath = os.path.join(self.alfdir,u"adobekey.py")
defaultkeys = self.WineGetKeys(scriptpath, u".der",dedrmprefs['adobewineprefix'])
except:
pass
scriptpath = os.path.join(self.alfdir,u"adobekey.py")
defaultkeys = WineGetKeys(scriptpath, u".der",dedrmprefs['adobewineprefix'])
newkeys = []
for keyvalue in defaultkeys:
if keyvalue.encode('hex') not in dedrmprefs['adeptkeys'].values():
newkeys.append(keyvalue)
self.default_key = defaultkeys[0]
except:
print u"{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
self.default_key = u""
if len(newkeys) > 0:
try:
for i,userkey in enumerate(newkeys):
print u"{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)
of = self.temporary_file(u".pdf")
newkeys = []
for keyvalue in defaultkeys:
if keyvalue.encode('hex') not in dedrmprefs['adeptkeys'].values():
newkeys.append(keyvalue)
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
if len(newkeys) > 0:
try:
for i,userkey in enumerate(newkeys):
print u"{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)
of = self.temporary_file(u".pdf")
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
try:
result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name)
except:
print u"{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
result = 1
of.close()
if result == 0:
# Decryption was a success
# Store the new successful key in the defaults
print u"{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)
try:
result = ineptepdf.decryptBook(userkey, inf.name, of.name)
dedrmprefs.addnamedvaluetoprefs('adeptkeys','default_key',keyvalue.encode('hex'))
dedrmprefs.writeprefs()
print u"{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
except:
result = 1
print u"{0} v{1}: Exception when saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
# Return the modified PersistentTemporary file to calibre.
return of.name
of.close()
if result == 0:
# Decryption was a success
# Store the new successful key in the defaults
print u"{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)
try:
dedrmprefs.addnamedvaluetoprefs('adeptkeys','default_key',keyvalue.encode('hex'))
dedrmprefs.writeprefs()
except:
traceback.print_exc()
# Return the modified PersistentTemporary file to calibre.
return of.name
print u"{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
except Exception, e:
pass
print u"{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
except Exception, e:
pass
# Something went wrong with decryption.
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt{2} after {3:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook),time.time()-self.starttime))
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
def KindleMobiDecrypt(self,path_to_ebook):
@@ -371,10 +486,15 @@ class DeDRM(FileTypePlugin):
dedrmprefs = prefs.DeDRM_Prefs()
pids = dedrmprefs['pids']
serials = dedrmprefs['serials']
for android_serials_list in dedrmprefs['androidkeys'].values():
#print android_serials_list
serials.extend(android_serials_list)
#print serials
androidFiles = []
kindleDatabases = dedrmprefs['kindlekeys'].items()
try:
book = k4mobidedrm.GetDecryptedBook(path_to_ebook,kindleDatabases,serials,pids,self.starttime)
book = k4mobidedrm.GetDecryptedBook(path_to_ebook,kindleDatabases,androidFiles,serials,pids,self.starttime)
except Exception, e:
decoded = False
# perhaps we need to get a new default Kindle for Mac/PC key
@@ -391,8 +511,10 @@ class DeDRM(FileTypePlugin):
from wineutils import WineGetKeys
scriptpath = os.path.join(self.alfdir,u"kindlekey.py")
defaultkeys = self.WineGetKeys(scriptpath, u".k4i",dedrmprefs['kindlewineprefix'])
defaultkeys = WineGetKeys(scriptpath, u".k4i",dedrmprefs['kindlewineprefix'])
except:
print u"{0} v{1}: Exception when getting default Kindle Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
pass
newkeys = {}
@@ -403,7 +525,7 @@ class DeDRM(FileTypePlugin):
if len(newkeys) > 0:
print u"{0} v{1}: Found {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), u"key" if len(newkeys)==1 else u"keys")
try:
book = k4mobidedrm.GetDecryptedBook(path_to_ebook,newkeys.items(),[],[],self.starttime)
book = k4mobidedrm.GetDecryptedBook(path_to_ebook,newkeys.items(),[],[],[],self.starttime)
decoded = True
# store the new successful keys in the defaults
print u"{0} v{1}: Saving {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), u"key" if len(newkeys)==1 else u"keys")
@@ -415,8 +537,7 @@ class DeDRM(FileTypePlugin):
if not decoded:
#if you reached here then no luck raise and exception
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
traceback.print_exc()
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt “{4}” after {3:.1f} seconds with error: {2}\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0],time.time()-self.starttime,os.path.basename(path_to_ebook)))
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
of = self.temporary_file(book.getBookExtension())
book.getFile(of.name)
@@ -445,12 +566,13 @@ class DeDRM(FileTypePlugin):
# Decryption was successful return the modified PersistentTemporary
# file to Calibre's import process.
if result == 0:
print u"{0} v{1}: Successfully decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime)
return of.name
print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime)
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt{2} after {3:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook),time.time()-self.starttime))
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
def run(self, path_to_ebook):
@@ -463,7 +585,7 @@ class DeDRM(FileTypePlugin):
self.starttime = time.time()
booktype = os.path.splitext(path_to_ebook)[1].lower()[1:]
if booktype in ['prc','mobi','azw','azw1','azw3','azw4','tpz']:
if booktype in ['prc','mobi','pobi','azw','azw1','azw3','azw4','tpz']:
# Kindle/Mobipocket
decrypted_ebook = self.KindleMobiDecrypt(path_to_ebook)
elif booktype == 'pdb':
@@ -480,7 +602,7 @@ class DeDRM(FileTypePlugin):
else:
print u"Unknown booktype {0}. Passing back to calibre unchanged".format(booktype)
return path_to_ebook
print u"{0} v{1}: Successfully decrypted book after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
print u"{0} v{1}: Finished after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
return decrypted_ebook
def is_customizable(self):

View File

@@ -0,0 +1,468 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import with_statement
# androidkindlekey.py
# Copyright © 2013-15 by Thom and Apprentice Harper
# Some portions Copyright © 2010-15 by some_updates and Apprentice Alf
#
# Revision history:
# 1.0 - AmazonSecureStorage.xml decryption to serial number
# 1.1 - map_data_storage.db decryption to serial number
# 1.2 - Changed to be callable from AppleScript by returning only serial number
# - and changed name to androidkindlekey.py
# - and added in unicode command line support
# 1.3 - added in TkInter interface, output to a file
# 1.4 - Fix some problems identified by Aldo Bleeker
# 1.5 - Fix another problem identified by Aldo Bleeker
"""
Retrieve Kindle for Android Serial Number.
"""
__license__ = 'GPL v3'
__version__ = '1.5'
import os
import sys
import traceback
import getopt
import tempfile
import zlib
import tarfile
from hashlib import md5
from cStringIO import StringIO
from binascii import a2b_hex, b2a_hex
# Routines common to Mac and PC
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
# encoded using "replace" before writing them.
class SafeUnbuffered:
def __init__(self, stream):
self.stream = stream
self.encoding = stream.encoding
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
try:
from calibre.constants import iswindows, isosx
except:
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
def unicode_argv():
if iswindows:
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
# as a list of Unicode strings and encode them as utf-8
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"kindlekey.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
class DrmException(Exception):
pass
STORAGE = u"backup.ab"
STORAGE1 = u"AmazonSecureStorage.xml"
STORAGE2 = u"map_data_storage.db"
class AndroidObfuscation(object):
'''AndroidObfuscation
For the key, it's written in java, and run in android dalvikvm
'''
key = a2b_hex('0176e04c9408b1702d90be333fd53523')
def encrypt(self, plaintext):
cipher = self._get_cipher()
padding = len(self.key) - len(plaintext) % len(self.key)
plaintext += chr(padding) * padding
return b2a_hex(cipher.encrypt(plaintext))
def decrypt(self, ciphertext):
cipher = self._get_cipher()
plaintext = cipher.decrypt(a2b_hex(ciphertext))
return plaintext[:-ord(plaintext[-1])]
def _get_cipher(self):
try:
from Crypto.Cipher import AES
return AES.new(self.key)
except ImportError:
from aescbc import AES, noPadding
return AES(self.key, padding=noPadding())
class AndroidObfuscationV2(AndroidObfuscation):
'''AndroidObfuscationV2
'''
count = 503
password = 'Thomsun was here!'
def __init__(self, salt):
key = self.password + salt
for _ in range(self.count):
key = md5(key).digest()
self.key = key[:8]
self.iv = key[8:16]
def _get_cipher(self):
try :
from Crypto.Cipher import DES
return DES.new(self.key, DES.MODE_CBC, self.iv)
except ImportError:
from python_des import Des, CBC
return Des(self.key, CBC, self.iv)
def parse_preference(path):
''' parse android's shared preference xml '''
storage = {}
read = open(path)
for line in read:
line = line.strip()
# <string name="key">value</string>
if line.startswith('<string name="'):
index = line.find('"', 14)
key = line[14:index]
value = line[index+2:-9]
storage[key] = value
read.close()
return storage
def get_serials1(path=STORAGE1):
''' get serials from android's shared preference xml '''
if not os.path.isfile(path):
return []
storage = parse_preference(path)
salt = storage.get('AmazonSaltKey')
if salt and len(salt) == 16:
obfuscation = AndroidObfuscationV2(a2b_hex(salt))
else:
obfuscation = AndroidObfuscation()
def get_value(key):
encrypted_key = obfuscation.encrypt(key)
encrypted_value = storage.get(encrypted_key)
if encrypted_value:
return obfuscation.decrypt(encrypted_value)
return ''
# also see getK4Pids in kgenpids.py
try:
dsnid = get_value('DsnId')
except:
sys.stderr.write('cannot get DsnId\n')
return []
try:
tokens = set(get_value('kindle.account.tokens').split(','))
except:
return []
serials = []
if dsnid:
serials.append(dsnid)
for token in tokens:
if token:
serials.append('%s%s' % (dsnid, token))
serials.append(token)
return serials
def get_serials2(path=STORAGE2):
''' get serials from android's sql database '''
if not os.path.isfile(path):
return []
import sqlite3
connection = sqlite3.connect(path)
cursor = connection.cursor()
cursor.execute('''select userdata_value from userdata where userdata_key like '%/%token.device.deviceserialname%' ''')
userdata_keys = cursor.fetchall()
dsns = []
for userdata_row in userdata_keys:
try:
if userdata_row and userdata_row[0]:
userdata_utf8 = userdata_row[0].encode('utf8')
if len(userdata_utf8) > 0:
dsns.append(userdata_utf8)
except:
print "Error getting one of the device serial name keys"
traceback.print_exc()
pass
dsns = list(set(dsns))
cursor.execute('''select userdata_value from userdata where userdata_key like '%/%kindle.account.tokens%' ''')
userdata_keys = cursor.fetchall()
tokens = []
for userdata_row in userdata_keys:
try:
if userdata_row and userdata_row[0]:
userdata_utf8 = userdata_row[0].encode('utf8')
if len(userdata_utf8) > 0:
tokens.append(userdata_utf8)
except:
print "Error getting one of the account token keys"
traceback.print_exc()
pass
tokens = list(set(tokens))
serials = []
for x in dsns:
serials.append(x)
for y in tokens:
serials.append('%s%s' % (x, y))
for y in tokens:
serials.append(y)
return serials
def get_serials(path=STORAGE):
'''get serials from files in from android backup.ab
backup.ab can be get using adb command:
shell> adb backup com.amazon.kindle
or from individual files if they're passed.
'''
if not os.path.isfile(path):
return []
basename = os.path.basename(path)
if basename == STORAGE1:
return get_serials1(path)
elif basename == STORAGE2:
return get_serials2(path)
output = None
try :
read = open(path, 'rb')
head = read.read(24)
if head[:14] == 'ANDROID BACKUP':
output = StringIO(zlib.decompress(read.read()))
except Exception:
pass
finally:
read.close()
if not output:
return []
serials = []
tar = tarfile.open(fileobj=output)
for member in tar.getmembers():
if member.name.strip().endswith(STORAGE1):
write = tempfile.NamedTemporaryFile(mode='wb', delete=False)
write.write(tar.extractfile(member).read())
write.close()
write_path = os.path.abspath(write.name)
serials.extend(get_serials1(write_path))
os.remove(write_path)
elif member.name.strip().endswith(STORAGE2):
write = tempfile.NamedTemporaryFile(mode='wb', delete=False)
write.write(tar.extractfile(member).read())
write.close()
write_path = os.path.abspath(write.name)
serials.extend(get_serials2(write_path))
os.remove(write_path)
return list(set(serials))
__all__ = [ 'get_serials', 'getkey']
# procedure for CLI and GUI interfaces
# returns single or multiple keys (one per line) in the specified file
def getkey(outfile, inpath):
keys = get_serials(inpath)
if len(keys) > 0:
with file(outfile, 'w') as keyfileout:
for key in keys:
keyfileout.write(key)
keyfileout.write("\n")
return True
return False
def usage(progname):
print u"Decrypts the serial number(s) of Kindle For Android from Android backup or file"
print u"Get backup.ab file using adb backup com.amazon.kindle for Android 4.0+."
print u"Otherwise extract AmazonSecureStorage.xml from /data/data/com.amazon.kindle/shared_prefs/AmazonSecureStorage.xml"
print u"Or map_data_storage.db from /data/data/com.amazon.kindle/databases/map_data_storage.db"
print u""
print u"Usage:"
print u" {0:s} [-h] [-b <backup.ab>] [<outfile.k4a>]".format(progname)
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv()
progname = os.path.basename(argv[0])
print u"{0} v{1}\nCopyright © 2010-2015 Thom, some_updates, Apprentice Alf and Apprentice Harper".format(progname,__version__)
try:
opts, args = getopt.getopt(argv[1:], "hb:")
except getopt.GetoptError, err:
usage(progname)
print u"\nError in options or arguments: {0}".format(err.args[0])
return 2
inpath = ""
for o, a in opts:
if o == "-h":
usage(progname)
return 0
if o == "-b":
inpath = a
if len(args) > 1:
usage(progname)
return 2
if len(args) == 1:
# save to the specified file or directory
outfile = args[0]
if not os.path.isabs(outfile):
outfile = os.path.join(os.path.dirname(argv[0]),outfile)
outfile = os.path.abspath(outfile)
if os.path.isdir(outfile):
outfile = os.path.join(os.path.dirname(argv[0]),"androidkindlekey.k4a")
else:
# save to the same directory as the script
outfile = os.path.join(os.path.dirname(argv[0]),"androidkindlekey.k4a")
# make sure the outpath is OK
outfile = os.path.realpath(os.path.normpath(outfile))
if not os.path.isfile(inpath):
usage(progname)
print u"\n{0:s} file not found".format(inpath)
return 2
if getkey(outfile, inpath):
print u"\nSaved Kindle for Android key to {0}".format(outfile)
else:
print u"\nCould not retrieve Kindle for Android key."
return 0
def gui_main():
try:
import Tkinter
import Tkconstants
import tkMessageBox
import tkFileDialog
except:
print "Tkinter not installed"
return cli_main()
class DecryptionDialog(Tkinter.Frame):
def __init__(self, root):
Tkinter.Frame.__init__(self, root, border=5)
self.status = Tkinter.Label(self, text=u"Select backup.ab file")
self.status.pack(fill=Tkconstants.X, expand=1)
body = Tkinter.Frame(self)
body.pack(fill=Tkconstants.X, expand=1)
sticky = Tkconstants.E + Tkconstants.W
body.grid_columnconfigure(1, weight=2)
Tkinter.Label(body, text=u"Backup file").grid(row=0, column=0)
self.keypath = Tkinter.Entry(body, width=40)
self.keypath.grid(row=0, column=1, sticky=sticky)
self.keypath.insert(2, u"backup.ab")
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
button.grid(row=0, column=2)
buttons = Tkinter.Frame(self)
buttons.pack()
button2 = Tkinter.Button(
buttons, text=u"Extract", width=10, command=self.generate)
button2.pack(side=Tkconstants.LEFT)
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
button3 = Tkinter.Button(
buttons, text=u"Quit", width=10, command=self.quit)
button3.pack(side=Tkconstants.RIGHT)
def get_keypath(self):
keypath = tkFileDialog.askopenfilename(
parent=None, title=u"Select backup.ab file",
defaultextension=u".ab",
filetypes=[('adb backup com.amazon.kindle', '.ab'),
('All Files', '.*')])
if keypath:
keypath = os.path.normpath(keypath)
self.keypath.delete(0, Tkconstants.END)
self.keypath.insert(0, keypath)
return
def generate(self):
inpath = self.keypath.get()
self.status['text'] = u"Getting key..."
try:
keys = get_serials(inpath)
keycount = 0
for key in keys:
while True:
keycount += 1
outfile = os.path.join(progpath,u"kindlekey{0:d}.k4a".format(keycount))
if not os.path.exists(outfile):
break
with file(outfile, 'w') as keyfileout:
keyfileout.write(key)
success = True
tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile))
except Exception, e:
self.status['text'] = u"Error: {0}".format(e.args[0])
return
self.status['text'] = u"Select backup.ab file"
argv=unicode_argv()
progpath, progname = os.path.split(argv[0])
root = Tkinter.Tk()
root.title(u"Kindle for Android Key Extraction v.{0}".format(__version__))
root.resizable(True, False)
root.minsize(300, 0)
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
root.mainloop()
return 0
if __name__ == '__main__':
if len(sys.argv) > 1:
sys.exit(cli_main())
sys.exit(gui_main())

View File

@@ -9,16 +9,24 @@ __license__ = 'GPL v3'
import os, traceback, json
# PyQT4 modules (part of calibre).
from PyQt4.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit,
try:
from PyQt5.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit,
QGroupBox, QPushButton, QListWidget, QListWidgetItem,
QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl, QString)
from PyQt4 import QtGui
QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl)
except ImportError:
from PyQt4.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit,
QGroupBox, QPushButton, QListWidget, QListWidgetItem,
QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl)
try:
from PyQt5 import Qt as QtGui
except ImportError:
from PyQt4 import QtGui
from zipfile import ZipFile
# calibre modules and constants.
from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url,
choose_dir, choose_files)
choose_dir, choose_files, choose_save_file)
from calibre.utils.config import dynamic, config_dir, JSONConfig
from calibre.constants import iswindows, isosx
@@ -28,6 +36,7 @@ from calibre_plugins.dedrm.__init__ import RESOURCE_NAME as help_file_name
from calibre_plugins.dedrm.utilities import uStrCmp
import calibre_plugins.dedrm.prefs as prefs
import calibre_plugins.dedrm.androidkindlekey as androidkindlekey
class ConfigWidget(QWidget):
def __init__(self, plugin_path, alfdir):
@@ -45,6 +54,7 @@ class ConfigWidget(QWidget):
self.tempdedrmprefs['adeptkeys'] = self.dedrmprefs['adeptkeys'].copy()
self.tempdedrmprefs['ereaderkeys'] = self.dedrmprefs['ereaderkeys'].copy()
self.tempdedrmprefs['kindlekeys'] = self.dedrmprefs['kindlekeys'].copy()
self.tempdedrmprefs['androidkeys'] = self.dedrmprefs['androidkeys'].copy()
self.tempdedrmprefs['pids'] = list(self.dedrmprefs['pids'])
self.tempdedrmprefs['serials'] = list(self.dedrmprefs['serials'])
self.tempdedrmprefs['adobewineprefix'] = self.dedrmprefs['adobewineprefix']
@@ -75,6 +85,10 @@ class ConfigWidget(QWidget):
self.bandn_button.setToolTip(_(u"Click to manage keys for Barnes and Noble ebooks"))
self.bandn_button.setText(u"Barnes and Noble ebooks")
self.bandn_button.clicked.connect(self.bandn_keys)
self.kindle_android_button = QtGui.QPushButton(self)
self.kindle_android_button.setToolTip(_(u"Click to manage keys for Kindle for Android ebooks"))
self.kindle_android_button.setText(u"Kindle for Android ebooks")
self.kindle_android_button.clicked.connect(self.kindle_android)
self.kindle_serial_button = QtGui.QPushButton(self)
self.kindle_serial_button.setToolTip(_(u"Click to manage eInk Kindle serial numbers for Kindle ebooks"))
self.kindle_serial_button.setText(u"eInk Kindle ebooks")
@@ -96,6 +110,7 @@ class ConfigWidget(QWidget):
self.ereader_button.setText(u"eReader ebooks")
self.ereader_button.clicked.connect(self.ereader_keys)
button_layout.addWidget(self.kindle_serial_button)
button_layout.addWidget(self.kindle_android_button)
button_layout.addWidget(self.bandn_button)
button_layout.addWidget(self.mobi_button)
button_layout.addWidget(self.ereader_button)
@@ -107,6 +122,10 @@ class ConfigWidget(QWidget):
def kindle_serials(self):
d = ManageKeysDialog(self,u"EInk Kindle Serial Number",self.tempdedrmprefs['serials'], AddSerialDialog)
d.exec_()
def kindle_android(self):
d = ManageKeysDialog(self,u"Kindle for Android Key",self.tempdedrmprefs['androidkeys'], AddAndroidDialog, 'k4a')
d.exec_()
def kindle_keys(self):
if isosx or iswindows:
@@ -154,6 +173,7 @@ class ConfigWidget(QWidget):
self.dedrmprefs.set('adeptkeys', self.tempdedrmprefs['adeptkeys'])
self.dedrmprefs.set('ereaderkeys', self.tempdedrmprefs['ereaderkeys'])
self.dedrmprefs.set('kindlekeys', self.tempdedrmprefs['kindlekeys'])
self.dedrmprefs.set('androidkeys', self.tempdedrmprefs['androidkeys'])
self.dedrmprefs.set('pids', self.tempdedrmprefs['pids'])
self.dedrmprefs.set('serials', self.tempdedrmprefs['serials'])
self.dedrmprefs.set('adobewineprefix', self.tempdedrmprefs['adobewineprefix'])
@@ -180,6 +200,7 @@ class ManageKeysDialog(QDialog):
self.import_key = (keyfile_ext != u"")
self.binary_file = (keyfile_ext == u"der")
self.json_file = (keyfile_ext == u"k4i")
self.android_file = (keyfile_ext == u"k4a")
self.wineprefix = wineprefix
self.setWindowTitle("{0} {1}: Manage {2}s".format(PLUGIN_NAME, PLUGIN_VERSION, self.key_type_name))
@@ -211,8 +232,8 @@ class ManageKeysDialog(QDialog):
button_layout = QVBoxLayout()
keys_group_box_layout.addLayout(button_layout)
self._add_key_button = QtGui.QToolButton(self)
self._add_key_button.setToolTip(u"Create new {0}".format(self.key_type_name))
self._add_key_button.setIcon(QIcon(I('plus.png')))
self._add_key_button.setToolTip(u"Create new {0}".format(self.key_type_name))
self._add_key_button.clicked.connect(self.add_key)
button_layout.addWidget(self._add_key_button)
@@ -267,7 +288,7 @@ class ManageKeysDialog(QDialog):
def getwineprefix(self):
if self.wineprefix is not None:
return unicode(self.wp_lineedit.text().toUtf8(), 'utf8').strip()
return unicode(self.wp_lineedit.text()).strip()
return u""
def populate_list(self):
@@ -316,7 +337,7 @@ class ManageKeysDialog(QDialog):
if d.result() != d.Accepted:
# rename cancelled or moot.
return
keyname = unicode(self.listy.currentItem().text().toUtf8(),'utf8')
keyname = unicode(self.listy.currentItem().text())
if not question_dialog(self, "{0} {1}: Confirm Rename".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to rename the {2} named <strong>{0}</strong> to <strong>{1}</strong>?".format(keyname,d.key_name,self.key_type_name), show_copy_button=False, default_yes=False):
return
self.plugin_keys[d.key_name] = self.plugin_keys[keyname]
@@ -328,7 +349,7 @@ class ManageKeysDialog(QDialog):
def delete_key(self):
if not self.listy.currentItem():
return
keyname = unicode(self.listy.currentItem().text().toUtf8(), 'utf8')
keyname = unicode(self.listy.currentItem().text())
if not question_dialog(self, "{0} {1}: Confirm Delete".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to delete the {1} <strong>{0}</strong>?".format(keyname, self.key_type_name), show_copy_button=False, default_yes=False):
return
if type(self.plugin_keys) == dict:
@@ -352,9 +373,10 @@ class ManageKeysDialog(QDialog):
open_url(QUrl(url))
def migrate_files(self):
dynamic[PLUGIN_NAME + u"config_dir"] = config_dir
files = choose_files(self, PLUGIN_NAME + u"config_dir",
u"Select {0} files to import".format(self.key_type_name), [(u"{0} files".format(self.key_type_name), [self.keyfile_ext])], False)
unique_dlg_name = PLUGIN_NAME + u"import {0} keys".format(self.key_type_name).replace(' ', '_') #takes care of automatically remembering last directory
caption = u"Select {0} files to import".format(self.key_type_name)
filters = [(u"{0} files".format(self.key_type_name), [self.keyfile_ext])]
files = choose_files(self, unique_dlg_name, caption, filters, all_files=False)
counter = 0
skipped = 0
if files:
@@ -368,6 +390,9 @@ class ManageKeysDialog(QDialog):
new_key_value = new_key_value.encode('hex')
elif self.json_file:
new_key_value = json.loads(new_key_value)
elif self.android_file:
# convert to list of the keys in the string
new_key_value = new_key_value.splitlines()
match = False
for key in self.plugin_keys.keys():
if uStrCmp(new_key_name, key, True):
@@ -386,7 +411,7 @@ class ManageKeysDialog(QDialog):
else:
counter += 1
self.plugin_keys[new_key_name] = new_key_value
msg = u""
if counter+skipped > 1:
if counter > 0:
@@ -408,21 +433,22 @@ class ManageKeysDialog(QDialog):
r = error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
_(errmsg), show=True, show_copy_button=False)
return
filter = QString(u"{0} Files (*.{1})".format(self.key_type_name, self.keyfile_ext))
keyname = unicode(self.listy.currentItem().text().toUtf8(), 'utf8')
if dynamic.get(PLUGIN_NAME + 'save_dir'):
defaultname = os.path.join(dynamic.get(PLUGIN_NAME + 'save_dir'), u"{0}.{1}".format(keyname , self.keyfile_ext))
else:
defaultname = os.path.join(os.path.expanduser('~'), u"{0}.{1}".format(keyname , self.keyfile_ext))
filename = unicode(QtGui.QFileDialog.getSaveFileName(self, u"Save {0} File as...".format(self.key_type_name), defaultname,
u"{0} Files (*.{1})".format(self.key_type_name,self.keyfile_ext), filter))
keyname = unicode(self.listy.currentItem().text())
unique_dlg_name = PLUGIN_NAME + u"export {0} keys".format(self.key_type_name).replace(' ', '_') #takes care of automatically remembering last directory
caption = u"Save {0} File as...".format(self.key_type_name)
filters = [(u"{0} Files".format(self.key_type_name), [u"{0}".format(self.keyfile_ext)])]
defaultname = u"{0}.{1}".format(keyname, self.keyfile_ext)
filename = choose_save_file(self, unique_dlg_name, caption, filters, all_files=False, initial_filename=defaultname)
if filename:
dynamic[PLUGIN_NAME + 'save_dir'] = os.path.split(filename)[0]
with file(filename, 'w') as fname:
with file(filename, 'wb') as fname:
if self.binary_file:
fname.write(self.plugin_keys[keyname].decode('hex'))
elif self.json_file:
fname.write(json.dumps(self.plugin_keys[keyname]))
elif self.android_file:
for key in self.plugin_keys[keyname]:
fname.write(key)
fname.write("\n")
else:
fname.write(self.plugin_keys[keyname])
@@ -458,7 +484,7 @@ class RenameKeyDialog(QDialog):
self.resize(self.sizeHint())
def accept(self):
if self.key_ledit.text().isEmpty() or unicode(self.key_ledit.text()).isspace():
if not unicode(self.key_ledit.text()) or unicode(self.key_ledit.text()).isspace():
errmsg = u"Key name field cannot be empty!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
_(errmsg), show=True, show_copy_button=False)
@@ -479,7 +505,7 @@ class RenameKeyDialog(QDialog):
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
@@ -509,20 +535,17 @@ class AddBandNKeyDialog(QDialog):
u"<p>It should be something that will help you remember " +
u"what personal information was used to create it."))
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
name_group = QHBoxLayout()
data_group_box_layout.addLayout(name_group)
name_group.addWidget(QLabel(u"Your Name:", self))
name_group.addWidget(QLabel(u"B&N/nook account email address:", self))
self.name_ledit = QLineEdit(u"", self)
self.name_ledit.setToolTip(_(u"<p>Enter your name as it appears in your B&N " +
u"account or on your credit card.</p>" +
self.name_ledit.setToolTip(_(u"<p>Enter your email address as it appears in your B&N " +
u"account.</p>" +
u"<p>It will only be used to generate this " +
u"one-time key and won\'t be stored anywhere " +
u"key and won\'t be stored anywhere " +
u"in calibre or on your computer.</p>" +
u"<p>(ex: Jonathan Smith)"))
u"<p>eg: apprenticeharper@gmail.com</p>"))
name_group.addWidget(self.name_ledit)
name_disclaimer_label = QLabel(_(u"(Will not be saved in configuration data)"), self)
name_disclaimer_label.setAlignment(Qt.AlignHCenter)
@@ -530,13 +553,12 @@ class AddBandNKeyDialog(QDialog):
ccn_group = QHBoxLayout()
data_group_box_layout.addLayout(ccn_group)
ccn_group.addWidget(QLabel(u"Credit Card#:", self))
ccn_group.addWidget(QLabel(u"B&N/nook account password:", self))
self.cc_ledit = QLineEdit(u"", self)
self.cc_ledit.setToolTip(_(u"<p>Enter the full credit card number on record " +
u"in your B&N account.</p>" +
u"<p>No spaces or dashes... just the numbers. " +
u"This number will only be used to generate this " +
u"one-time key and won\'t be stored anywhere in " +
self.cc_ledit.setToolTip(_(u"<p>Enter the password " +
u"for your B&N account.</p>" +
u"<p>The password will only be used to generate this " +
u"key and won\'t be stored anywhere in " +
u"calibre or on your computer."))
ccn_group.addWidget(self.cc_ledit)
ccn_disclaimer_label = QLabel(_('(Will not be saved in configuration data)'), self)
@@ -553,29 +575,26 @@ class AddBandNKeyDialog(QDialog):
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
@property
def key_value(self):
from calibre_plugins.dedrm.ignoblekeygen import generate_key as generate_bandn_key
return generate_bandn_key(self.user_name,self.cc_number)
from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key
return fetch_bandn_key(self.user_name,self.cc_number)
@property
def user_name(self):
return unicode(self.name_ledit.text().toUtf8(), 'utf8').strip().lower().replace(' ','')
return unicode(self.name_ledit.text()).strip().lower().replace(' ','')
@property
def cc_number(self):
return unicode(self.cc_ledit.text().toUtf8(), 'utf8').strip().replace(' ', '').replace('-','')
return unicode(self.cc_ledit.text()).strip()
def accept(self):
if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace():
errmsg = u"All fields are required!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if not self.cc_number.isdigit():
errmsg = u"Numbers only in the credit card number field!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) < 4:
errmsg = u"Key name must be at <i>least</i> 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
@@ -600,9 +619,6 @@ class AddEReaderDialog(QDialog):
self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(u"<p>Enter an identifying name for this new key.\nIt should be something that will help you remember what personal information was used to create it.")
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
name_group = QHBoxLayout()
data_group_box_layout.addLayout(name_group)
@@ -634,7 +650,7 @@ class AddEReaderDialog(QDialog):
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
@property
def key_value(self):
@@ -643,11 +659,11 @@ class AddEReaderDialog(QDialog):
@property
def user_name(self):
return unicode(self.name_ledit.text().toUtf8(), 'utf8').strip().lower().replace(' ','')
return unicode(self.name_ledit.text()).strip().lower().replace(' ','')
@property
def cc_number(self):
return unicode(self.cc_ledit.text().toUtf8(), 'utf8').strip().replace(' ', '').replace('-','')
return unicode(self.cc_ledit.text()).strip().replace(' ', '').replace('-','')
def accept(self):
@@ -701,9 +717,7 @@ class AddAdeptDialog(QDialog):
self.key_ledit = QLineEdit(u"default_key", self)
self.key_ledit.setToolTip(u"<p>Enter an identifying name for the current default Adobe Digital Editions key.")
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
self.button_box.accepted.connect(self.accept)
else:
default_key_error = QLabel(u"The default encryption key for Adobe Digital Editions could not be found.", self)
@@ -719,7 +733,7 @@ class AddAdeptDialog(QDialog):
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
@property
def key_value(self):
@@ -774,15 +788,14 @@ class AddKindleDialog(QDialog):
self.key_ledit = QLineEdit(u"default_key", self)
self.key_ledit.setToolTip(u"<p>Enter an identifying name for the current default Kindle for Mac/PC key.")
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
self.button_box.accepted.connect(self.accept)
else:
default_key_error = QLabel(u"The default encryption key for Kindle for Mac/PC could not be found.", self)
default_key_error.setAlignment(Qt.AlignHCenter)
layout.addWidget(default_key_error)
# if no default, bot buttons do the same
# if no default, both buttons do the same
self.button_box.accepted.connect(self.reject)
self.button_box.rejected.connect(self.reject)
@@ -792,7 +805,7 @@ class AddKindleDialog(QDialog):
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
@property
def key_value(self):
@@ -828,9 +841,6 @@ class AddSerialDialog(QDialog):
self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(u"Enter an eInk Kindle serial number. EInk Kindle serial numbers are 16 characters long and usually start with a 'B' or a '9'. Kindle Serial Numbers are case-sensitive, so be sure to enter the upper and lower case letters unchanged.")
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
@@ -841,11 +851,11 @@ class AddSerialDialog(QDialog):
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
@property
def key_value(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
def accept(self):
if len(self.key_name) == 0 or self.key_name.isspace():
@@ -857,6 +867,89 @@ class AddSerialDialog(QDialog):
QDialog.accept(self)
class AddAndroidDialog(QDialog):
def __init__(self, parent=None,):
QDialog.__init__(self, parent)
self.parent = parent
self.setWindowTitle(u"{0} {1}: Add new Kindle for Android Key".format(PLUGIN_NAME, PLUGIN_VERSION))
layout = QVBoxLayout(self)
self.setLayout(layout)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
data_group_box = QGroupBox(u"", self)
layout.addWidget(data_group_box)
data_group_box_layout = QVBoxLayout()
data_group_box.setLayout(data_group_box_layout)
file_group = QHBoxLayout()
data_group_box_layout.addLayout(file_group)
add_btn = QPushButton(u"Choose Backup File", self)
add_btn.setToolTip(u"Import Kindle for Android backup file.")
add_btn.clicked.connect(self.get_android_file)
file_group.addWidget(add_btn)
self.selected_file_name = QLabel(u"",self)
self.selected_file_name.setAlignment(Qt.AlignHCenter)
file_group.addWidget(self.selected_file_name)
key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel(u"Unique Key Name:", self))
self.key_ledit = QLineEdit(u"", self)
self.key_ledit.setToolTip(u"<p>Enter an identifying name for the Android for Kindle key.")
key_group.addWidget(self.key_ledit)
#key_label = QLabel(_(''), self)
#key_label.setAlignment(Qt.AlignHCenter)
#data_group_box_layout.addWidget(key_label)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
layout.addWidget(self.button_box)
self.resize(self.sizeHint())
@property
def key_name(self):
return unicode(self.key_ledit.text()).strip()
@property
def file_name(self):
return unicode(self.selected_file_name.text()).strip()
@property
def key_value(self):
return self.serials_from_file
def get_android_file(self):
unique_dlg_name = PLUGIN_NAME + u"Import Kindle for Android backup file" #takes care of automatically remembering last directory
caption = u"Select Kindle for Android backup file to add"
filters = [(u"Kindle for Android backup files", ['db','ab','xml'])]
files = choose_files(self, unique_dlg_name, caption, filters, all_files=False)
self.serials_from_file = []
file_name = u""
if files:
# find the first selected file that yields some serial numbers
for filename in files:
fpath = os.path.join(config_dir, filename)
self.filename = os.path.basename(filename)
file_serials = androidkindlekey.get_serials(fpath)
if len(file_serials)>0:
file_name = os.path.basename(self.filename)
self.serials_from_file.extend(file_serials)
self.selected_file_name.setText(file_name)
def accept(self):
if len(self.file_name) == 0 or len(self.key_value) == 0:
errmsg = u"Please choose a Kindle for Android backup file."
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) == 0 or self.key_name.isspace():
errmsg = u"Please enter a key name."
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) < 4:
errmsg = u"Key name must be at <i>least</i> 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
QDialog.accept(self)
class AddPIDDialog(QDialog):
def __init__(self, parent=None,):
QDialog.__init__(self, parent)
@@ -876,9 +969,6 @@ class AddPIDDialog(QDialog):
self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(u"Enter a Mobipocket PID. Mobipocket PIDs are 8 or 10 characters long. Mobipocket PIDs are case-sensitive, so be sure to enter the upper and lower case letters unchanged.")
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
@@ -889,11 +979,11 @@ class AddPIDDialog(QDialog):
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
@property
def key_value(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
def accept(self):
if len(self.key_name) == 0 or self.key_name.isspace():

View File

@@ -1,719 +0,0 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement
__license__ = 'GPL v3'
# Standard Python modules.
import os, sys, re, hashlib
import json
from PyQt4.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QListWidget, QListWidgetItem, QAbstractItemView, QLineEdit, QPushButton, QIcon, QGroupBox, QDialog, QDialogButtonBox, QUrl, QString)
from PyQt4 import QtGui
# calibre modules and constants.
from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url,
choose_dir, choose_files)
from calibre.utils.config import dynamic, config_dir, JSONConfig
from calibre_plugins.dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION
from calibre_plugins.dedrm.utilities import (uStrCmp, DETAILED_MESSAGE, parseCustString)
from calibre_plugins.dedrm.ignoblekeygen import generate_key as generate_bandn_key
from calibre_plugins.dedrm.erdr2pml import getuser_key as generate_ereader_key
from calibre_plugins.dedrm.adobekey import adeptkeys as retrieve_adept_keys
from calibre_plugins.dedrm.kindlekey import kindlekeys as retrieve_kindle_keys
class ManageKeysDialog(QDialog):
def __init__(self, parent, key_type_name, plugin_keys, create_key, keyfile_ext = u""):
QDialog.__init__(self,parent)
self.parent = parent
self.key_type_name = key_type_name
self.plugin_keys = plugin_keys
self.create_key = create_key
self.keyfile_ext = keyfile_ext
self.import_key = (keyfile_ext != u"")
self.binary_file = (key_type_name == u"Adobe Digital Editions Key")
self.json_file = (key_type_name == u"Kindle for Mac and PC Key")
self.setWindowTitle("{0} {1}: Manage {2}s".format(PLUGIN_NAME, PLUGIN_VERSION, self.key_type_name))
# Start Qt Gui dialog layout
layout = QVBoxLayout(self)
self.setLayout(layout)
help_layout = QHBoxLayout()
layout.addLayout(help_layout)
# Add hyperlink to a help file at the right. We will replace the correct name when it is clicked.
help_label = QLabel('<a href="http://www.foo.com/">Help</a>', self)
help_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
help_label.setAlignment(Qt.AlignRight)
help_label.linkActivated.connect(self.help_link_activated)
help_layout.addWidget(help_label)
keys_group_box = QGroupBox(_(u"{0}s".format(self.key_type_name)), self)
layout.addWidget(keys_group_box)
keys_group_box_layout = QHBoxLayout()
keys_group_box.setLayout(keys_group_box_layout)
self.listy = QListWidget(self)
self.listy.setToolTip(u"{0}s that will be used to decrypt ebooks".format(self.key_type_name))
self.listy.setSelectionMode(QAbstractItemView.SingleSelection)
self.populate_list()
keys_group_box_layout.addWidget(self.listy)
button_layout = QVBoxLayout()
keys_group_box_layout.addLayout(button_layout)
self._add_key_button = QtGui.QToolButton(self)
self._add_key_button.setToolTip(u"Create new {0}".format(self.key_type_name))
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(_(u"Delete highlighted key"))
self._delete_key_button.setIcon(QIcon(I('list_remove.png')))
self._delete_key_button.clicked.connect(self.delete_key)
button_layout.addWidget(self._delete_key_button)
if type(self.plugin_keys) == dict:
self._rename_key_button = QtGui.QToolButton(self)
self._rename_key_button.setToolTip(_(u"Rename highlighted key"))
self._rename_key_button.setIcon(QIcon(I('edit-select-all.png')))
self._rename_key_button.clicked.connect(self.rename_key)
button_layout.addWidget(self._rename_key_button)
self.export_key_button = QtGui.QToolButton(self)
self.export_key_button.setToolTip(u"Save highlighted key to a .{0} file".format(self.keyfile_ext))
self.export_key_button.setIcon(QIcon(I('save.png')))
self.export_key_button.clicked.connect(self.export_key)
button_layout.addWidget(self.export_key_button)
spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
button_layout.addItem(spacerItem)
layout.addSpacing(5)
migrate_layout = QHBoxLayout()
layout.addLayout(migrate_layout)
if self.import_key:
migrate_layout.setAlignment(Qt.AlignJustify)
self.migrate_btn = QPushButton(u"Import Existing Keyfiles", self)
self.migrate_btn.setToolTip(u"Import *.{0} files (created using other tools).".format(self.keyfile_ext))
self.migrate_btn.clicked.connect(self.migrate_wrapper)
migrate_layout.addWidget(self.migrate_btn)
migrate_layout.addStretch()
self.button_box = QDialogButtonBox(QDialogButtonBox.Close)
self.button_box.rejected.connect(self.close)
migrate_layout.addWidget(self.button_box)
self.resize(self.sizeHint())
def populate_list(self):
if type(self.plugin_keys) == dict:
for key in self.plugin_keys.keys():
self.listy.addItem(QListWidgetItem(key))
else:
for key in self.plugin_keys:
self.listy.addItem(QListWidgetItem(key))
def add_key(self):
d = self.create_key(self)
d.exec_()
if d.result() != d.Accepted:
# New key generation cancelled.
return
new_key_value = d.key_value
if type(self.plugin_keys) == dict:
if new_key_value in self.plugin_keys.values():
old_key_name = [name for name, value in self.plugin_keys.iteritems() if value == new_key_value][0]
info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
u"The new {1} is the same as the existing {1} named <strong>{0}</strong> and has not been added.".format(old_key_name,self.key_type_name), show=True)
return
self.plugin_keys[d.key_name] = new_key_value
else:
if new_key_value in self.plugin_keys:
info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
u"This {0} is already in the list of {0}s has not been added.".format(self.key_type_name), show=True)
return
self.plugin_keys.append(d.key_value)
self.listy.clear()
self.populate_list()
def rename_key(self):
if not self.listy.currentItem():
errmsg = u"No {0} selected to rename. Highlight a keyfile first.".format(self.key_type_name)
r = error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
_(errmsg), show=True, show_copy_button=False)
return
d = RenameKeyDialog(self)
d.exec_()
if d.result() != d.Accepted:
# rename cancelled or moot.
return
keyname = unicode(self.listy.currentItem().text().toUtf8(),'utf8')
if not question_dialog(self, "{0} {1}: Confirm Rename".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to rename the {2} named <strong>{0}</strong> to <strong>{1}</strong>?".format(keyname,d.key_name,self.key_type_name), show_copy_button=False, default_yes=False):
return
self.plugin_keys[d.key_name] = self.plugin_keys[keyname]
del self.plugin_keys[keyname]
self.listy.clear()
self.populate_list()
def delete_key(self):
if not self.listy.currentItem():
return
keyname = unicode(self.listy.currentItem().text().toUtf8(), 'utf8')
if not question_dialog(self, "{0} {1}: Confirm Delete".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to delete the {1} <strong>{0}</strong>?".format(keyname, self.key_type_name), show_copy_button=False, default_yes=False):
return
if type(self.plugin_keys) == dict:
del self.plugin_keys[keyname]
else:
self.plugin_keys.remove(keyname)
self.listy.clear()
self.populate_list()
def help_link_activated(self, url):
def get_help_file_resource():
# Copy the HTML helpfile to the plugin directory each time the
# link is clicked in case the helpfile is updated in newer plugins.
help_file_name = u"{0}_{1}_Help.htm".format(PLUGIN_NAME, self.key_type_name)
file_path = os.path.join(config_dir, u"plugins", u"DeDRM", u"help", help_file_name)
with open(file_path,'w') as f:
f.write(self.parent.load_resource(help_file_name))
return file_path
url = 'file:///' + get_help_file_resource()
open_url(QUrl(url))
def migrate_files(self):
dynamic[PLUGIN_NAME + u"config_dir"] = config_dir
files = choose_files(self, PLUGIN_NAME + u"config_dir",
u"Select {0} files to import".format(self.key_type_name), [(u"{0} files".format(self.key_type_name), [self.keyfile_ext])], False)
counter = 0
skipped = 0
if files:
for filename in files:
fpath = os.path.join(config_dir, filename)
filename = os.path.basename(filename)
new_key_name = os.path.splitext(os.path.basename(filename))[0]
with open(fpath,'rb') as keyfile:
new_key_value = keyfile.read()
if self.binary_file:
new_key_value = new_key_value.encode('hex')
elif self.json_file:
new_key_value = json.loads(new_key_value)
match = False
for key in self.plugin_keys.keys():
if uStrCmp(new_key_name, key, True):
skipped += 1
msg = u"A key with the name <strong>{0}</strong> already exists!\nSkipping key file <strong>{1}</strong>.\nRename the existing key and import again".format(new_key_name,filename)
inf = info_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
_(msg), show_copy_button=False, show=True)
match = True
break
if not match:
if new_key_value in self.plugin_keys.values():
old_key_name = [name for name, value in self.plugin_keys.iteritems() if value == new_key_value][0]
skipped += 1
info_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
u"The key in file {0} is the same as the existing key <strong>{1}</strong> and has been skipped.".format(filename,old_key_name), show_copy_button=False, show=True)
else:
counter += 1
self.plugin_keys[new_key_name] = new_key_value
msg = u""
if counter+skipped > 1:
if counter > 0:
msg += u"Imported <strong>{0:d}</strong> key {1}. ".format(counter, u"file" if counter == 1 else u"files")
if skipped > 0:
msg += u"Skipped <strong>{0:d}</strong> key {1}.".format(skipped, u"file" if counter == 1 else u"files")
inf = info_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
_(msg), show_copy_button=False, show=True)
return counter > 0
def migrate_wrapper(self):
if self.migrate_files():
self.listy.clear()
self.populate_list()
def export_key(self):
if not self.listy.currentItem():
errmsg = u"No keyfile selected to export. Highlight a keyfile first."
r = error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
_(errmsg), show=True, show_copy_button=False)
return
filter = QString(u"{0} Files (*.{1})".format(self.key_type_name, self.keyfile_ext))
keyname = unicode(self.listy.currentItem().text().toUtf8(), 'utf8')
if dynamic.get(PLUGIN_NAME + 'save_dir'):
defaultname = os.path.join(dynamic.get(PLUGIN_NAME + 'save_dir'), u"{0}.{1}".format(keyname , self.keyfile_ext))
else:
defaultname = os.path.join(os.path.expanduser('~'), u"{0}.{1}".format(keyname , self.keyfile_ext))
filename = unicode(QtGui.QFileDialog.getSaveFileName(self, u"Save {0} File as...".format(self.key_type_name), defaultname,
u"{0} Files (*.{1})".format(self.key_type_name,self.keyfile_ext), filter))
if filename:
dynamic[PLUGIN_NAME + 'save_dir'] = os.path.split(filename)[0]
with file(filename, 'w') as fname:
if self.binary_file:
fname.write(self.plugin_keys[keyname].decode('hex'))
elif self.json_file:
fname.write(json.dumps(self.plugin_keys[keyname]))
else:
fname.write(self.plugin_keys[keyname])
class RenameKeyDialog(QDialog):
def __init__(self, parent=None,):
print repr(self), repr(parent)
QDialog.__init__(self, parent)
self.parent = parent
self.setWindowTitle("{0} {1}: Rename {0}".format(PLUGIN_NAME, PLUGIN_VERSION, parent.key_type_name))
layout = QVBoxLayout(self)
self.setLayout(layout)
data_group_box = QGroupBox('', self)
layout.addWidget(data_group_box)
data_group_box_layout = QVBoxLayout()
data_group_box.setLayout(data_group_box_layout)
data_group_box_layout.addWidget(QLabel('New Key Name:', self))
self.key_ledit = QLineEdit(self.parent.listy.currentItem().text(), self)
self.key_ledit.setToolTip(u"Enter a new name for this existing {0}.".format(parent.key_type_name))
data_group_box_layout.addWidget(self.key_ledit)
layout.addSpacing(20)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
layout.addWidget(self.button_box)
self.resize(self.sizeHint())
def accept(self):
if self.key_ledit.text().isEmpty() or unicode(self.key_ledit.text()).isspace():
errmsg = u"Key name field cannot be empty!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
_(errmsg), show=True, show_copy_button=False)
if len(self.key_ledit.text()) < 4:
errmsg = u"Key name must be at <i>least</i> 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
_(errmsg), show=True, show_copy_button=False)
if uStrCmp(self.key_ledit.text(), self.parent.listy.currentItem().text()):
# Same exact name ... do nothing.
return QDialog.reject(self)
for k in self.parent.plugin_keys.keys():
if (uStrCmp(self.key_ledit.text(), k, True) and
not uStrCmp(k, self.parent.listy.currentItem().text(), True)):
errmsg = u"The key name <strong>{0}</strong> is already being used.".format(self.key_ledit.text())
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
_(errmsg), show=True, show_copy_button=False)
QDialog.accept(self)
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
class AddBandNKeyDialog(QDialog):
def __init__(self, parent=None,):
QDialog.__init__(self, parent)
self.parent = parent
self.setWindowTitle(u"{0} {1}: Create New Barnes & Noble Key".format(PLUGIN_NAME, PLUGIN_VERSION))
layout = QVBoxLayout(self)
self.setLayout(layout)
data_group_box = QGroupBox(u"", self)
layout.addWidget(data_group_box)
data_group_box_layout = QVBoxLayout()
data_group_box.setLayout(data_group_box_layout)
key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel(u"Unique Key Name:", self))
self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(_(u"<p>Enter an identifying name for this new key.</p>" +
u"<p>It should be something that will help you remember " +
u"what personal information was used to create it."))
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
name_group = QHBoxLayout()
data_group_box_layout.addLayout(name_group)
name_group.addWidget(QLabel(u"Your Name:", self))
self.name_ledit = QLineEdit(u"", self)
self.name_ledit.setToolTip(_(u"<p>Enter your name as it appears in your B&N " +
u"account or on your credit card.</p>" +
u"<p>It will only be used to generate this " +
u"one-time key and won\'t be stored anywhere " +
u"in calibre or on your computer.</p>" +
u"<p>(ex: Jonathan Smith)"))
name_group.addWidget(self.name_ledit)
name_disclaimer_label = QLabel(_(u"(Will not be saved in configuration data)"), self)
name_disclaimer_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(name_disclaimer_label)
ccn_group = QHBoxLayout()
data_group_box_layout.addLayout(ccn_group)
ccn_group.addWidget(QLabel(u"Credit Card#:", self))
self.cc_ledit = QLineEdit(u"", self)
self.cc_ledit.setToolTip(_(u"<p>Enter the full credit card number on record " +
u"in your B&N account.</p>" +
u"<p>No spaces or dashes... just the numbers. " +
u"This number will only be used to generate this " +
u"one-time key and won\'t be stored anywhere in " +
u"calibre or on your computer."))
ccn_group.addWidget(self.cc_ledit)
ccn_disclaimer_label = QLabel(_('(Will not be saved in configuration data)'), self)
ccn_disclaimer_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(ccn_disclaimer_label)
layout.addSpacing(10)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
layout.addWidget(self.button_box)
self.resize(self.sizeHint())
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
@property
def key_value(self):
return generate_bandn_key(self.user_name,self.cc_number)
@property
def user_name(self):
return unicode(self.name_ledit.text().toUtf8(), 'utf8').strip().lower().replace(' ','')
@property
def cc_number(self):
return unicode(self.cc_ledit.text().toUtf8(), 'utf8').strip().replace(' ', '').replace('-','')
def accept(self):
if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace():
errmsg = u"All fields are required!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if not self.cc_number.isdigit():
errmsg = u"Numbers only in the credit card number field!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) < 4:
errmsg = u"Key name must be at <i>least</i> 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
QDialog.accept(self)
class AddEReaderDialog(QDialog):
def __init__(self, parent=None,):
QDialog.__init__(self, parent)
self.parent = parent
self.setWindowTitle(u"{0} {1}: Create New eReader Key".format(PLUGIN_NAME, PLUGIN_VERSION))
layout = QVBoxLayout(self)
self.setLayout(layout)
data_group_box = QGroupBox(u"", self)
layout.addWidget(data_group_box)
data_group_box_layout = QVBoxLayout()
data_group_box.setLayout(data_group_box_layout)
key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel(u"Unique Key Name:", self))
self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(u"<p>Enter an identifying name for this new key.\nIt should be something that will help you remember what personal information was used to create it.")
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
name_group = QHBoxLayout()
data_group_box_layout.addLayout(name_group)
name_group.addWidget(QLabel(u"Your Name:", self))
self.name_ledit = QLineEdit(u"", self)
self.name_ledit.setToolTip(u"Enter the name for this eReader key, usually the name on your credit card.\nIt will only be used to generate this one-time key and won\'t be stored anywhere in calibre or on your computer.\n(ex: Mr Jonathan Q Smith)")
name_group.addWidget(self.name_ledit)
name_disclaimer_label = QLabel(_(u"(Will not be saved in configuration data)"), self)
name_disclaimer_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(name_disclaimer_label)
ccn_group = QHBoxLayout()
data_group_box_layout.addLayout(ccn_group)
ccn_group.addWidget(QLabel(u"Credit Card#:", self))
self.cc_ledit = QLineEdit(u"", self)
self.cc_ledit.setToolTip(u"<p>Enter the last 8 digits of credit card number for this eReader key.\nThey will only be used to generate this one-time key and won\'t be stored anywhere in calibre or on your computer.")
ccn_group.addWidget(self.cc_ledit)
ccn_disclaimer_label = QLabel(_('(Will not be saved in configuration data)'), self)
ccn_disclaimer_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(ccn_disclaimer_label)
layout.addSpacing(10)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
layout.addWidget(self.button_box)
self.resize(self.sizeHint())
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
@property
def key_value(self):
return generate_ereader_key(self.user_name,self.cc_number).encode('hex')
@property
def user_name(self):
return unicode(self.name_ledit.text().toUtf8(), 'utf8').strip().lower().replace(' ','')
@property
def cc_number(self):
return unicode(self.cc_ledit.text().toUtf8(), 'utf8').strip().replace(' ', '').replace('-','')
def accept(self):
if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace():
errmsg = u"All fields are required!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if not self.cc_number.isdigit():
errmsg = u"Numbers only in the credit card number field!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) < 4:
errmsg = u"Key name must be at <i>least</i> 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
QDialog.accept(self)
class AddAdeptDialog(QDialog):
def __init__(self, parent=None,):
QDialog.__init__(self, parent)
self.parent = parent
self.setWindowTitle(u"{0} {1}: Getting Default Adobe Digital Editions Key".format(PLUGIN_NAME, PLUGIN_VERSION))
layout = QVBoxLayout(self)
self.setLayout(layout)
try:
self.default_key = retrieve_adept_keys()[0]
except:
self.default_key = u""
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
if len(self.default_key)>0:
data_group_box = QGroupBox(u"", self)
layout.addWidget(data_group_box)
data_group_box_layout = QVBoxLayout()
data_group_box.setLayout(data_group_box_layout)
key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel(u"Unique Key Name:", self))
self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(u"<p>Enter an identifying name for the current default Adobe Digital Editions key.")
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
self.button_box.accepted.connect(self.accept)
else:
default_key_error = QLabel(u"The default encryption key for Adobe Digital Editions could not be found.", self)
default_key_error.setAlignment(Qt.AlignHCenter)
layout.addWidget(default_key_error)
# if no default, bot buttons do the same
self.button_box.accepted.connect(self.reject)
self.button_box.rejected.connect(self.reject)
layout.addWidget(self.button_box)
self.resize(self.sizeHint())
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
@property
def key_value(self):
return self.default_key.encode('hex')
def accept(self):
if len(self.key_name) == 0 or self.key_name.isspace():
errmsg = u"All fields are required!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) < 4:
errmsg = u"Key name must be at <i>least</i> 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
QDialog.accept(self)
class AddKindleDialog(QDialog):
def __init__(self, parent=None,):
QDialog.__init__(self, parent)
self.parent = parent
self.setWindowTitle(u"{0} {1}: Getting Default Kindle for Mac/PC Key".format(PLUGIN_NAME, PLUGIN_VERSION))
layout = QVBoxLayout(self)
self.setLayout(layout)
try:
self.default_key = retrieve_kindle_keys()[0]
except:
self.default_key = u""
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
if len(self.default_key)>0:
data_group_box = QGroupBox(u"", self)
layout.addWidget(data_group_box)
data_group_box_layout = QVBoxLayout()
data_group_box.setLayout(data_group_box_layout)
key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel(u"Unique Key Name:", self))
self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(u"<p>Enter an identifying name for the current default Kindle for Mac/PC key.")
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
self.button_box.accepted.connect(self.accept)
else:
default_key_error = QLabel(u"The default encryption key for Kindle for Mac/PC could not be found.", self)
default_key_error.setAlignment(Qt.AlignHCenter)
layout.addWidget(default_key_error)
# if no default, bot buttons do the same
self.button_box.accepted.connect(self.reject)
self.button_box.rejected.connect(self.reject)
layout.addWidget(self.button_box)
self.resize(self.sizeHint())
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
@property
def key_value(self):
return self.default_key
def accept(self):
if len(self.key_name) == 0 or self.key_name.isspace():
errmsg = u"All fields are required!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) < 4:
errmsg = u"Key name must be at <i>least</i> 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
QDialog.accept(self)
class AddSerialDialog(QDialog):
def __init__(self, parent=None,):
QDialog.__init__(self, parent)
self.parent = parent
self.setWindowTitle(u"{0} {1}: Add New EInk Kindle Serial Number".format(PLUGIN_NAME, PLUGIN_VERSION))
layout = QVBoxLayout(self)
self.setLayout(layout)
data_group_box = QGroupBox(u"", self)
layout.addWidget(data_group_box)
data_group_box_layout = QVBoxLayout()
data_group_box.setLayout(data_group_box_layout)
key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel(u"EInk Kindle Serial Number:", self))
self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(u"Enter an eInk Kindle serial number. EInk Kindle serial numbers are 16 characters long and usually start with a 'B' or a '9'. Kindle Serial Numbers are case-sensitive, so be sure to enter the upper and lower case letters unchanged.")
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
layout.addWidget(self.button_box)
self.resize(self.sizeHint())
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
@property
def key_value(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
def accept(self):
if len(self.key_name) == 0 or self.key_name.isspace():
errmsg = u"Please enter an eInk Kindle Serial Number or click Cancel in the dialog."
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) != 16:
errmsg = u"EInk Kindle Serial Numbers must be 16 characters long. This is {0:d} characters long.".format(len(self.key_name))
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
QDialog.accept(self)
class AddPIDDialog(QDialog):
def __init__(self, parent=None,):
QDialog.__init__(self, parent)
self.parent = parent
self.setWindowTitle(u"{0} {1}: Add New Mobipocket PID".format(PLUGIN_NAME, PLUGIN_VERSION))
layout = QVBoxLayout(self)
self.setLayout(layout)
data_group_box = QGroupBox(u"", self)
layout.addWidget(data_group_box)
data_group_box_layout = QVBoxLayout()
data_group_box.setLayout(data_group_box_layout)
key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel(u"PID:", self))
self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(u"Enter a Mobipocket PID. Mobipocket PIDs are 8 or 10 characters long. Mobipocket PIDs are case-sensitive, so be sure to enter the upper and lower case letters unchanged.")
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
layout.addWidget(self.button_box)
self.resize(self.sizeHint())
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
@property
def key_value(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
def accept(self):
if len(self.key_name) == 0 or self.key_name.isspace():
errmsg = u"Please enter a Mobipocket PID or click Cancel in the dialog."
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) != 8 and len(self.key_name) != 10:
errmsg = u"Mobipocket PIDs must be 8 or 10 characters long. This is {0:d} characters long.".format(len(self.key_name))
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
QDialog.accept(self)

View File

@@ -458,7 +458,11 @@ class DocParser(object):
(wtype, num) = pdesc[j]
if wtype == 'ocr' :
word = self.ocrtext[num]
try:
word = self.ocrtext[num]
except:
word = ""
sep = ' '
if handle_links:

View File

@@ -0,0 +1,336 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import with_statement
# ignoblekey.py
# Copyright © 2015 Apprentice Alf and Apprentice Harper
# Based on kindlekey.py, Copyright © 2010-2013 by some_updates and Apprentice Alf
# Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/>
# Revision history:
# 1.0 - Initial release
# 1.1 - remove duplicates and return last key as single key
"""
Get Barnes & Noble EPUB user key from nook Studio log file
"""
__license__ = 'GPL v3'
__version__ = "1.1"
import sys
import os
import hashlib
import getopt
import re
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
# encoded using "replace" before writing them.
class SafeUnbuffered:
def __init__(self, stream):
self.stream = stream
self.encoding = stream.encoding
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
try:
from calibre.constants import iswindows, isosx
except:
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
def unicode_argv():
if iswindows:
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
# as a list of Unicode strings and encode them as utf-8
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"ignoblekey.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
class DrmException(Exception):
pass
# Locate all of the nookStudy/nook for PC/Mac log file and return as list
def getNookLogFiles():
logFiles = []
found = False
if iswindows:
import _winreg as winreg
# some 64 bit machines do not have the proper registry key for some reason
# or the python interface to the 32 vs 64 bit registry is broken
paths = set()
if 'LOCALAPPDATA' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
if os.path.isdir(path):
paths.add(path)
if 'USERPROFILE' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
path = winreg.ExpandEnvironmentStrings(u"%USERPROFILE%")+u"\\AppData\\Local"
if os.path.isdir(path):
paths.add(path)
path = winreg.ExpandEnvironmentStrings(u"%USERPROFILE%")+u"\\AppData\\Roaming"
if os.path.isdir(path):
paths.add(path)
# User Shell Folders show take precedent over Shell Folders if present
try:
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\")
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
if os.path.isdir(path):
paths.add(path)
except WindowsError:
pass
try:
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\")
path = winreg.QueryValueEx(regkey, 'AppData')[0]
if os.path.isdir(path):
paths.add(path)
except WindowsError:
pass
try:
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
if os.path.isdir(path):
paths.add(path)
except WindowsError:
pass
try:
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
path = winreg.QueryValueEx(regkey, 'AppData')[0]
if os.path.isdir(path):
paths.add(path)
except WindowsError:
pass
for path in paths:
# look for nookStudy log file
logpath = path +'\\Barnes & Noble\\NOOKstudy\\logs\\BNClientLog.txt'
if os.path.isfile(logpath):
found = True
print('Found nookStudy log file: ' + logpath.encode('ascii','ignore'))
logFiles.append(logpath)
else:
home = os.getenv('HOME')
# check for BNClientLog.txt in various locations
testpath = home + '/Library/Application Support/Barnes & Noble/DesktopReader/logs/BNClientLog.txt'
if os.path.isfile(testpath):
logFiles.append(testpath)
print('Found nookStudy log file: ' + testpath)
found = True
testpath = home + '/Library/Application Support/Barnes & Noble/DesktopReader/indices/BNClientLog.txt'
if os.path.isfile(testpath):
logFiles.append(testpath)
print('Found nookStudy log file: ' + testpath)
found = True
testpath = home + '/Library/Application Support/Barnes & Noble/BNDesktopReader/logs/BNClientLog.txt'
if os.path.isfile(testpath):
logFiles.append(testpath)
print('Found nookStudy log file: ' + testpath)
found = True
testpath = home + '/Library/Application Support/Barnes & Noble/BNDesktopReader/indices/BNClientLog.txt'
if os.path.isfile(testpath):
logFiles.append(testpath)
print('Found nookStudy log file: ' + testpath)
found = True
if not found:
print('No nook Study log files have been found.')
return logFiles
# Extract CCHash key(s) from log file
def getKeysFromLog(kLogFile):
keys = []
regex = re.compile("ccHash: \"(.{28})\"");
for line in open(kLogFile):
for m in regex.findall(line):
keys.append(m)
return keys
# interface for calibre plugin
def nookkeys(files = []):
keys = []
if files == []:
files = getNookLogFiles()
for file in files:
fileKeys = getKeysFromLog(file)
if fileKeys:
print u"Found {0} keys in the Nook Study log files".format(len(fileKeys))
keys.extend(fileKeys)
return list(set(keys))
# interface for Python DeDRM
# returns single key or multiple keys, depending on path or file passed in
def getkey(outpath, files=[]):
keys = nookkeys(files)
if len(keys) > 0:
if not os.path.isdir(outpath):
outfile = outpath
with file(outfile, 'w') as keyfileout:
keyfileout.write(keys[-1])
print u"Saved a key to {0}".format(outfile)
else:
keycount = 0
for key in keys:
while True:
keycount += 1
outfile = os.path.join(outpath,u"nookkey{0:d}.b64".format(keycount))
if not os.path.exists(outfile):
break
with file(outfile, 'w') as keyfileout:
keyfileout.write(key)
print u"Saved a key to {0}".format(outfile)
return True
return False
def usage(progname):
print u"Finds the nook Study encryption keys."
print u"Keys are saved to the current directory, or a specified output directory."
print u"If a file name is passed instead of a directory, only the first key is saved, in that file."
print u"Usage:"
print u" {0:s} [-h] [-k <logFile>] [<outpath>]".format(progname)
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv()
progname = os.path.basename(argv[0])
print u"{0} v{1}\nCopyright © 2015 Apprentice Alf".format(progname,__version__)
try:
opts, args = getopt.getopt(argv[1:], "hk:")
except getopt.GetoptError, err:
print u"Error in options or arguments: {0}".format(err.args[0])
usage(progname)
sys.exit(2)
files = []
for o, a in opts:
if o == "-h":
usage(progname)
sys.exit(0)
if o == "-k":
files = [a]
if len(args) > 1:
usage(progname)
sys.exit(2)
if len(args) == 1:
# save to the specified file or directory
outpath = args[0]
if not os.path.isabs(outpath):
outpath = os.path.abspath(outpath)
else:
# save to the same directory as the script
outpath = os.path.dirname(argv[0])
# make sure the outpath is the
outpath = os.path.realpath(os.path.normpath(outpath))
if not getkey(outpath, files):
print u"Could not retrieve nook Study key."
return 0
def gui_main():
try:
import Tkinter
import Tkconstants
import tkMessageBox
import traceback
except:
return cli_main()
class ExceptionDialog(Tkinter.Frame):
def __init__(self, root, text):
Tkinter.Frame.__init__(self, root, border=5)
label = Tkinter.Label(self, text=u"Unexpected error:",
anchor=Tkconstants.W, justify=Tkconstants.LEFT)
label.pack(fill=Tkconstants.X, expand=0)
self.text = Tkinter.Text(self)
self.text.pack(fill=Tkconstants.BOTH, expand=1)
self.text.insert(Tkconstants.END, text)
argv=unicode_argv()
root = Tkinter.Tk()
root.withdraw()
progpath, progname = os.path.split(argv[0])
success = False
try:
keys = nookkeys()
keycount = 0
for key in keys:
print key
while True:
keycount += 1
outfile = os.path.join(progpath,u"nookkey{0:d}.b64".format(keycount))
if not os.path.exists(outfile):
break
with file(outfile, 'w') as keyfileout:
keyfileout.write(key)
success = True
tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile))
except DrmException, e:
tkMessageBox.showerror(progname, u"Error: {0}".format(str(e)))
except Exception:
root.wm_state('normal')
root.title(progname)
text = traceback.format_exc()
ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1)
root.mainloop()
if not success:
return 1
return 0
if __name__ == '__main__':
if len(sys.argv) > 1:
sys.exit(cli_main())
sys.exit(gui_main())

View File

@@ -0,0 +1,258 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import with_statement
# ignoblekeyfetch.pyw, version 1.1
# Copyright © 2015 Apprentice Harper
# Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/>
# Based on discoveries by "Nobody You Know"
# Code partly based on ignoblekeygen.py by several people.
# Windows users: Before running this program, you must first install Python.
# We recommend ActiveState Python 2.7.X for Windows from
# http://www.activestate.com/activepython/downloads.
# Then save this script file as ignoblekeyfetch.pyw and double-click on it to run it.
#
# Mac OS X users: Save this script file as ignoblekeyfetch.pyw. You can run this
# program from the command line (python ignoblekeyfetch.pyw) or by double-clicking
# it when it has been associated with PythonLauncher.
# Revision history:
# 1.0 - Initial version
# 1.1 - Try second URL if first one fails
"""
Fetch Barnes & Noble EPUB user key from B&N servers using email and password
"""
__license__ = 'GPL v3'
__version__ = "1.1"
import sys
import os
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
# encoded using "replace" before writing them.
class SafeUnbuffered:
def __init__(self, stream):
self.stream = stream
self.encoding = stream.encoding
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
try:
from calibre.constants import iswindows, isosx
except:
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
def unicode_argv():
if iswindows:
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
# as a list of Unicode strings and encode them as utf-8
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"ignoblekeyfetch.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
class IGNOBLEError(Exception):
pass
def fetch_key(email, password):
# change email and password to utf-8 if unicode
if type(email)==unicode:
email = email.encode('utf-8')
if type(password)==unicode:
password = password.encode('utf-8')
import random
random = "%030x" % random.randrange(16**30)
import urllib, urllib2, re
# try the URL from nook for PC
fetch_url = "https://cart4.barnesandnoble.com/services/service.aspx?Version=2&acctPassword="
fetch_url += urllib.quote(password,'')+"&devID=PC_BN_2.5.6.9575_"+random+"&emailAddress="
fetch_url += urllib.quote(email,"")+"&outFormat=5&schema=1&service=1&stage=deviceHashB"
#print fetch_url
found = ''
try:
req = urllib2.Request(fetch_url)
response = urllib2.urlopen(req)
the_page = response.read()
#print the_page
found = re.search('ccHash>(.+?)</ccHash', the_page).group(1)
except:
found = ''
if len(found)!=28:
# try the URL from android devices
fetch_url = "https://cart4.barnesandnoble.com/services/service.aspx?Version=2&acctPassword="
fetch_url += urllib.quote(password,'')+"&devID=hobbes_9.3.50818_"+random+"&emailAddress="
fetch_url += urllib.quote(email,"")+"&outFormat=5&schema=1&service=1&stage=deviceHashB"
#print fetch_url
found = ''
try:
req = urllib2.Request(fetch_url)
response = urllib2.urlopen(req)
the_page = response.read()
#print the_page
found = re.search('ccHash>(.+?)</ccHash', the_page).group(1)
except:
found = ''
return found
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv()
progname = os.path.basename(argv[0])
if len(argv) != 4:
print u"usage: {0} <email> <password> <keyfileout.b64>".format(progname)
return 1
email, password, keypath = argv[1:]
userkey = fetch_key(email, password)
if len(userkey) == 28:
open(keypath,'wb').write(userkey)
return 0
print u"Failed to fetch key."
return 1
def gui_main():
try:
import Tkinter
import tkFileDialog
import Tkconstants
import tkMessageBox
import traceback
except:
return cli_main()
class DecryptionDialog(Tkinter.Frame):
def __init__(self, root):
Tkinter.Frame.__init__(self, root, border=5)
self.status = Tkinter.Label(self, text=u"Enter parameters")
self.status.pack(fill=Tkconstants.X, expand=1)
body = Tkinter.Frame(self)
body.pack(fill=Tkconstants.X, expand=1)
sticky = Tkconstants.E + Tkconstants.W
body.grid_columnconfigure(1, weight=2)
Tkinter.Label(body, text=u"Account email address").grid(row=0)
self.name = Tkinter.Entry(body, width=40)
self.name.grid(row=0, column=1, sticky=sticky)
Tkinter.Label(body, text=u"Account password").grid(row=1)
self.ccn = Tkinter.Entry(body, width=40)
self.ccn.grid(row=1, column=1, sticky=sticky)
Tkinter.Label(body, text=u"Output file").grid(row=2)
self.keypath = Tkinter.Entry(body, width=40)
self.keypath.grid(row=2, column=1, sticky=sticky)
self.keypath.insert(2, u"bnepubkey.b64")
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
button.grid(row=2, column=2)
buttons = Tkinter.Frame(self)
buttons.pack()
botton = Tkinter.Button(
buttons, text=u"Fetch", width=10, command=self.generate)
botton.pack(side=Tkconstants.LEFT)
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
button = Tkinter.Button(
buttons, text=u"Quit", width=10, command=self.quit)
button.pack(side=Tkconstants.RIGHT)
def get_keypath(self):
keypath = tkFileDialog.asksaveasfilename(
parent=None, title=u"Select B&N ePub key file to produce",
defaultextension=u".b64",
filetypes=[('base64-encoded files', '.b64'),
('All Files', '.*')])
if keypath:
keypath = os.path.normpath(keypath)
self.keypath.delete(0, Tkconstants.END)
self.keypath.insert(0, keypath)
return
def generate(self):
email = self.name.get()
password = self.ccn.get()
keypath = self.keypath.get()
if not email:
self.status['text'] = u"Email address not given"
return
if not password:
self.status['text'] = u"Account password not given"
return
if not keypath:
self.status['text'] = u"Output keyfile path not set"
return
self.status['text'] = u"Fetching..."
try:
userkey = fetch_key(email, password)
except Exception, e:
self.status['text'] = u"Error: {0}".format(e.args[0])
return
if len(userkey) == 28:
open(keypath,'wb').write(userkey)
self.status['text'] = u"Keyfile fetched successfully"
else:
self.status['text'] = u"Keyfile fetch failed."
root = Tkinter.Tk()
root.title(u"Barnes & Noble ePub Keyfile Fetch v.{0}".format(__version__))
root.resizable(True, False)
root.minsize(300, 0)
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
root.mainloop()
return 0
if __name__ == '__main__':
if len(sys.argv) > 1:
sys.exit(cli_main())
sys.exit(gui_main())

View File

@@ -33,13 +33,14 @@ from __future__ import with_statement
# 2.5 - Additional improvement for unicode and plugin support
# 2.6 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 2.7 - Work if TkInter is missing
# 2.8 - Fix bug in stand-alone use (import tkFileDialog)
"""
Generate Barnes & Noble EPUB user key from name and credit card number.
"""
__license__ = 'GPL v3'
__version__ = "2.7"
__version__ = "2.8"
import sys
import os
@@ -240,6 +241,7 @@ def gui_main():
import Tkinter
import Tkconstants
import tkMessageBox
import tkFileDialog
import traceback
except:
return cli_main()

View File

@@ -37,13 +37,14 @@ from __future__ import with_statement
# 5.9 - Fixed to retain zip file metadata (e.g. file modification date)
# 6.0 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 6.1 - Work if TkInter is missing
# 6.2 - Handle UTF-8 file names inside an ePub, fix by Jose Luis
"""
Decrypt Adobe Digital Editions encrypted ePub books.
"""
__license__ = 'GPL v3'
__version__ = "6.1"
__version__ = "6.2"
import sys
import os
@@ -366,7 +367,7 @@ class Decryptor(object):
return bytes
def decrypt(self, path, data):
if path in self._encrypted:
if path.encode('utf-8') in self._encrypted:
data = self._aes.decrypt(data)[16:]
data = data[:-ord(data[-1])]
data = self.decompress(data)

View File

@@ -3,15 +3,15 @@
from __future__ import with_statement
# ignobleepub.pyw, version 3.6
# Copyright © 2009-2012 by DiapDealer et al.
# k4mobidedrm.py, version 5.3
# Copyright © 2009-2015 by ApprenticeHarper et al.
# engine to remove drm from Kindle for Mac and Kindle for PC books
# engine to remove drm from Kindle and Mobipocket ebooks
# for personal use for archiving and converting your ebooks
# PLEASE DO NOT PIRATE EBOOKS!
# We want all authors and publishers, and eBook stores to live
# 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
@@ -55,8 +55,9 @@ from __future__ import with_statement
# - tweaked GetDecryptedBook interface to leave passed parameters unchanged
# 5.1 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 5.2 - Fixed error in command line processing of unicode arguments
# 5.3 - Changed Android support to allow passing of backup .ab files
__version__ = '5.2'
__version__ = '5.3'
import sys, os, re
@@ -80,10 +81,12 @@ if inCalibre:
from calibre_plugins.dedrm import mobidedrm
from calibre_plugins.dedrm import topazextract
from calibre_plugins.dedrm import kgenpids
from calibre_plugins.dedrm import androidkindlekey
else:
import mobidedrm
import topazextract
import kgenpids
import androidkindlekey
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
@@ -185,7 +188,7 @@ def unescape(text):
return text # leave as is
return re.sub(u"&#?\w+;", fixup, text)
def GetDecryptedBook(infile, kDatabases, serials, pids, starttime = time.time()):
def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime = time.time()):
# handle the obvious cases at the beginning
if not os.path.isfile(infile):
raise DrmException(u"Input file does not exist.")
@@ -205,9 +208,14 @@ def GetDecryptedBook(infile, kDatabases, serials, pids, starttime = time.time())
# copy list of pids
totalpids = list(pids)
# extend PID list with book-specific PIDs
# extend list of serials with serials from android databases
for aFile in androidFiles:
serials.extend(androidkindlekey.get_serials(aFile))
# extend PID list with book-specific PIDs from seriala and kDatabases
md1, md2 = mb.getPIDMetaInfo()
totalpids.extend(kgenpids.getPidList(md1, md2, serials, kDatabases))
# remove any duplicates
totalpid = list(set(totalpids))
print u"Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(totalpids))
try:
@@ -221,7 +229,7 @@ def GetDecryptedBook(infile, kDatabases, serials, pids, starttime = time.time())
# kDatabaseFiles is a list of files created by kindlekey
def decryptBook(infile, outdir, kDatabaseFiles, serials, pids):
def decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids):
starttime = time.time()
kDatabases = []
for dbfile in kDatabaseFiles:
@@ -237,7 +245,7 @@ def decryptBook(infile, outdir, kDatabaseFiles, serials, pids):
try:
book = GetDecryptedBook(infile, kDatabases, serials, pids, starttime)
book = GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime)
except Exception, e:
print u"Error decrypting book after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime)
traceback.print_exc()
@@ -252,7 +260,7 @@ def decryptBook(infile, outdir, kDatabaseFiles, serials, pids):
# avoid excessively long file names
if len(outfilename)>150:
outfilename = outfilename[:150]
outfilename = outfilename[:99]+"--"+outfilename[-49:]
outfilename = outfilename+u"_nodrm"
outfile = os.path.join(outdir, outfilename + book.getBookExtension())
@@ -273,7 +281,7 @@ def decryptBook(infile, outdir, kDatabaseFiles, serials, pids):
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.k4i>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] <infile> <outdir>".format(progname)
print u" {0} [-k <kindle.k4i>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] [ -a <AmazonSecureStorage.xml|backup.ab> ] <infile> <outdir>".format(progname)
#
# Main
@@ -284,7 +292,7 @@ def cli_main():
print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2013 The Dark Reverser et al.".format(__version__)
try:
opts, args = getopt.getopt(argv[1:], "k:p:s:")
opts, args = getopt.getopt(argv[1:], "k:p:s:a:")
except getopt.GetoptError, err:
print u"Error in options or arguments: {0}".format(err.args[0])
usage(progname)
@@ -296,6 +304,7 @@ def cli_main():
infile = args[0]
outdir = args[1]
kDatabaseFiles = []
androidFiles = []
serials = []
pids = []
@@ -312,11 +321,15 @@ def cli_main():
if a == None :
raise DrmException("Invalid parameter for -s")
serials = a.split(',')
if o == '-a':
if a == None:
raise DrmException("Invalid parameter for -a")
androidFiles.append(a)
# try with built in Kindle Info files if not on Linux
k4 = not sys.platform.startswith('linux')
return decryptBook(infile, outdir, kDatabaseFiles, serials, pids)
return decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids)
if __name__ == '__main__':

View File

@@ -2,6 +2,13 @@
# -*- coding: utf-8 -*-
from __future__ import with_statement
# kgenpids.py
# Copyright © 2010-2015 by some_updates, Apprentice Alf and Apprentice Harper
# Revision history:
# 2.0 - Fix for non-ascii Windows user names
import sys
import os, csv
import binascii
@@ -164,7 +171,7 @@ def getKindlePids(rec209, token, serialnum):
pids=[]
if isinstance(serialnum,unicode):
serialnum = serialnum.encode('ascii')
serialnum = serialnum.encode('utf-8')
# Compute book PID
pidHash = SHA1(serialnum+rec209+token)
@@ -190,16 +197,16 @@ def getK4Pids(rec209, token, kindleDatabase):
try:
# Get the Mazama Random number
MazamaRandomNumber = (kindleDatabase[1])['MazamaRandomNumber'].decode('hex').encode('ascii')
MazamaRandomNumber = (kindleDatabase[1])['MazamaRandomNumber'].decode('hex')
# Get the kindle account token
kindleAccountToken = (kindleDatabase[1])['kindle.account.tokens'].decode('hex').encode('ascii')
kindleAccountToken = (kindleDatabase[1])['kindle.account.tokens'].decode('hex')
# Get the IDString used to decode the Kindle Info file
IDString = (kindleDatabase[1])['IDString'].decode('hex').encode('ascii')
IDString = (kindleDatabase[1])['IDString'].decode('hex')
# Get the UserName stored when the Kindle Info file was decoded
UserName = (kindleDatabase[1])['UserName'].decode('hex').encode('ascii')
UserName = (kindleDatabase[1])['UserName'].decode('hex')
except KeyError:
print u"Keys not found in the database {0}.".format(kindleDatabase[0])

View File

@@ -4,9 +4,7 @@
from __future__ import with_statement
# kindlekey.py
# Copyright © 2010-2013 by some_updates and Apprentice Alf
#
# Currently requires alfcrypto.py which requires the alfcrypto library
# Copyright © 2010-2015 by some_updates, Apprentice Alf and Apprentice Harper
# Revision history:
# 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc.
@@ -19,6 +17,8 @@ from __future__ import with_statement
# 1.6 - Fixed a problem getting the disk serial numbers
# 1.7 - Work if TkInter is missing
# 1.8 - Fixes for Kindle for Mac, and non-ascii in Windows user names
# 1.9 - Fixes for Unicode in Windows user names
# 2.0 - Added comments and extra fix for non-ascii Windows user names
"""
@@ -26,7 +26,7 @@ Retrieve Kindle for PC/Mac user key.
"""
__license__ = 'GPL v3'
__version__ = '1.8'
__version__ = '1.9'
import sys, os, re
from struct import pack, unpack, unpack_from
@@ -884,6 +884,7 @@ if iswindows:
return "AlternateUserName"
buffer = create_unicode_buffer(len(buffer) * 2)
size.value = len(buffer)
# return low byte of the unicode value of each character of the username
return buffer.value.encode('utf-16-le')[::2]
return GetUserName
GetUserName = GetUserName()
@@ -907,18 +908,34 @@ if iswindows:
return CryptUnprotectData
CryptUnprotectData = CryptUnprotectData()
# Returns Environmental Variables that contain unicode
def getEnvironmentVariable(name):
import ctypes
name = unicode(name) # make sure string argument is unicode
n = ctypes.windll.kernel32.GetEnvironmentVariableW(name, None, 0)
if n == 0:
return None
buf = ctypes.create_unicode_buffer(u'\0'*n)
ctypes.windll.kernel32.GetEnvironmentVariableW(name, buf, n)
return buf.value
# Locate all of the kindle-info style files and return as list
def getKindleInfoFiles():
kInfoFiles = []
# some 64 bit machines do not have the proper registry key for some reason
# or the pythonn interface to the 32 vs 64 bit registry is broken
# or the python interface to the 32 vs 64 bit registry is broken
path = ""
if 'LOCALAPPDATA' in os.environ.keys():
path = os.environ['LOCALAPPDATA']
# Python 2.x does not return unicode env. Use Python 3.x
path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
# this is just another alternative.
# path = getEnvironmentVariable('LOCALAPPDATA')
if not os.path.isdir(path):
path = ""
else:
# User Shell Folders show take precedent over Shell Folders if present
try:
# this will still break
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\")
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
if not os.path.isdir(path):
@@ -937,13 +954,14 @@ if iswindows:
if path == "":
print ('Could not find the folder in which to look for kinfoFiles.')
else:
print('searching for kinfoFiles in ' + path)
# Probably not the best. To Fix (shouldn't ignore in encoding) or use utf-8
print(u'searching for kinfoFiles in ' + path.encode('ascii', 'ignore'))
# look for (K4PC 1.9.0 and later) .kinf2011 file
kinfopath = path +'\\Amazon\\Kindle\\storage\\.kinf2011'
if os.path.isfile(kinfopath):
found = True
print('Found K4PC 1.9+ kinf2011 file: ' + kinfopath)
print('Found K4PC 1.9+ kinf2011 file: ' + kinfopath.encode('ascii','ignore'))
kInfoFiles.append(kinfopath)
# look for (K4PC 1.6.0 and later) rainier.2.1.1.kinf file
@@ -1142,11 +1160,11 @@ if iswindows:
cleartext = CryptUnprotectData(encryptedValue, entropy, 1)
DB[keyname] = cleartext
if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB:
print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(GetIDString(), GetUserName().decode("latin-1"))
if 'kindle.account.tokens' in DB:
# store values used in decryption
DB['IDString'] = GetIDString()
DB['UserName'] = GetUserName()
print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(GetIDString(), GetUserName().encode('hex'))
else:
DB = {}
return DB
@@ -1758,7 +1776,7 @@ elif isosx:
break
except:
pass
if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB:
if 'kindle.account.tokens' in DB:
# store values used in decryption
print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString, GetUserName())
DB['IDString'] = IDString

View File

@@ -156,6 +156,8 @@ def PC1(key, src, decryption=True):
return Pukall_Cipher().PC1(key,src,decryption)
except NameError:
pass
except TypeError:
pass
# use slow python version, since Pukall_Cipher didn't load
sum1 = 0;

View File

@@ -23,6 +23,7 @@ class DeDRM_Prefs():
self.dedrmprefs.defaults['adeptkeys'] = {}
self.dedrmprefs.defaults['ereaderkeys'] = {}
self.dedrmprefs.defaults['kindlekeys'] = {}
self.dedrmprefs.defaults['androidkeys'] = {}
self.dedrmprefs.defaults['pids'] = []
self.dedrmprefs.defaults['serials'] = []
self.dedrmprefs.defaults['adobewineprefix'] = ""
@@ -40,6 +41,8 @@ class DeDRM_Prefs():
self.dedrmprefs['ereaderkeys'] = {}
if self.dedrmprefs['kindlekeys'] == {}:
self.dedrmprefs['kindlekeys'] = {}
if self.dedrmprefs['androidkeys'] == {}:
self.dedrmprefs['androidkeys'] = {}
if self.dedrmprefs['pids'] == []:
self.dedrmprefs['pids'] = []
if self.dedrmprefs['serials'] == []:

View File

@@ -166,8 +166,30 @@ def decryptk4mobi(infile, outdir, rscpath):
for filename in files:
dpath = os.path.join(rscpath,filename)
kDatabaseFiles.append(dpath)
androidFiles = []
files = os.listdir(rscpath)
filefilter = re.compile("\.ab$", re.IGNORECASE)
files = filter(filefilter.search, files)
if files:
for filename in files:
dpath = os.path.join(rscpath,filename)
androidFiles.append(dpath)
files = os.listdir(rscpath)
filefilter = re.compile("\.db$", re.IGNORECASE)
files = filter(filefilter.search, files)
if files:
for filename in files:
dpath = os.path.join(rscpath,filename)
androidFiles.append(dpath)
files = os.listdir(rscpath)
filefilter = re.compile("\.xml$", re.IGNORECASE)
files = filter(filefilter.search, files)
if files:
for filename in files:
dpath = os.path.join(rscpath,filename)
androidFiles.append(dpath)
try:
rv = k4mobidedrm.decryptBook(infile, outdir, kDatabaseFiles, serialnums, pidnums)
rv = k4mobidedrm.decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serialnums, pidnums)
except Exception, e:
errlog += traceback.format_exc()
errlog += str(e)

View File

@@ -178,7 +178,12 @@ class DocParser(object):
if val == "":
val = 0
if not ((attr == 'hang') and (int(val) == 0)) :
if not ((attr == 'hang') and (int(val) == 0)):
try:
f = float(val)
except:
print "Warning: unrecognised val, ignoring"
val = 0
pv = float(val)/scale
cssargs[attr] = (self.attr_val_map[attr], pv)
keep = True

View File

@@ -0,0 +1,148 @@
#!/usr/bin/env python
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
import os, sys
import signal
import threading
import subprocess
from subprocess import Popen, PIPE, STDOUT
# **heavily** chopped up and modfied version of asyncproc.py
# to make it actually work on Windows as well as Mac/Linux
# For the original see:
# "http://www.lysator.liu.se/~bellman/download/"
# author is "Thomas Bellman <bellman@lysator.liu.se>"
# available under GPL version 3 or Later
# create an asynchronous subprocess whose output can be collected in
# a non-blocking manner
# What a mess! Have to use threads just to get non-blocking io
# in a cross-platform manner
# luckily all thread use is hidden within this class
class Process(object):
def __init__(self, *params, **kwparams):
if len(params) <= 3:
kwparams.setdefault('stdin', subprocess.PIPE)
if len(params) <= 4:
kwparams.setdefault('stdout', subprocess.PIPE)
if len(params) <= 5:
kwparams.setdefault('stderr', subprocess.PIPE)
self.__pending_input = []
self.__collected_outdata = []
self.__collected_errdata = []
self.__exitstatus = None
self.__lock = threading.Lock()
self.__inputsem = threading.Semaphore(0)
self.__quit = False
self.__process = subprocess.Popen(*params, **kwparams)
if self.__process.stdin:
self.__stdin_thread = threading.Thread(
name="stdin-thread",
target=self.__feeder, args=(self.__pending_input,
self.__process.stdin))
self.__stdin_thread.setDaemon(True)
self.__stdin_thread.start()
if self.__process.stdout:
self.__stdout_thread = threading.Thread(
name="stdout-thread",
target=self.__reader, args=(self.__collected_outdata,
self.__process.stdout))
self.__stdout_thread.setDaemon(True)
self.__stdout_thread.start()
if self.__process.stderr:
self.__stderr_thread = threading.Thread(
name="stderr-thread",
target=self.__reader, args=(self.__collected_errdata,
self.__process.stderr))
self.__stderr_thread.setDaemon(True)
self.__stderr_thread.start()
def pid(self):
return self.__process.pid
def kill(self, signal):
self.__process.send_signal(signal)
# check on subprocess (pass in 'nowait') to act like poll
def wait(self, flag):
if flag.lower() == 'nowait':
rc = self.__process.poll()
else:
rc = self.__process.wait()
if rc != None:
if self.__process.stdin:
self.closeinput()
if self.__process.stdout:
self.__stdout_thread.join()
if self.__process.stderr:
self.__stderr_thread.join()
return self.__process.returncode
def terminate(self):
if self.__process.stdin:
self.closeinput()
self.__process.terminate()
# thread gets data from subprocess stdout
def __reader(self, collector, source):
while True:
data = os.read(source.fileno(), 65536)
self.__lock.acquire()
collector.append(data)
self.__lock.release()
if data == "":
source.close()
break
return
# thread feeds data to subprocess stdin
def __feeder(self, pending, drain):
while True:
self.__inputsem.acquire()
self.__lock.acquire()
if not pending and self.__quit:
drain.close()
self.__lock.release()
break
data = pending.pop(0)
self.__lock.release()
drain.write(data)
# non-blocking read of data from subprocess stdout
def read(self):
self.__lock.acquire()
outdata = "".join(self.__collected_outdata)
del self.__collected_outdata[:]
self.__lock.release()
return outdata
# non-blocking read of data from subprocess stderr
def readerr(self):
self.__lock.acquire()
errdata = "".join(self.__collected_errdata)
del self.__collected_errdata[:]
self.__lock.release()
return errdata
# non-blocking write to stdin of subprocess
def write(self, data):
if self.__process.stdin is None:
raise ValueError("Writing to process with stdin not a pipe")
self.__lock.acquire()
self.__pending_input.append(data)
self.__inputsem.release()
self.__lock.release()
# close stdinput of subprocess
def closeinput(self):
self.__lock.acquire()
self.__quit = True
self.__inputsem.release()
self.__lock.release()

View File

@@ -356,7 +356,7 @@ class TopazBook:
self.setBookKey(bookKey)
self.createBookDirectory()
self.extractFiles()
self.extractFiles()
print u"Successfully Extracted Topaz contents"
if inCalibre:
from calibre_plugins.dedrm import genbook

View File

@@ -1,66 +1,77 @@
DeDRM_App - DeDRM_App.pyw and DeDRM_Drop_Target.bat
===========================================================
DeDRM_App - DeDRM_App.pyw and DeDRM_Drop_Target.bat
===================================================
DeDRM_App.pyw 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.bat to have the DRM removed. It repackages all the "tools" python software in one easy to use program that remembers preferences and settings.
DeDRM_App.pyw is a python drag and drop application that allows users to drag and drop ebooks or folders of ebooks onto the DeDRM_Drop_Target.bat to have the DRM removed. It repackages all the "tools" python software (except obok) in one easy to use program that remembers preferences and settings.
It will work without manual configuration for Kindle for PC ebooks and Adobe Digital Edition epub and pdf ebooks, when Kindle for PC and/or Adobe Digital Editions are installed on the same computer.
It will work without manual configuration for Kindle for PC ebooks, Adobe Digital Edition epub and pdf ebooks and Barnes & Noble NOOK Study ePubs when Kindle for PC, Adobe Digital Editions and NOOK Study are installed on the same computer and user account.
To remove the DRM from eInk Kindle ebooks, Barnes and Noble epubs, Mobipocket ebooks and Fictionwise eReader ebooks requires the user to double-click the DeDRM_Drop_Target.bat file and set some additional Preferences including:
To remove the DRM from eInk Kindle ebooks, Kindle for Android ebooks, Mobipocket ebooks and Fictionwise eReader ebooks requires the user to double-click the DeDRM_Drop_Target.bat file and set some additional Preferences including:
eInk Kindle: 16 digit Serial Number
Barnes & Noble: key file (bnepubkey.b64) generate using ignoblekeygen.pyw
Kindle for Android: backup.ab file, details below.
eReader Social DRM: Name:Last 8 digits of CC number
MobiPocket: 10 digit PID
Once these preferences have been set, the user can simply drag and drop ebooks onto the DeDRM_Drop_Target to remove the DRM. Note that after setting preferences it is necessary to click on "Set Prefs" button and then quit the application for the change in preferences to fully take effect.
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.
This program requires that Python 2.7 and PyCrypto 2.6 for Python 2.7 be installed on your computer before it will work. See below for how to get and install these programs for Windows.
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_App folder from tools_v6.0.0/DeDRM_Application_Windows to your "My Documents" folder.
1. Drag the DeDRM_App folder from tools_v6.2.2/DeDRM_Application_Windows to your "My Documents" folder.
2. Open the DeDRM_App 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 the short-cut you've just created.
Kindle for Android
------------------
The backup.ab file needs to be obtained using Android-specific tools.
Obtain and install adb (Android Debug Bridge) on your computer. Details of how to do this are beyond the scope of this readme file, but there are plenty of on-line guides.
Enable developer mode on your Android device. Again, look for an on-line guide for your device.
Once you have adb installed and your device in developer mode, connect your device to your computer with a USB cable and then open up a command line (Terminal on Mac OS X and cmd.exe on Windows) and enter "adb backup com.amazon.kindle" (without the quotation marks!) and press return. A file "backup.ab" should be created in your home directory.
Credits
-------
The mobidedrm and erdr2pml scripts were created by The Dark Reverser
The ignobleepub, ignoblekeygen, ineptepub and adobe key scripts were created by i♥cabbages
The k4mobidedrm script and supporting scripts were written by some_updates with help from DiapDealer and Apprentice Alf, based on code by Bart Simpson (aka Skindle), CMBDTC and clarknova
The alfcrypto library was created by some_updates
The ePub encryption detection script was adapted by Apprentice Alf from a script by Paul Durrant
The DeDRM all-in-one AppleScript was created by Apprentice Alf
The DeDRM all-in-one python script was created by some_updates and Apprentice Alf
The original inept and ignoble scripts were by i♥cabbages
The original mobidedrm and erdr2pml scripts were by The Dark Reverser
The original topaz DRM removal script was by CMBDTC
The original topaz format conversion scripts were by some_updates, clarknova and Bart Simpson
The alfcrypto library is by some_updates
The ePub encryption detection script is by Apprentice Alf, adapted from a script by Paul Durrant
The ignoblekey script is by Apprentice Harper
The DeDRM python GUI was by some_updates and is maintained by Apprentice Alf and Apprentice Harper
Many fixes, updates and enhancements to the scripts and applicatons have been made by many other people. For more details, see the commments in the individual scripts.
Installing Python on Windows
----------------------------
I strongly recommend fully installing ActiveStates 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.
I strongly recommend fully installing ActiveStates Active Python, free Community Edition for Windows. 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.
1. Download ActivePython 2.7.8 for Windows (or later 2.7.x version for Windows) from http://www.activestate.com/activepython/downloads.
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 Foords 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.
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 Python 2.7. I recommend the installer linked from Michael Foords blog.
1. Download PyCrypto 2.6 (or later) for Windows and Python 2.7 from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
2. When it has finished downloading, run the application. Accept the default options.
Linux Users
===========
The DeDRM_app.pyw script, although not the bat shortcut, should work under Linux. Drag & drop functionality is not available.
The DeDRM_app.pyw script, although not the .bat shortcut, should work under Linux. Drag & drop functionality is not available. Depending on your Linux installation, you may or may not need to install Python 2 and PyCrypto.

View File

@@ -22,13 +22,19 @@ li {margin-top: 0.5em}
<p>If you have upgraded from an earlier version of the plugin, any existing Barnes and Noble keys will have been automatically imported, so you might not need to do any more configuration. Continue reading for key generation and management instructions.</p>
<h3>Changes at Barnes & Noble</h3>
<p>In mid-2014, Barnes & Noble changed the way they generated encryption keys. Instead of deriving the key from the user's name and credit card number, they started generating a random key themselves, sending that key through to devices when they connected to the Barnes & Noble servers. This means that most users will now find that no combination of their name and CC# will work in decrypting their recently downloaded ebooks.</p>
<p>Someone commenting at Apprentice Alf's blog detailed a way to retrieve a new account key using the account's email address and password. This method has now been incorporated into the plugin.
<h3>Creating New Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering the necessary data to generate a new key.</p>
<ul>
<li><span class="bold">Unique Key Name:</span> this is a unique name you choose to help you identify the key. This name will show in the list of configured keys. Choose something that will help you remember the data (name, cc#) it was created with.</li>
<li><span class="bold">Your Name:</span> This is the name used by Barnes and Noble to generate your encryption key. Seemingly at random, Barnes and Noble choose one of three places from which to take this name. Most commonly, its your name as set in your Barnes &amp; Noble account, My Account page, directly under PERSONAL INFORMATION. Sometimes it is the the name used in the default shipping address, and sometimes its the name listed for the active credit card. If these names are different in your Barnes and Noble account preferences, I suggest creating one key for each version of your name. This name will not be stored anywhere on your computer or in calibre. It will only be used in the creation of the one-way hash/key thats stored in the preferences.</li>
<li><span class="bold">Credit Card#:</span> this is the default credit card number that was on file with Barnes and Noble at the time of download of the ebook to be de-DRMed. Just enter the 16 (15 for American Express) digits. As with the name, this number will not be stored anywhere on your computer or in calibre. It will only be used in the creation of the one-way hash/key thats stored in the preferences.</li>
<li><span class="bold">Unique Key Name:</span> this is a unique name you choose to help you identify the key. This name will show in the list of configured keys. Choose something that will help you remember the data (account email address) it was created with.</li>
<li><span class="bold">B&N/nook account email address:</span> This is the default email address for your Barnes and Noble/nook account. This email will not be stored anywhere on your computer or in calibre. It will only be used to fetch the account key that from the B&N server, and it is that key that will be stored in the preferences.</li>
<li><span class="bold">B&N/nook account password:</span> this is the password for your Barnes and Noble/nook account. As with the email address, this will not be stored anywhere on your computer or in calibre. It will only be used to fetch the key from the B&N server.</li>
</ul>
<p>Click the OK button to create and store the generated key. Or Cancel if you dont want to create a key.</p>
@@ -48,10 +54,15 @@ li {margin-top: 0.5em}
<h3>Importing Existing Keyfiles:</h3>
<p>At the bottom-left of the plugins customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing .b64 key files. Key files might come from being exported from this or older plugins, or may have been generated using the original i♥cabbages script.</p>
<p>At the bottom-left of the plugins customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing .b64 key files. Key files might come from being exported from this or older plugins, or may have been generated using the original i♥cabbages script, or you may have made it by following the instructions above.</p>
<p>Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.</p>
<h3>NOOK Study</h3>
<p>Books downloaded through NOOK Study may or may not use the key found using the above method. If a book is not decrypted successfully with any of the keys, the plugin will attempt to recover keys from the NOOK Study log file and use them.</p>
</body>
</html>

View File

@@ -1,4 +1,4 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
@@ -27,7 +27,7 @@ li {margin-top: 0.5em}
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering a new Kindle serial number.</p>
<ul>
<li><span class="bold">Eink Kindle Serial Number:</span> this is the unique serial number of your device. It usually starts with a B or a 9 and is sixteen characters long. For a reference of where to find serial numbers and their ranges, please refere to this <a href="http://wiki.mobileread.com/wiki/Kindle_serial_numbers">mobileread wiki page.</a></li>
<li><span class="bold">Eink Kindle Serial Number:</span> this is the unique serial number of your device. It usually starts with a B or a 9 and is sixteen characters long. For a reference of where to find serial numbers and their ranges, please refer to this <a href="http://wiki.mobileread.com/wiki/Kindle_serial_numbers">mobileread wiki page.</a></li>
</ul>
<p>Click the OK button to save the serial number. Or Cancel if you didnt want to enter a serial number.</p>

View File

@@ -17,7 +17,7 @@ p {margin-top: 0}
<body>
<h1>DeDRM Plugin <span class="version">(v6.0.0)</span></h1>
<h1>DeDRM Plugin <span class="version">(v6.3.0)</span></h1>
<p>This plugin removes DRM from ebooks when they are imported into calibre. If you already have DRMed ebooks in your calibre library, you will need to remove them and import them again.</p>

View File

@@ -0,0 +1,61 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Managing Kindle for Android Keys</title>
<style type="text/css">
span.version {font-size: 50%}
span.bold {font-weight: bold}
h3 {margin-bottom: 0}
p {margin-top: 0}
li {margin-top: 0.5em}
</style>
</head>
<body>
<h1>Managing Kindle for Android Keys</h1>
<p>Amazon's Kindle for Android application uses an internal key equivalent to an eInk Kindle's serial number. Extracting that key is a little tricky, but worth it, as it then allows the DRM to be removed from any Kindle ebooks that have been downloaded to that Android device.</p>
<p>Please note that it is not currently known whether the same applies to the Kindle application on the Kindle Fire and Fire HD.</p>
<h3>Getting the Kindle for Android backup file</h3>
<p>Obtain and install adb (Android Debug Bridge) on your computer. Details of how to do this are beyond the scope of this help file, but there are plenty of on-line guides.</p>
<p>Enable developer mode on your Android device. Again, look for an on-line guide for your device.</p>
<p>Once you have adb installed and your device in developer mode, connect your device to your computer with a USB cable and then open up a command line (Terminal on Mac OS X and cmd.exe on Windows) and enter "adb backup com.amazon.kindle" (without the quotation marks!) and press return. A file "backup.ab" should be created in your home directory.
<h3>Adding a Kindle for Android Key</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog with two main controls.
<ul>
<li><span class="bold">Choose backup file:</span> click this button and you will be prompted to find the backup.ab file you created earlier. Once selected the file will be processed to extract the decryption key, and if successful the file name will be displayed to the right of the button.</li>
<li><span class="bold">Unique Key Name:</span> this is a unique name you choose to help you identify the key. This name will show in the list of Kindle for Android keys. Enter a name that will help you remember which device this key came from.</li>
</ul>
<p>Click the OK button to store the Kindle for Android key for the current list of Kindle for Android keys. Or click Cancel if you dont want to store the key.</p>
<p>New keys are checked against the current list of keys before being added, and duplicates are discarded.</p>
<h3>Deleting Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted key in the list. You will be prompted once to be sure thats what you truly mean to do. Once gone, its permanently gone.</p>
<h3>Renaming Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will prompt you to enter a new name for the highlighted key in the list. Enter the new name for the key and click the OK button to use the new name, or Cancel to revert to the old name.</p>
<h3>Exporting Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a computers hard-drive. Use this button to export the highlighted key to a file (with a .k4a' file name extension). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.</p>
<h3>Importing Existing Keyfiles:</h3>
<p>At the bottom-left of the plugins customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import any .k4a file you obtained by using the androidkindlekey.py script manually, or by exporting from another copy of calibre.</p>
</body>
</html>

View File

@@ -31,14 +31,29 @@ __docformat__ = 'restructuredtext en'
# 6.0.3 - Fixes for Kindle for Mac and Windows non-ascii user names
# 6.0.4 - Fixes for stand-alone scripts and applications
# and pdb files in plugin and initial conversion of prefs.
# 6.0.5 - Fix a key issue
# 6.0.6 - Fix up an incorrect function call
# 6.0.7 - Error handling for incomplete PDF metadata
# 6.0.8 - Fixes a Wine key issue and topaz support
# 6.0.9 - Ported to work with newer versions of Calibre (moved to Qt5). Still supports older Qt4 versions.
# 6.1.0 - Fixed multiple books import problem and PDF import with no key problem
# 6.2.0 - Support for getting B&N key from nook Study log. Fix for UTF-8 filenames in Adobe ePubs.
# Fix for not copying needed files. Fix for getting default Adobe key for PDFs
# 6.2.1 - Fix for non-ascii Windows user names
# 6.2.2 - Added URL method for B&N/nook books
# 6.3.0 - Added in Kindle for Android serial number solution
# 6.3.1 - Version number bump for clarity
# 6.3.2 - Fixed Kindle for Android help file
# 6.3.3 - Bug fix for Kindle for PC support
# 6.3.4 - Fixes for Kindle for Android, Linux, and Kobo 3.17
"""
Decrypt DRMed ebooks.
"""
PLUGIN_NAME = u"DeDRM"
PLUGIN_VERSION_TUPLE = (6, 0, 7)
PLUGIN_VERSION_TUPLE = (6, 3, 4)
PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE])
# Include an html helpfile in the plugin's zipfile with the following name.
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
@@ -79,37 +94,27 @@ class DeDRM(FileTypePlugin):
name = PLUGIN_NAME
description = u"Removes DRM from Amazon Kindle, Adobe Adept (including Kobo), Barnes & Noble, Mobipocket and eReader ebooks. Credit given to i♥cabbages and The Dark Reverser for the original stand-alone scripts."
supported_platforms = ['linux', 'osx', 'windows']
author = u"DiapDealer, Apprentice Alf, The Dark Reverser and i♥cabbages"
author = u"Apprentice Alf, Aprentice Harper, The Dark Reverser 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','pdf','pdb','prc','mobi','azw','azw1','azw3','azw4','tpz'])
file_types = set(['epub','pdf','pdb','prc','mobi','pobi','azw','azw1','azw3','azw4','tpz'])
on_import = True
priority = 600
def initialize(self):
# convert old preferences, if necessary.
try:
from calibre_plugins.dedrm.prefs import convertprefs
convertprefs()
except:
traceback.print_exc()
def initialize(self):
"""
Dynamic modules can't be imported/loaded from a zipfile... so this routine
runs whenever the plugin gets initialized. This will extract the appropriate
Dynamic modules can't be imported/loaded from a zipfile.
So this routine will extract the appropriate
library for the target OS and copy it to the 'alfcrypto' subdirectory of
calibre's configuration directory. That 'alfcrypto' directory is then
inserted into the syspath (as the very first entry) in the run function
so the CDLL stuff will work in the alfcrypto.py script.
The extraction only happens once per version of the plugin
Also perform upgrade of preferences once per version
"""
try:
if iswindows:
names = [u"alfcrypto.dll",u"alfcrypto64.dll"]
elif isosx:
names = [u"libalfcrypto.dylib"]
else:
names = [u"libalfcrypto32.so",u"libalfcrypto64.so",u"kindlekey.py",u"adobekey.py",u"subasyncio.py"]
lib_dict = self.load_resources(names)
self.pluginsdir = os.path.join(config_dir,u"plugins")
if not os.path.exists(self.pluginsdir):
os.mkdir(self.pluginsdir)
@@ -122,9 +127,38 @@ class DeDRM(FileTypePlugin):
self.alfdir = os.path.join(self.maindir,u"libraryfiles")
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)
open(file_path,'wb').write(data)
# only continue if we've never run this version of the plugin before
self.verdir = os.path.join(self.maindir,PLUGIN_VERSION)
if not os.path.exists(self.verdir):
if iswindows:
names = [u"alfcrypto.dll",u"alfcrypto64.dll"]
elif isosx:
names = [u"libalfcrypto.dylib"]
else:
names = [u"libalfcrypto32.so",u"libalfcrypto64.so",u"kindlekey.py",u"adobekey.py",u"subasyncio.py"]
lib_dict = self.load_resources(names)
print u"{0} v{1}: Copying needed library files from plugin's zip".format(PLUGIN_NAME, PLUGIN_VERSION)
for entry, data in lib_dict.items():
file_path = os.path.join(self.alfdir, entry)
try:
os.remove(file_path)
except:
pass
try:
open(file_path,'wb').write(data)
except:
print u"{0} v{1}: Exception when copying needed library files".format(PLUGIN_NAME, PLUGIN_VERSION)
traceback.print_exc()
pass
# convert old preferences, if necessary.
from calibre_plugins.dedrm.prefs import convertprefs
convertprefs()
# mark that this version has been initialized
os.mkdir(self.verdir)
except Exception, e:
traceback.print_exc()
raise
@@ -162,7 +196,12 @@ class DeDRM(FileTypePlugin):
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)
try:
result = ignobleepub.decryptBook(userkey, inf.name, of.name)
except:
print u"{0} v{1}: Exception when trying to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
result = 1
of.close()
@@ -173,8 +212,69 @@ class DeDRM(FileTypePlugin):
print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime)
# perhaps we should see if we can get a key from a log file
print u"{0} v{1}: Looking for new NOOK Study Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
# get the default NOOK Study keys
defaultkeys = []
try:
if iswindows or isosx:
from calibre_plugins.dedrm.ignoblekey import nookkeys
defaultkeys = nookkeys()
else: # linux
from wineutils import WineGetKeys
scriptpath = os.path.join(self.alfdir,u"ignoblekey.py")
defaultkeys = WineGetKeys(scriptpath, u".b64",dedrmprefs['adobewineprefix'])
except:
print u"{0} v{1}: Exception when getting default NOOK Study Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
newkeys = []
for keyvalue in defaultkeys:
if keyvalue not in dedrmprefs['bandnkeys'].values():
newkeys.append(keyvalue)
if len(newkeys) > 0:
try:
for i,userkey in enumerate(newkeys):
print u"{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)
of = self.temporary_file(u".epub")
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
try:
result = ignobleepub.decryptBook(userkey, inf.name, of.name)
except:
print u"{0} v{1}: Exception when trying to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
result = 1
of.close()
if result == 0:
# Decryption was a success
# Store the new successful key in the defaults
print u"{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)
try:
dedrmprefs.addnamedvaluetoprefs('bandnkeys','nook_Study_key',keyvalue)
dedrmprefs.writeprefs()
print u"{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
except:
print u"{0} v{1}: Exception saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
# Return the modified PersistentTemporary file to calibre.
return of.name
print u"{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
except Exception, e:
pass
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt{2} after {3:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook),time.time()-self.starttime))
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
# import the Adobe Adept ePub handler
import calibre_plugins.dedrm.ineptepub as ineptepub
@@ -192,6 +292,8 @@ class DeDRM(FileTypePlugin):
try:
result = ineptepub.decryptBook(userkey, inf.name, of.name)
except:
print u"{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
result = 1
of.close()
@@ -222,6 +324,7 @@ class DeDRM(FileTypePlugin):
self.default_key = defaultkeys[0]
except:
print u"{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
self.default_key = u""
@@ -240,6 +343,8 @@ class DeDRM(FileTypePlugin):
try:
result = ineptepub.decryptBook(userkey, inf.name, of.name)
except:
print u"{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
result = 1
of.close()
@@ -251,7 +356,9 @@ class DeDRM(FileTypePlugin):
try:
dedrmprefs.addnamedvaluetoprefs('adeptkeys','default_key',keyvalue.encode('hex'))
dedrmprefs.writeprefs()
print u"{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
except:
print u"{0} v{1}: Exception when saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
# Return the modified PersistentTemporary file to calibre.
return of.name
@@ -262,12 +369,12 @@ class DeDRM(FileTypePlugin):
# Something went wrong with decryption.
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt{2} after {3:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook),time.time()-self.starttime))
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
# Not a Barnes & Noble nor an Adobe Adept
# Import the fixed epub.
print u"{0} v{1}: “{2}” is neither an Adobe Adept nor a Barnes & Noble encrypted ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))
return inf.name
raise DeDRMError(u"{0} v{1}: Couldn't decrypt after {2:.1f} seconds. DRM free perhaps?".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
def PDFDecrypt(self,path_to_ebook):
import calibre_plugins.dedrm.prefs as prefs
@@ -285,6 +392,8 @@ class DeDRM(FileTypePlugin):
try:
result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name)
except:
print u"{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
result = 1
of.close()
@@ -294,67 +403,73 @@ class DeDRM(FileTypePlugin):
# Return the modified PersistentTemporary file to calibre.
return of.name
# perhaps we need to get a new default ADE key
print u"{0} v{1}: Looking for new default Adobe Digital Editions Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime)
# get the default Adobe keys
defaultkeys = []
# perhaps we need to get a new default ADE key
print u"{0} v{1}: Looking for new default Adobe Digital Editions Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
# get the default Adobe keys
defaultkeys = []
try:
if iswindows or isosx:
import calibre_plugins.dedrm.adobekey as adobe
from calibre_plugins.dedrm.adobekey import adeptkeys
try:
defaultkeys = adobe.adeptkeys()
except:
pass
else:
# linux
try:
from wineutils import WineGetKeys
defaultkeys = adeptkeys()
else: # linux
from wineutils import WineGetKeys
scriptpath = os.path.join(self.alfdir,u"adobekey.py")
defaultkeys = self.WineGetKeys(scriptpath, u".der",dedrmprefs['adobewineprefix'])
except:
pass
scriptpath = os.path.join(self.alfdir,u"adobekey.py")
defaultkeys = WineGetKeys(scriptpath, u".der",dedrmprefs['adobewineprefix'])
newkeys = []
for keyvalue in defaultkeys:
if keyvalue.encode('hex') not in dedrmprefs['adeptkeys'].values():
newkeys.append(keyvalue)
self.default_key = defaultkeys[0]
except:
print u"{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
self.default_key = u""
if len(newkeys) > 0:
try:
for i,userkey in enumerate(newkeys):
print u"{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)
of = self.temporary_file(u".pdf")
newkeys = []
for keyvalue in defaultkeys:
if keyvalue.encode('hex') not in dedrmprefs['adeptkeys'].values():
newkeys.append(keyvalue)
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
if len(newkeys) > 0:
try:
for i,userkey in enumerate(newkeys):
print u"{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)
of = self.temporary_file(u".pdf")
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
try:
result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name)
except:
print u"{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
result = 1
of.close()
if result == 0:
# Decryption was a success
# Store the new successful key in the defaults
print u"{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)
try:
result = ineptepdf.decryptBook(userkey, inf.name, of.name)
dedrmprefs.addnamedvaluetoprefs('adeptkeys','default_key',keyvalue.encode('hex'))
dedrmprefs.writeprefs()
print u"{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
except:
result = 1
print u"{0} v{1}: Exception when saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
# Return the modified PersistentTemporary file to calibre.
return of.name
of.close()
if result == 0:
# Decryption was a success
# Store the new successful key in the defaults
print u"{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)
try:
dedrmprefs.addnamedvaluetoprefs('adeptkeys','default_key',keyvalue.encode('hex'))
dedrmprefs.writeprefs()
except:
traceback.print_exc()
# Return the modified PersistentTemporary file to calibre.
return of.name
print u"{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
except Exception, e:
pass
print u"{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
except Exception, e:
pass
# Something went wrong with decryption.
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt{2} after {3:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook),time.time()-self.starttime))
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
def KindleMobiDecrypt(self,path_to_ebook):
@@ -371,10 +486,15 @@ class DeDRM(FileTypePlugin):
dedrmprefs = prefs.DeDRM_Prefs()
pids = dedrmprefs['pids']
serials = dedrmprefs['serials']
for android_serials_list in dedrmprefs['androidkeys'].values():
#print android_serials_list
serials.extend(android_serials_list)
#print serials
androidFiles = []
kindleDatabases = dedrmprefs['kindlekeys'].items()
try:
book = k4mobidedrm.GetDecryptedBook(path_to_ebook,kindleDatabases,serials,pids,self.starttime)
book = k4mobidedrm.GetDecryptedBook(path_to_ebook,kindleDatabases,androidFiles,serials,pids,self.starttime)
except Exception, e:
decoded = False
# perhaps we need to get a new default Kindle for Mac/PC key
@@ -391,8 +511,10 @@ class DeDRM(FileTypePlugin):
from wineutils import WineGetKeys
scriptpath = os.path.join(self.alfdir,u"kindlekey.py")
defaultkeys = self.WineGetKeys(scriptpath, u".k4i",dedrmprefs['kindlewineprefix'])
defaultkeys = WineGetKeys(scriptpath, u".k4i",dedrmprefs['kindlewineprefix'])
except:
print u"{0} v{1}: Exception when getting default Kindle Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
traceback.print_exc()
pass
newkeys = {}
@@ -403,7 +525,7 @@ class DeDRM(FileTypePlugin):
if len(newkeys) > 0:
print u"{0} v{1}: Found {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), u"key" if len(newkeys)==1 else u"keys")
try:
book = k4mobidedrm.GetDecryptedBook(path_to_ebook,newkeys.items(),[],[],self.starttime)
book = k4mobidedrm.GetDecryptedBook(path_to_ebook,newkeys.items(),[],[],[],self.starttime)
decoded = True
# store the new successful keys in the defaults
print u"{0} v{1}: Saving {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), u"key" if len(newkeys)==1 else u"keys")
@@ -415,8 +537,7 @@ class DeDRM(FileTypePlugin):
if not decoded:
#if you reached here then no luck raise and exception
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
traceback.print_exc()
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt “{4}” after {3:.1f} seconds with error: {2}\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0],time.time()-self.starttime,os.path.basename(path_to_ebook)))
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
of = self.temporary_file(book.getBookExtension())
book.getFile(of.name)
@@ -445,12 +566,13 @@ class DeDRM(FileTypePlugin):
# Decryption was successful return the modified PersistentTemporary
# file to Calibre's import process.
if result == 0:
print u"{0} v{1}: Successfully decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime)
return of.name
print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime)
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt{2} after {3:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook),time.time()-self.starttime))
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
def run(self, path_to_ebook):
@@ -463,7 +585,7 @@ class DeDRM(FileTypePlugin):
self.starttime = time.time()
booktype = os.path.splitext(path_to_ebook)[1].lower()[1:]
if booktype in ['prc','mobi','azw','azw1','azw3','azw4','tpz']:
if booktype in ['prc','mobi','pobi','azw','azw1','azw3','azw4','tpz']:
# Kindle/Mobipocket
decrypted_ebook = self.KindleMobiDecrypt(path_to_ebook)
elif booktype == 'pdb':
@@ -480,7 +602,7 @@ class DeDRM(FileTypePlugin):
else:
print u"Unknown booktype {0}. Passing back to calibre unchanged".format(booktype)
return path_to_ebook
print u"{0} v{1}: Successfully decrypted book after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
print u"{0} v{1}: Finished after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
return decrypted_ebook
def is_customizable(self):

View File

@@ -0,0 +1,75 @@
import sys
import Tkinter
import Tkconstants
class ActivityBar(Tkinter.Frame):
def __init__(self, master, length=300, height=20, barwidth=15, interval=50, bg='white', fillcolor='orchid1',\
bd=2, relief=Tkconstants.GROOVE, *args, **kw):
Tkinter.Frame.__init__(self, master, bg=bg, width=length, height=height, *args, **kw)
self._master = master
self._interval = interval
self._maximum = length
self._startx = 0
self._barwidth = barwidth
self._bardiv = length / barwidth
if self._bardiv < 10:
self._bardiv = 10
stopx = self._startx + self._barwidth
if stopx > self._maximum:
stopx = self._maximum
# self._canv = Tkinter.Canvas(self, bg=self['bg'], width=self['width'], height=self['height'],\
# highlightthickness=0, relief='flat', bd=0)
self._canv = Tkinter.Canvas(self, bg=self['bg'], width=self['width'], height=self['height'],\
highlightthickness=0, relief=relief, bd=bd)
self._canv.pack(fill='both', expand=1)
self._rect = self._canv.create_rectangle(0, 0, self._canv.winfo_reqwidth(), self._canv.winfo_reqheight(), fill=fillcolor, width=0)
self._set()
self.bind('<Configure>', self._update_coords)
self._running = False
def _update_coords(self, event):
'''Updates the position of the rectangle inside the canvas when the size of
the widget gets changed.'''
# looks like we have to call update_idletasks() twice to make sure
# to get the results we expect
self._canv.update_idletasks()
self._maximum = self._canv.winfo_width()
self._startx = 0
self._barwidth = self._maximum / self._bardiv
if self._barwidth < 2:
self._barwidth = 2
stopx = self._startx + self._barwidth
if stopx > self._maximum:
stopx = self._maximum
self._canv.coords(self._rect, 0, 0, stopx, self._canv.winfo_height())
self._canv.update_idletasks()
def _set(self):
if self._startx < 0:
self._startx = 0
if self._startx > self._maximum:
self._startx = self._startx % self._maximum
stopx = self._startx + self._barwidth
if stopx > self._maximum:
stopx = self._maximum
self._canv.coords(self._rect, self._startx, 0, stopx, self._canv.winfo_height())
self._canv.update_idletasks()
def start(self):
self._running = True
self.after(self._interval, self._step)
def stop(self):
self._running = False
self._set()
def _step(self):
if self._running:
stepsize = self._barwidth / 4
if stepsize < 2:
stepsize = 2
self._startx += stepsize
self._set()
self.after(self._interval, self._step)

View File

@@ -0,0 +1,468 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import with_statement
# androidkindlekey.py
# Copyright © 2013-15 by Thom and Apprentice Harper
# Some portions Copyright © 2010-15 by some_updates and Apprentice Alf
#
# Revision history:
# 1.0 - AmazonSecureStorage.xml decryption to serial number
# 1.1 - map_data_storage.db decryption to serial number
# 1.2 - Changed to be callable from AppleScript by returning only serial number
# - and changed name to androidkindlekey.py
# - and added in unicode command line support
# 1.3 - added in TkInter interface, output to a file
# 1.4 - Fix some problems identified by Aldo Bleeker
# 1.5 - Fix another problem identified by Aldo Bleeker
"""
Retrieve Kindle for Android Serial Number.
"""
__license__ = 'GPL v3'
__version__ = '1.5'
import os
import sys
import traceback
import getopt
import tempfile
import zlib
import tarfile
from hashlib import md5
from cStringIO import StringIO
from binascii import a2b_hex, b2a_hex
# Routines common to Mac and PC
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
# encoded using "replace" before writing them.
class SafeUnbuffered:
def __init__(self, stream):
self.stream = stream
self.encoding = stream.encoding
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
try:
from calibre.constants import iswindows, isosx
except:
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
def unicode_argv():
if iswindows:
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
# as a list of Unicode strings and encode them as utf-8
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"kindlekey.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
class DrmException(Exception):
pass
STORAGE = u"backup.ab"
STORAGE1 = u"AmazonSecureStorage.xml"
STORAGE2 = u"map_data_storage.db"
class AndroidObfuscation(object):
'''AndroidObfuscation
For the key, it's written in java, and run in android dalvikvm
'''
key = a2b_hex('0176e04c9408b1702d90be333fd53523')
def encrypt(self, plaintext):
cipher = self._get_cipher()
padding = len(self.key) - len(plaintext) % len(self.key)
plaintext += chr(padding) * padding
return b2a_hex(cipher.encrypt(plaintext))
def decrypt(self, ciphertext):
cipher = self._get_cipher()
plaintext = cipher.decrypt(a2b_hex(ciphertext))
return plaintext[:-ord(plaintext[-1])]
def _get_cipher(self):
try:
from Crypto.Cipher import AES
return AES.new(self.key)
except ImportError:
from aescbc import AES, noPadding
return AES(self.key, padding=noPadding())
class AndroidObfuscationV2(AndroidObfuscation):
'''AndroidObfuscationV2
'''
count = 503
password = 'Thomsun was here!'
def __init__(self, salt):
key = self.password + salt
for _ in range(self.count):
key = md5(key).digest()
self.key = key[:8]
self.iv = key[8:16]
def _get_cipher(self):
try :
from Crypto.Cipher import DES
return DES.new(self.key, DES.MODE_CBC, self.iv)
except ImportError:
from python_des import Des, CBC
return Des(self.key, CBC, self.iv)
def parse_preference(path):
''' parse android's shared preference xml '''
storage = {}
read = open(path)
for line in read:
line = line.strip()
# <string name="key">value</string>
if line.startswith('<string name="'):
index = line.find('"', 14)
key = line[14:index]
value = line[index+2:-9]
storage[key] = value
read.close()
return storage
def get_serials1(path=STORAGE1):
''' get serials from android's shared preference xml '''
if not os.path.isfile(path):
return []
storage = parse_preference(path)
salt = storage.get('AmazonSaltKey')
if salt and len(salt) == 16:
obfuscation = AndroidObfuscationV2(a2b_hex(salt))
else:
obfuscation = AndroidObfuscation()
def get_value(key):
encrypted_key = obfuscation.encrypt(key)
encrypted_value = storage.get(encrypted_key)
if encrypted_value:
return obfuscation.decrypt(encrypted_value)
return ''
# also see getK4Pids in kgenpids.py
try:
dsnid = get_value('DsnId')
except:
sys.stderr.write('cannot get DsnId\n')
return []
try:
tokens = set(get_value('kindle.account.tokens').split(','))
except:
return []
serials = []
if dsnid:
serials.append(dsnid)
for token in tokens:
if token:
serials.append('%s%s' % (dsnid, token))
serials.append(token)
return serials
def get_serials2(path=STORAGE2):
''' get serials from android's sql database '''
if not os.path.isfile(path):
return []
import sqlite3
connection = sqlite3.connect(path)
cursor = connection.cursor()
cursor.execute('''select userdata_value from userdata where userdata_key like '%/%token.device.deviceserialname%' ''')
userdata_keys = cursor.fetchall()
dsns = []
for userdata_row in userdata_keys:
try:
if userdata_row and userdata_row[0]:
userdata_utf8 = userdata_row[0].encode('utf8')
if len(userdata_utf8) > 0:
dsns.append(userdata_utf8)
except:
print "Error getting one of the device serial name keys"
traceback.print_exc()
pass
dsns = list(set(dsns))
cursor.execute('''select userdata_value from userdata where userdata_key like '%/%kindle.account.tokens%' ''')
userdata_keys = cursor.fetchall()
tokens = []
for userdata_row in userdata_keys:
try:
if userdata_row and userdata_row[0]:
userdata_utf8 = userdata_row[0].encode('utf8')
if len(userdata_utf8) > 0:
tokens.append(userdata_utf8)
except:
print "Error getting one of the account token keys"
traceback.print_exc()
pass
tokens = list(set(tokens))
serials = []
for x in dsns:
serials.append(x)
for y in tokens:
serials.append('%s%s' % (x, y))
for y in tokens:
serials.append(y)
return serials
def get_serials(path=STORAGE):
'''get serials from files in from android backup.ab
backup.ab can be get using adb command:
shell> adb backup com.amazon.kindle
or from individual files if they're passed.
'''
if not os.path.isfile(path):
return []
basename = os.path.basename(path)
if basename == STORAGE1:
return get_serials1(path)
elif basename == STORAGE2:
return get_serials2(path)
output = None
try :
read = open(path, 'rb')
head = read.read(24)
if head[:14] == 'ANDROID BACKUP':
output = StringIO(zlib.decompress(read.read()))
except Exception:
pass
finally:
read.close()
if not output:
return []
serials = []
tar = tarfile.open(fileobj=output)
for member in tar.getmembers():
if member.name.strip().endswith(STORAGE1):
write = tempfile.NamedTemporaryFile(mode='wb', delete=False)
write.write(tar.extractfile(member).read())
write.close()
write_path = os.path.abspath(write.name)
serials.extend(get_serials1(write_path))
os.remove(write_path)
elif member.name.strip().endswith(STORAGE2):
write = tempfile.NamedTemporaryFile(mode='wb', delete=False)
write.write(tar.extractfile(member).read())
write.close()
write_path = os.path.abspath(write.name)
serials.extend(get_serials2(write_path))
os.remove(write_path)
return list(set(serials))
__all__ = [ 'get_serials', 'getkey']
# procedure for CLI and GUI interfaces
# returns single or multiple keys (one per line) in the specified file
def getkey(outfile, inpath):
keys = get_serials(inpath)
if len(keys) > 0:
with file(outfile, 'w') as keyfileout:
for key in keys:
keyfileout.write(key)
keyfileout.write("\n")
return True
return False
def usage(progname):
print u"Decrypts the serial number(s) of Kindle For Android from Android backup or file"
print u"Get backup.ab file using adb backup com.amazon.kindle for Android 4.0+."
print u"Otherwise extract AmazonSecureStorage.xml from /data/data/com.amazon.kindle/shared_prefs/AmazonSecureStorage.xml"
print u"Or map_data_storage.db from /data/data/com.amazon.kindle/databases/map_data_storage.db"
print u""
print u"Usage:"
print u" {0:s} [-h] [-b <backup.ab>] [<outfile.k4a>]".format(progname)
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv()
progname = os.path.basename(argv[0])
print u"{0} v{1}\nCopyright © 2010-2015 Thom, some_updates, Apprentice Alf and Apprentice Harper".format(progname,__version__)
try:
opts, args = getopt.getopt(argv[1:], "hb:")
except getopt.GetoptError, err:
usage(progname)
print u"\nError in options or arguments: {0}".format(err.args[0])
return 2
inpath = ""
for o, a in opts:
if o == "-h":
usage(progname)
return 0
if o == "-b":
inpath = a
if len(args) > 1:
usage(progname)
return 2
if len(args) == 1:
# save to the specified file or directory
outfile = args[0]
if not os.path.isabs(outfile):
outfile = os.path.join(os.path.dirname(argv[0]),outfile)
outfile = os.path.abspath(outfile)
if os.path.isdir(outfile):
outfile = os.path.join(os.path.dirname(argv[0]),"androidkindlekey.k4a")
else:
# save to the same directory as the script
outfile = os.path.join(os.path.dirname(argv[0]),"androidkindlekey.k4a")
# make sure the outpath is OK
outfile = os.path.realpath(os.path.normpath(outfile))
if not os.path.isfile(inpath):
usage(progname)
print u"\n{0:s} file not found".format(inpath)
return 2
if getkey(outfile, inpath):
print u"\nSaved Kindle for Android key to {0}".format(outfile)
else:
print u"\nCould not retrieve Kindle for Android key."
return 0
def gui_main():
try:
import Tkinter
import Tkconstants
import tkMessageBox
import tkFileDialog
except:
print "Tkinter not installed"
return cli_main()
class DecryptionDialog(Tkinter.Frame):
def __init__(self, root):
Tkinter.Frame.__init__(self, root, border=5)
self.status = Tkinter.Label(self, text=u"Select backup.ab file")
self.status.pack(fill=Tkconstants.X, expand=1)
body = Tkinter.Frame(self)
body.pack(fill=Tkconstants.X, expand=1)
sticky = Tkconstants.E + Tkconstants.W
body.grid_columnconfigure(1, weight=2)
Tkinter.Label(body, text=u"Backup file").grid(row=0, column=0)
self.keypath = Tkinter.Entry(body, width=40)
self.keypath.grid(row=0, column=1, sticky=sticky)
self.keypath.insert(2, u"backup.ab")
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
button.grid(row=0, column=2)
buttons = Tkinter.Frame(self)
buttons.pack()
button2 = Tkinter.Button(
buttons, text=u"Extract", width=10, command=self.generate)
button2.pack(side=Tkconstants.LEFT)
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
button3 = Tkinter.Button(
buttons, text=u"Quit", width=10, command=self.quit)
button3.pack(side=Tkconstants.RIGHT)
def get_keypath(self):
keypath = tkFileDialog.askopenfilename(
parent=None, title=u"Select backup.ab file",
defaultextension=u".ab",
filetypes=[('adb backup com.amazon.kindle', '.ab'),
('All Files', '.*')])
if keypath:
keypath = os.path.normpath(keypath)
self.keypath.delete(0, Tkconstants.END)
self.keypath.insert(0, keypath)
return
def generate(self):
inpath = self.keypath.get()
self.status['text'] = u"Getting key..."
try:
keys = get_serials(inpath)
keycount = 0
for key in keys:
while True:
keycount += 1
outfile = os.path.join(progpath,u"kindlekey{0:d}.k4a".format(keycount))
if not os.path.exists(outfile):
break
with file(outfile, 'w') as keyfileout:
keyfileout.write(key)
success = True
tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile))
except Exception, e:
self.status['text'] = u"Error: {0}".format(e.args[0])
return
self.status['text'] = u"Select backup.ab file"
argv=unicode_argv()
progpath, progname = os.path.split(argv[0])
root = Tkinter.Tk()
root.title(u"Kindle for Android Key Extraction v.{0}".format(__version__))
root.resizable(True, False)
root.minsize(300, 0)
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
root.mainloop()
return 0
if __name__ == '__main__':
if len(sys.argv) > 1:
sys.exit(cli_main())
sys.exit(gui_main())

View File

@@ -0,0 +1,88 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys, os
import locale
import codecs
# get sys.argv arguments and encode them into utf-8
def unicode_argv():
if sys.platform.startswith('win'):
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'.
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"DeDRM.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
def add_cp65001_codec():
try:
codecs.lookup('cp65001')
except LookupError:
codecs.register(
lambda name: name == 'cp65001' and codecs.lookup('utf-8') or None)
return
def set_utf8_default_encoding():
if sys.getdefaultencoding() == 'utf-8':
return
# Regenerate setdefaultencoding.
reload(sys)
sys.setdefaultencoding('utf-8')
for attr in dir(locale):
if attr[0:3] != 'LC_':
continue
aref = getattr(locale, attr)
try:
locale.setlocale(aref, '')
except locale.Error:
continue
try:
lang = locale.getlocale(aref)[0]
except (TypeError, ValueError):
continue
if lang:
try:
locale.setlocale(aref, (lang, 'UTF-8'))
except locale.Error:
os.environ[attr] = lang + '.UTF-8'
try:
locale.setlocale(locale.LC_ALL, '')
except locale.Error:
pass
return

View File

@@ -0,0 +1,211 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
# to work around tk_chooseDirectory not properly returning unicode paths on Windows
# need to use a dialog that can be hacked up to actually return full unicode paths
# originally based on AskFolder from EasyDialogs for Windows but modified to fix it
# to actually use unicode for path
# The original license for EasyDialogs is as follows
#
# Copyright (c) 2003-2005 Jimmy Retzlaff
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
"""
AskFolder(...) -- Ask the user to select a folder Windows specific
"""
import os
import ctypes
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
import ctypes.wintypes as wintypes
__all__ = ['AskFolder']
# Load required Windows DLLs
ole32 = ctypes.windll.ole32
shell32 = ctypes.windll.shell32
user32 = ctypes.windll.user32
# Windows Constants
BFFM_INITIALIZED = 1
BFFM_SETOKTEXT = 1129
BFFM_SETSELECTIONA = 1126
BFFM_SETSELECTIONW = 1127
BIF_EDITBOX = 16
BS_DEFPUSHBUTTON = 1
CB_ADDSTRING = 323
CB_GETCURSEL = 327
CB_SETCURSEL = 334
CDM_SETCONTROLTEXT = 1128
EM_GETLINECOUNT = 186
EM_GETMARGINS = 212
EM_POSFROMCHAR = 214
EM_SETSEL = 177
GWL_STYLE = -16
IDC_STATIC = -1
IDCANCEL = 2
IDNO = 7
IDOK = 1
IDYES = 6
MAX_PATH = 260
OFN_ALLOWMULTISELECT = 512
OFN_ENABLEHOOK = 32
OFN_ENABLESIZING = 8388608
OFN_ENABLETEMPLATEHANDLE = 128
OFN_EXPLORER = 524288
OFN_OVERWRITEPROMPT = 2
OPENFILENAME_SIZE_VERSION_400 = 76
PBM_GETPOS = 1032
PBM_SETMARQUEE = 1034
PBM_SETPOS = 1026
PBM_SETRANGE = 1025
PBM_SETRANGE32 = 1030
PBS_MARQUEE = 8
PM_REMOVE = 1
SW_HIDE = 0
SW_SHOW = 5
SW_SHOWNORMAL = 1
SWP_NOACTIVATE = 16
SWP_NOMOVE = 2
SWP_NOSIZE = 1
SWP_NOZORDER = 4
VER_PLATFORM_WIN32_NT = 2
WM_COMMAND = 273
WM_GETTEXT = 13
WM_GETTEXTLENGTH = 14
WM_INITDIALOG = 272
WM_NOTIFY = 78
# Windows function prototypes
BrowseCallbackProc = ctypes.WINFUNCTYPE(ctypes.c_int, wintypes.HWND, ctypes.c_uint, wintypes.LPARAM, wintypes.LPARAM)
# Windows types
LPCTSTR = ctypes.c_char_p
LPTSTR = ctypes.c_char_p
LPVOID = ctypes.c_voidp
TCHAR = ctypes.c_char
class BROWSEINFO(ctypes.Structure):
_fields_ = [
("hwndOwner", wintypes.HWND),
("pidlRoot", LPVOID),
("pszDisplayName", LPTSTR),
("lpszTitle", LPCTSTR),
("ulFlags", ctypes.c_uint),
("lpfn", BrowseCallbackProc),
("lParam", wintypes.LPARAM),
("iImage", ctypes.c_int)
]
# Utilities
def CenterWindow(hwnd):
desktopRect = GetWindowRect(user32.GetDesktopWindow())
myRect = GetWindowRect(hwnd)
x = width(desktopRect) // 2 - width(myRect) // 2
y = height(desktopRect) // 2 - height(myRect) // 2
user32.SetWindowPos(hwnd, 0,
desktopRect.left + x,
desktopRect.top + y,
0, 0,
SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER
)
def GetWindowRect(hwnd):
rect = wintypes.RECT()
user32.GetWindowRect(hwnd, ctypes.byref(rect))
return rect
def width(rect):
return rect.right-rect.left
def height(rect):
return rect.bottom-rect.top
def AskFolder(
message=None,
version=None,
defaultLocation=None,
location=None,
windowTitle=None,
actionButtonLabel=None,
cancelButtonLabel=None,
multiple=None):
"""Display a dialog asking the user for select a folder.
modified to use unicode strings as much as possible
returns unicode path
"""
def BrowseCallback(hwnd, uMsg, lParam, lpData):
if uMsg == BFFM_INITIALIZED:
if actionButtonLabel:
label = unicode(actionButtonLabel, errors='replace')
user32.SendMessageW(hwnd, BFFM_SETOKTEXT, 0, label)
if cancelButtonLabel:
label = unicode(cancelButtonLabel, errors='replace')
cancelButton = user32.GetDlgItem(hwnd, IDCANCEL)
if cancelButton:
user32.SetWindowTextW(cancelButton, label)
if windowTitle:
title = unicode(windowTitle, erros='replace')
user32.SetWindowTextW(hwnd, title)
if defaultLocation:
user32.SendMessageW(hwnd, BFFM_SETSELECTIONW, 1, defaultLocation.replace('/', '\\'))
if location:
x, y = location
desktopRect = wintypes.RECT()
user32.GetWindowRect(0, ctypes.byref(desktopRect))
user32.SetWindowPos(hwnd, 0,
desktopRect.left + x,
desktopRect.top + y, 0, 0,
SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER)
else:
CenterWindow(hwnd)
return 0
# This next line is needed to prevent gc of the callback
callback = BrowseCallbackProc(BrowseCallback)
browseInfo = BROWSEINFO()
browseInfo.pszDisplayName = ctypes.c_char_p('\0' * (MAX_PATH+1))
browseInfo.lpszTitle = message
browseInfo.lpfn = callback
pidl = shell32.SHBrowseForFolder(ctypes.byref(browseInfo))
if not pidl:
result = None
else:
path = LPCWSTR(u" " * (MAX_PATH+1))
shell32.SHGetPathFromIDListW(pidl, path)
ole32.CoTaskMemFree(pidl)
result = path.value
return result

View File

@@ -9,16 +9,24 @@ __license__ = 'GPL v3'
import os, traceback, json
# PyQT4 modules (part of calibre).
from PyQt4.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit,
try:
from PyQt5.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit,
QGroupBox, QPushButton, QListWidget, QListWidgetItem,
QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl, QString)
from PyQt4 import QtGui
QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl)
except ImportError:
from PyQt4.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit,
QGroupBox, QPushButton, QListWidget, QListWidgetItem,
QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl)
try:
from PyQt5 import Qt as QtGui
except ImportError:
from PyQt4 import QtGui
from zipfile import ZipFile
# calibre modules and constants.
from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url,
choose_dir, choose_files)
choose_dir, choose_files, choose_save_file)
from calibre.utils.config import dynamic, config_dir, JSONConfig
from calibre.constants import iswindows, isosx
@@ -28,6 +36,7 @@ from calibre_plugins.dedrm.__init__ import RESOURCE_NAME as help_file_name
from calibre_plugins.dedrm.utilities import uStrCmp
import calibre_plugins.dedrm.prefs as prefs
import calibre_plugins.dedrm.androidkindlekey as androidkindlekey
class ConfigWidget(QWidget):
def __init__(self, plugin_path, alfdir):
@@ -45,6 +54,7 @@ class ConfigWidget(QWidget):
self.tempdedrmprefs['adeptkeys'] = self.dedrmprefs['adeptkeys'].copy()
self.tempdedrmprefs['ereaderkeys'] = self.dedrmprefs['ereaderkeys'].copy()
self.tempdedrmprefs['kindlekeys'] = self.dedrmprefs['kindlekeys'].copy()
self.tempdedrmprefs['androidkeys'] = self.dedrmprefs['androidkeys'].copy()
self.tempdedrmprefs['pids'] = list(self.dedrmprefs['pids'])
self.tempdedrmprefs['serials'] = list(self.dedrmprefs['serials'])
self.tempdedrmprefs['adobewineprefix'] = self.dedrmprefs['adobewineprefix']
@@ -75,6 +85,10 @@ class ConfigWidget(QWidget):
self.bandn_button.setToolTip(_(u"Click to manage keys for Barnes and Noble ebooks"))
self.bandn_button.setText(u"Barnes and Noble ebooks")
self.bandn_button.clicked.connect(self.bandn_keys)
self.kindle_android_button = QtGui.QPushButton(self)
self.kindle_android_button.setToolTip(_(u"Click to manage keys for Kindle for Android ebooks"))
self.kindle_android_button.setText(u"Kindle for Android ebooks")
self.kindle_android_button.clicked.connect(self.kindle_android)
self.kindle_serial_button = QtGui.QPushButton(self)
self.kindle_serial_button.setToolTip(_(u"Click to manage eInk Kindle serial numbers for Kindle ebooks"))
self.kindle_serial_button.setText(u"eInk Kindle ebooks")
@@ -96,6 +110,7 @@ class ConfigWidget(QWidget):
self.ereader_button.setText(u"eReader ebooks")
self.ereader_button.clicked.connect(self.ereader_keys)
button_layout.addWidget(self.kindle_serial_button)
button_layout.addWidget(self.kindle_android_button)
button_layout.addWidget(self.bandn_button)
button_layout.addWidget(self.mobi_button)
button_layout.addWidget(self.ereader_button)
@@ -107,6 +122,10 @@ class ConfigWidget(QWidget):
def kindle_serials(self):
d = ManageKeysDialog(self,u"EInk Kindle Serial Number",self.tempdedrmprefs['serials'], AddSerialDialog)
d.exec_()
def kindle_android(self):
d = ManageKeysDialog(self,u"Kindle for Android Key",self.tempdedrmprefs['androidkeys'], AddAndroidDialog, 'k4a')
d.exec_()
def kindle_keys(self):
if isosx or iswindows:
@@ -154,6 +173,7 @@ class ConfigWidget(QWidget):
self.dedrmprefs.set('adeptkeys', self.tempdedrmprefs['adeptkeys'])
self.dedrmprefs.set('ereaderkeys', self.tempdedrmprefs['ereaderkeys'])
self.dedrmprefs.set('kindlekeys', self.tempdedrmprefs['kindlekeys'])
self.dedrmprefs.set('androidkeys', self.tempdedrmprefs['androidkeys'])
self.dedrmprefs.set('pids', self.tempdedrmprefs['pids'])
self.dedrmprefs.set('serials', self.tempdedrmprefs['serials'])
self.dedrmprefs.set('adobewineprefix', self.tempdedrmprefs['adobewineprefix'])
@@ -180,6 +200,7 @@ class ManageKeysDialog(QDialog):
self.import_key = (keyfile_ext != u"")
self.binary_file = (keyfile_ext == u"der")
self.json_file = (keyfile_ext == u"k4i")
self.android_file = (keyfile_ext == u"k4a")
self.wineprefix = wineprefix
self.setWindowTitle("{0} {1}: Manage {2}s".format(PLUGIN_NAME, PLUGIN_VERSION, self.key_type_name))
@@ -211,8 +232,8 @@ class ManageKeysDialog(QDialog):
button_layout = QVBoxLayout()
keys_group_box_layout.addLayout(button_layout)
self._add_key_button = QtGui.QToolButton(self)
self._add_key_button.setToolTip(u"Create new {0}".format(self.key_type_name))
self._add_key_button.setIcon(QIcon(I('plus.png')))
self._add_key_button.setToolTip(u"Create new {0}".format(self.key_type_name))
self._add_key_button.clicked.connect(self.add_key)
button_layout.addWidget(self._add_key_button)
@@ -267,7 +288,7 @@ class ManageKeysDialog(QDialog):
def getwineprefix(self):
if self.wineprefix is not None:
return unicode(self.wp_lineedit.text().toUtf8(), 'utf8').strip()
return unicode(self.wp_lineedit.text()).strip()
return u""
def populate_list(self):
@@ -316,7 +337,7 @@ class ManageKeysDialog(QDialog):
if d.result() != d.Accepted:
# rename cancelled or moot.
return
keyname = unicode(self.listy.currentItem().text().toUtf8(),'utf8')
keyname = unicode(self.listy.currentItem().text())
if not question_dialog(self, "{0} {1}: Confirm Rename".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to rename the {2} named <strong>{0}</strong> to <strong>{1}</strong>?".format(keyname,d.key_name,self.key_type_name), show_copy_button=False, default_yes=False):
return
self.plugin_keys[d.key_name] = self.plugin_keys[keyname]
@@ -328,7 +349,7 @@ class ManageKeysDialog(QDialog):
def delete_key(self):
if not self.listy.currentItem():
return
keyname = unicode(self.listy.currentItem().text().toUtf8(), 'utf8')
keyname = unicode(self.listy.currentItem().text())
if not question_dialog(self, "{0} {1}: Confirm Delete".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to delete the {1} <strong>{0}</strong>?".format(keyname, self.key_type_name), show_copy_button=False, default_yes=False):
return
if type(self.plugin_keys) == dict:
@@ -352,9 +373,10 @@ class ManageKeysDialog(QDialog):
open_url(QUrl(url))
def migrate_files(self):
dynamic[PLUGIN_NAME + u"config_dir"] = config_dir
files = choose_files(self, PLUGIN_NAME + u"config_dir",
u"Select {0} files to import".format(self.key_type_name), [(u"{0} files".format(self.key_type_name), [self.keyfile_ext])], False)
unique_dlg_name = PLUGIN_NAME + u"import {0} keys".format(self.key_type_name).replace(' ', '_') #takes care of automatically remembering last directory
caption = u"Select {0} files to import".format(self.key_type_name)
filters = [(u"{0} files".format(self.key_type_name), [self.keyfile_ext])]
files = choose_files(self, unique_dlg_name, caption, filters, all_files=False)
counter = 0
skipped = 0
if files:
@@ -368,6 +390,9 @@ class ManageKeysDialog(QDialog):
new_key_value = new_key_value.encode('hex')
elif self.json_file:
new_key_value = json.loads(new_key_value)
elif self.android_file:
# convert to list of the keys in the string
new_key_value = new_key_value.splitlines()
match = False
for key in self.plugin_keys.keys():
if uStrCmp(new_key_name, key, True):
@@ -386,7 +411,7 @@ class ManageKeysDialog(QDialog):
else:
counter += 1
self.plugin_keys[new_key_name] = new_key_value
msg = u""
if counter+skipped > 1:
if counter > 0:
@@ -408,21 +433,22 @@ class ManageKeysDialog(QDialog):
r = error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
_(errmsg), show=True, show_copy_button=False)
return
filter = QString(u"{0} Files (*.{1})".format(self.key_type_name, self.keyfile_ext))
keyname = unicode(self.listy.currentItem().text().toUtf8(), 'utf8')
if dynamic.get(PLUGIN_NAME + 'save_dir'):
defaultname = os.path.join(dynamic.get(PLUGIN_NAME + 'save_dir'), u"{0}.{1}".format(keyname , self.keyfile_ext))
else:
defaultname = os.path.join(os.path.expanduser('~'), u"{0}.{1}".format(keyname , self.keyfile_ext))
filename = unicode(QtGui.QFileDialog.getSaveFileName(self, u"Save {0} File as...".format(self.key_type_name), defaultname,
u"{0} Files (*.{1})".format(self.key_type_name,self.keyfile_ext), filter))
keyname = unicode(self.listy.currentItem().text())
unique_dlg_name = PLUGIN_NAME + u"export {0} keys".format(self.key_type_name).replace(' ', '_') #takes care of automatically remembering last directory
caption = u"Save {0} File as...".format(self.key_type_name)
filters = [(u"{0} Files".format(self.key_type_name), [u"{0}".format(self.keyfile_ext)])]
defaultname = u"{0}.{1}".format(keyname, self.keyfile_ext)
filename = choose_save_file(self, unique_dlg_name, caption, filters, all_files=False, initial_filename=defaultname)
if filename:
dynamic[PLUGIN_NAME + 'save_dir'] = os.path.split(filename)[0]
with file(filename, 'w') as fname:
with file(filename, 'wb') as fname:
if self.binary_file:
fname.write(self.plugin_keys[keyname].decode('hex'))
elif self.json_file:
fname.write(json.dumps(self.plugin_keys[keyname]))
elif self.android_file:
for key in self.plugin_keys[keyname]:
fname.write(key)
fname.write("\n")
else:
fname.write(self.plugin_keys[keyname])
@@ -458,7 +484,7 @@ class RenameKeyDialog(QDialog):
self.resize(self.sizeHint())
def accept(self):
if self.key_ledit.text().isEmpty() or unicode(self.key_ledit.text()).isspace():
if not unicode(self.key_ledit.text()) or unicode(self.key_ledit.text()).isspace():
errmsg = u"Key name field cannot be empty!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
_(errmsg), show=True, show_copy_button=False)
@@ -479,7 +505,7 @@ class RenameKeyDialog(QDialog):
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
@@ -509,20 +535,17 @@ class AddBandNKeyDialog(QDialog):
u"<p>It should be something that will help you remember " +
u"what personal information was used to create it."))
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
name_group = QHBoxLayout()
data_group_box_layout.addLayout(name_group)
name_group.addWidget(QLabel(u"Your Name:", self))
name_group.addWidget(QLabel(u"B&N/nook account email address:", self))
self.name_ledit = QLineEdit(u"", self)
self.name_ledit.setToolTip(_(u"<p>Enter your name as it appears in your B&N " +
u"account or on your credit card.</p>" +
self.name_ledit.setToolTip(_(u"<p>Enter your email address as it appears in your B&N " +
u"account.</p>" +
u"<p>It will only be used to generate this " +
u"one-time key and won\'t be stored anywhere " +
u"key and won\'t be stored anywhere " +
u"in calibre or on your computer.</p>" +
u"<p>(ex: Jonathan Smith)"))
u"<p>eg: apprenticeharper@gmail.com</p>"))
name_group.addWidget(self.name_ledit)
name_disclaimer_label = QLabel(_(u"(Will not be saved in configuration data)"), self)
name_disclaimer_label.setAlignment(Qt.AlignHCenter)
@@ -530,13 +553,12 @@ class AddBandNKeyDialog(QDialog):
ccn_group = QHBoxLayout()
data_group_box_layout.addLayout(ccn_group)
ccn_group.addWidget(QLabel(u"Credit Card#:", self))
ccn_group.addWidget(QLabel(u"B&N/nook account password:", self))
self.cc_ledit = QLineEdit(u"", self)
self.cc_ledit.setToolTip(_(u"<p>Enter the full credit card number on record " +
u"in your B&N account.</p>" +
u"<p>No spaces or dashes... just the numbers. " +
u"This number will only be used to generate this " +
u"one-time key and won\'t be stored anywhere in " +
self.cc_ledit.setToolTip(_(u"<p>Enter the password " +
u"for your B&N account.</p>" +
u"<p>The password will only be used to generate this " +
u"key and won\'t be stored anywhere in " +
u"calibre or on your computer."))
ccn_group.addWidget(self.cc_ledit)
ccn_disclaimer_label = QLabel(_('(Will not be saved in configuration data)'), self)
@@ -553,29 +575,26 @@ class AddBandNKeyDialog(QDialog):
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
@property
def key_value(self):
from calibre_plugins.dedrm.ignoblekeygen import generate_key as generate_bandn_key
return generate_bandn_key(self.user_name,self.cc_number)
from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key
return fetch_bandn_key(self.user_name,self.cc_number)
@property
def user_name(self):
return unicode(self.name_ledit.text().toUtf8(), 'utf8').strip().lower().replace(' ','')
return unicode(self.name_ledit.text()).strip().lower().replace(' ','')
@property
def cc_number(self):
return unicode(self.cc_ledit.text().toUtf8(), 'utf8').strip().replace(' ', '').replace('-','')
return unicode(self.cc_ledit.text()).strip()
def accept(self):
if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace():
errmsg = u"All fields are required!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if not self.cc_number.isdigit():
errmsg = u"Numbers only in the credit card number field!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) < 4:
errmsg = u"Key name must be at <i>least</i> 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
@@ -600,9 +619,6 @@ class AddEReaderDialog(QDialog):
self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(u"<p>Enter an identifying name for this new key.\nIt should be something that will help you remember what personal information was used to create it.")
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
name_group = QHBoxLayout()
data_group_box_layout.addLayout(name_group)
@@ -634,7 +650,7 @@ class AddEReaderDialog(QDialog):
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
@property
def key_value(self):
@@ -643,11 +659,11 @@ class AddEReaderDialog(QDialog):
@property
def user_name(self):
return unicode(self.name_ledit.text().toUtf8(), 'utf8').strip().lower().replace(' ','')
return unicode(self.name_ledit.text()).strip().lower().replace(' ','')
@property
def cc_number(self):
return unicode(self.cc_ledit.text().toUtf8(), 'utf8').strip().replace(' ', '').replace('-','')
return unicode(self.cc_ledit.text()).strip().replace(' ', '').replace('-','')
def accept(self):
@@ -701,9 +717,7 @@ class AddAdeptDialog(QDialog):
self.key_ledit = QLineEdit(u"default_key", self)
self.key_ledit.setToolTip(u"<p>Enter an identifying name for the current default Adobe Digital Editions key.")
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
self.button_box.accepted.connect(self.accept)
else:
default_key_error = QLabel(u"The default encryption key for Adobe Digital Editions could not be found.", self)
@@ -719,7 +733,7 @@ class AddAdeptDialog(QDialog):
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
@property
def key_value(self):
@@ -774,15 +788,14 @@ class AddKindleDialog(QDialog):
self.key_ledit = QLineEdit(u"default_key", self)
self.key_ledit.setToolTip(u"<p>Enter an identifying name for the current default Kindle for Mac/PC key.")
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
self.button_box.accepted.connect(self.accept)
else:
default_key_error = QLabel(u"The default encryption key for Kindle for Mac/PC could not be found.", self)
default_key_error.setAlignment(Qt.AlignHCenter)
layout.addWidget(default_key_error)
# if no default, bot buttons do the same
# if no default, both buttons do the same
self.button_box.accepted.connect(self.reject)
self.button_box.rejected.connect(self.reject)
@@ -792,7 +805,7 @@ class AddKindleDialog(QDialog):
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
@property
def key_value(self):
@@ -828,9 +841,6 @@ class AddSerialDialog(QDialog):
self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(u"Enter an eInk Kindle serial number. EInk Kindle serial numbers are 16 characters long and usually start with a 'B' or a '9'. Kindle Serial Numbers are case-sensitive, so be sure to enter the upper and lower case letters unchanged.")
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
@@ -841,11 +851,11 @@ class AddSerialDialog(QDialog):
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
@property
def key_value(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
def accept(self):
if len(self.key_name) == 0 or self.key_name.isspace():
@@ -857,6 +867,89 @@ class AddSerialDialog(QDialog):
QDialog.accept(self)
class AddAndroidDialog(QDialog):
def __init__(self, parent=None,):
QDialog.__init__(self, parent)
self.parent = parent
self.setWindowTitle(u"{0} {1}: Add new Kindle for Android Key".format(PLUGIN_NAME, PLUGIN_VERSION))
layout = QVBoxLayout(self)
self.setLayout(layout)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
data_group_box = QGroupBox(u"", self)
layout.addWidget(data_group_box)
data_group_box_layout = QVBoxLayout()
data_group_box.setLayout(data_group_box_layout)
file_group = QHBoxLayout()
data_group_box_layout.addLayout(file_group)
add_btn = QPushButton(u"Choose Backup File", self)
add_btn.setToolTip(u"Import Kindle for Android backup file.")
add_btn.clicked.connect(self.get_android_file)
file_group.addWidget(add_btn)
self.selected_file_name = QLabel(u"",self)
self.selected_file_name.setAlignment(Qt.AlignHCenter)
file_group.addWidget(self.selected_file_name)
key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel(u"Unique Key Name:", self))
self.key_ledit = QLineEdit(u"", self)
self.key_ledit.setToolTip(u"<p>Enter an identifying name for the Android for Kindle key.")
key_group.addWidget(self.key_ledit)
#key_label = QLabel(_(''), self)
#key_label.setAlignment(Qt.AlignHCenter)
#data_group_box_layout.addWidget(key_label)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
layout.addWidget(self.button_box)
self.resize(self.sizeHint())
@property
def key_name(self):
return unicode(self.key_ledit.text()).strip()
@property
def file_name(self):
return unicode(self.selected_file_name.text()).strip()
@property
def key_value(self):
return self.serials_from_file
def get_android_file(self):
unique_dlg_name = PLUGIN_NAME + u"Import Kindle for Android backup file" #takes care of automatically remembering last directory
caption = u"Select Kindle for Android backup file to add"
filters = [(u"Kindle for Android backup files", ['db','ab','xml'])]
files = choose_files(self, unique_dlg_name, caption, filters, all_files=False)
self.serials_from_file = []
file_name = u""
if files:
# find the first selected file that yields some serial numbers
for filename in files:
fpath = os.path.join(config_dir, filename)
self.filename = os.path.basename(filename)
file_serials = androidkindlekey.get_serials(fpath)
if len(file_serials)>0:
file_name = os.path.basename(self.filename)
self.serials_from_file.extend(file_serials)
self.selected_file_name.setText(file_name)
def accept(self):
if len(self.file_name) == 0 or len(self.key_value) == 0:
errmsg = u"Please choose a Kindle for Android backup file."
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) == 0 or self.key_name.isspace():
errmsg = u"Please enter a key name."
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) < 4:
errmsg = u"Key name must be at <i>least</i> 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
QDialog.accept(self)
class AddPIDDialog(QDialog):
def __init__(self, parent=None,):
QDialog.__init__(self, parent)
@@ -876,9 +969,6 @@ class AddPIDDialog(QDialog):
self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(u"Enter a Mobipocket PID. Mobipocket PIDs are 8 or 10 characters long. Mobipocket PIDs are case-sensitive, so be sure to enter the upper and lower case letters unchanged.")
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
@@ -889,11 +979,11 @@ class AddPIDDialog(QDialog):
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
@property
def key_value(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
return unicode(self.key_ledit.text()).strip()
def accept(self):
if len(self.key_name) == 0 or self.key_name.isspace():

View File

@@ -1,45 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# base64.py, version 1.0
# Copyright © 2010 Apprentice Alf
# Released under the terms of the GNU General Public Licence, version 3 or
# later. <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())

View File

@@ -1,208 +1,45 @@
#!/usr/bin/python
#
# This is a python script. You need a Python interpreter to run it.
# For example, ActiveState Python, which exists for windows.
#
# Changelog drmcheck
# 1.00 - Initial version, with code from various other scripts
# 1.01 - Moved authorship announcement to usage section.
#
# Changelog epubtest
# 1.00 - Cut to epubtest.py, testing ePub files only by Apprentice Alf
# 1.01 - Added routine for use by Windows DeDRM
#
# Written in 2011 by Paul Durrant
# Released with unlicense. See http://unlicense.org/
#
#############################################################################
#
# This is free and unencumbered software released into the public domain.
#
# Anyone is free to copy, modify, publish, use, compile, sell, or
# distribute this software, either in source code form or as a compiled
# binary, for any purpose, commercial or non-commercial, and by any
# means.
#
# In jurisdictions that recognize copyright laws, the author or authors
# of this software dedicate any and all copyright interest in the
# software to the public domain. We make this dedication for the benefit
# of the public at large and to the detriment of our heirs and
# successors. We intend this dedication to be an overt act of
# relinquishment in perpetuity of all present and future rights to this
# software under copyright law.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
#############################################################################
#
# It's still polite to give attribution if you do reuse this code.
#
#!/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
__version__ = '1.01'
__license__ = 'GPL v3'
import sys, struct, os
import zlib
import zipfile
import xml.etree.ElementTree as etree
import sys
import os
import base64
NSMAP = {'adept': 'http://ns.adobe.com/adept',
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
def usage(progname):
print "Applies base64 encoding to the supplied file, sending to standard output"
print "Usage:"
print " %s <infile>" % progname
# 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 cli_main(argv=sys.argv):
progname = os.path.basename(argv[0])
try:
from calibre.constants import iswindows, isosx
except:
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
if len(argv)<2:
usage(progname)
sys.exit(2)
def unicode_argv():
if iswindows:
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
# as a list of Unicode strings and encode them as utf-8
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"epubtest.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
_FILENAME_LEN_OFFSET = 26
_EXTRA_LEN_OFFSET = 28
_FILENAME_OFFSET = 30
_MAX_SIZE = 64 * 1024
def uncompress(cmpdata):
dc = zlib.decompressobj(-15)
data = ''
while len(cmpdata) > 0:
if len(cmpdata) > _MAX_SIZE :
newdata = cmpdata[0:_MAX_SIZE]
cmpdata = cmpdata[_MAX_SIZE:]
else:
newdata = cmpdata
cmpdata = ''
newdata = dc.decompress(newdata)
unprocessed = dc.unconsumed_tail
if len(unprocessed) == 0:
newdata += dc.flush()
data += newdata
cmpdata += unprocessed
unprocessed = ''
return data
def getfiledata(file, zi):
# get file name length and exta data length to find start of file data
local_header_offset = zi.header_offset
file.seek(local_header_offset + _FILENAME_LEN_OFFSET)
leninfo = file.read(2)
local_name_length, = struct.unpack('<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 encryption(infile):
# returns encryption: one of Unencrypted, Adobe, B&N and Unknown
encryption = "Unknown"
try:
with open(infile,'rb') as infileobject:
bookdata = infileobject.read(58)
# Check for Zip
if bookdata[0:0+2] == "PK":
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"
except:
traceback.print_exc()
return encryption
def main():
argv=unicode_argv()
print encryption(argv[1])
keypath = argv[1]
with open(keypath, 'rb') as f:
keyder = f.read()
print keyder.encode('base64')
return 0
if __name__ == "__main__":
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
sys.exit(main())
if __name__ == '__main__':
sys.exit(cli_main())

View File

@@ -1,82 +1,60 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# erdr2pml.py
# Copyright © 2008 The Dark Reverser
#!/usr/bin/python
#
# Modified 20082012 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
#
# Based on ereader2html version 0.08 plus some later small fixes
# Changelog drmcheck
# 1.00 - Initial version, with code from various other scripts
# 1.01 - Moved authorship announcement to usage section.
#
# Changelog epubtest
# 1.00 - Cut to epubtest.py, testing ePub files only by Apprentice Alf
# 1.01 - Added routine for use by Windows DeDRM
#
# Written in 2011 by Paul Durrant
# Released with unlicense. See http://unlicense.org/
#
#############################################################################
#
# This is free and unencumbered software released into the public domain.
#
# Anyone is free to copy, modify, publish, use, compile, sell, or
# distribute this software, either in source code form or as a compiled
# binary, for any purpose, commercial or non-commercial, and by any
# means.
#
# In jurisdictions that recognize copyright laws, the author or authors
# of this software dedicate any and all copyright interest in the
# software to the public domain. We make this dedication for the benefit
# of the public at large and to the detriment of our heirs and
# successors. We intend this dedication to be an overt act of
# relinquishment in perpetuity of all present and future rights to this
# software under copyright law.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
#############################################################################
#
# It's still polite to give attribution if you do reuse this code.
#
# 0.01 - Initial version
# 0.02 - Support more eReader files. Support bold text and links. Fix PML decoder parsing bug.
# 0.03 - Fix incorrect variable usage at one place.
# 0.03b - enhancement by DeBockle (version 259 support)
# 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,
# 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
# - Psyco support - reduces runtime by a factor of (over) 3!
# E.g. (~600Kb file) 90 secs down to 24 secs
# - newstyle classes
# - changed map call to list comprehension
# may not work with python 2.3
# without Psyco this reduces runtime to 90%
# E.g. 90 secs down to 77 secs
# Psyco with map calls takes longer, do not run with map in Psyco JIT!
# - izip calls used instead of zip (if available), further reduction
# in run time (factor of 4.5).
# E.g. (~600Kb file) 90 secs down to 20 secs
# - Python 2.6+ support, avoid DeprecationWarning with sha/sha1
# 0.04 - Footnote support, PML output, correct charset in html, support more PML tags
# - Feature change, dump out PML file
# - Added supprt for footnote tags. NOTE footnote ids appear to be bad (not usable)
# in some pdb files :-( due to the same id being used multiple times
# - Added correct charset encoding (pml is based on cp1252)
# - Added logging support.
# 0.05 - Improved type 272 support for sidebars, links, chapters, metainfo, etc
# 0.06 - Merge of 0.04 and 0.05. Improved HTML output
# Placed images in subfolder, so that it's possible to just
# drop the book.pml file onto DropBook to make an unencrypted
# copy of the eReader file.
# Using that with Calibre works a lot better than the HTML
# conversion in this code.
# 0.07 - Further Improved type 272 support for sidebars with all earlier fixes
# 0.08 - fixed typos, removed extraneous things
# 0.09 - fixed typos in first_pages to first_page to again support older formats
# 0.10 - minor cleanups
# 0.11 - fixups for using correct xml for footnotes and sidebars for use with Dropbook
# 0.12 - Fix added to prevent lowercasing of image names when the pml code itself uses a different case in the link name.
# 0.13 - change to unbuffered stdout for use with gui front ends
# 0.14 - contributed enhancement to support --make-pmlz switch
# 0.15 - enabled high-ascii to pml character encoding. DropBook now works on Mac.
# 0.16 - convert to use openssl DES (very very fast) or pure python DES if openssl's libcrypto is not available
# 0.17 - added support for pycrypto's DES as well
# 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.
# - Ignore sidebars for dictionaries (different format?)
# 0.22 - Unicode and plugin support, different image folders for PMLZ and source
# 0.23 - moved unicode_argv call inside main for Windows DeDRM compatibility
__version__='0.23'
from __future__ import with_statement
import sys, re
import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile, traceback
__version__ = '1.01'
if 'calibre' in sys.modules:
inCalibre = True
else:
inCalibre = False
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
@@ -95,8 +73,11 @@ class SafeUnbuffered:
def __getattr__(self, attr):
return getattr(self.stream, attr)
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
try:
from calibre.constants import iswindows, isosx
except:
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
def unicode_argv():
if iswindows:
@@ -105,8 +86,8 @@ def unicode_argv():
# 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 '?'.
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
# as a list of Unicode strings and encode them as utf-8
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
@@ -129,469 +110,99 @@ def unicode_argv():
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"]
return [u"epubtest.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
Des = None
if iswindows:
# first try with pycrypto
if inCalibre:
from calibre_plugins.dedrm import pycrypto_des
else:
import pycrypto_des
Des = pycrypto_des.load_pycrypto()
if Des == None:
# they try with openssl
if inCalibre:
from calibre_plugins.dedrm import openssl_des
else:
import openssl_des
Des = openssl_des.load_libcrypto()
else:
# first try with openssl
if inCalibre:
from calibre_plugins.dedrm import openssl_des
else:
import openssl_des
Des = openssl_des.load_libcrypto()
if Des == None:
# then try with pycrypto
if inCalibre:
from calibre_plugins.dedrm import pycrypto_des
else:
import pycrypto_des
Des = pycrypto_des.load_pycrypto()
_FILENAME_LEN_OFFSET = 26
_EXTRA_LEN_OFFSET = 28
_FILENAME_OFFSET = 30
_MAX_SIZE = 64 * 1024
# if that did not work then use pure python implementation
# of DES and try to speed it up with Psycho
if Des == None:
if inCalibre:
from calibre_plugins.dedrm import python_des
else:
import python_des
Des = python_des.Des
# Import Psyco if available
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 encryption(infile):
# returns encryption: one of Unencrypted, Adobe, B&N and Unknown
encryption = "Unknown"
try:
# http://psyco.sourceforge.net
import psyco
psyco.full()
except ImportError:
pass
try:
from hashlib import sha1
except ImportError:
# older Python release
import sha
sha1 = lambda s: sha.new(s)
import cgi
import logging
logging.basicConfig()
#logging.basicConfig(level=logging.DEBUG)
class Sectionizer(object):
bkType = "Book"
def __init__(self, filename, ident):
self.contents = file(filename, 'rb').read()
self.header = self.contents[0:72]
self.num_sections, = struct.unpack('>H', self.contents[76:78])
# Dictionary or normal content (TODO: Not hard-coded)
if self.header[0x3C:0x3C+8] != ident:
if self.header[0x3C:0x3C+8] == "PDctPPrs":
self.bkType = "Dict"
else:
raise ValueError('Invalid file format')
self.sections = []
for i in xrange(self.num_sections):
offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', self.contents[78+i*8:78+i*8+8])
flags, val = a1, a2<<16|a3<<8|a4
self.sections.append( (offset, flags, val) )
def loadSection(self, section):
if section + 1 == self.num_sections:
end_off = len(self.contents)
else:
end_off = self.sections[section + 1][0]
off = self.sections[section][0]
return self.contents[off:end_off]
# cleanup unicode filenames
# borrowed from calibre from calibre/src/calibre/__init__.py
# added in removal of control (<32) chars
# and removal of . at start and end
# and with some (heavily edited) code from Paul Durrant's kindlenamer.py
def sanitizeFileName(name):
# substitute filename unfriendly characters
name = name.replace(u"<",u"[").replace(u">",u"]").replace(u" : ",u" ").replace(u": ",u" ").replace(u":",u"").replace(u"/",u"_").replace(u"\\",u"_").replace(u"|",u"_").replace(u"\"",u"\'")
# delete control characters
name = u"".join(char for char in name if ord(char)>=32)
# white space to single space, delete leading and trailing while space
name = re.sub(ur"\s", u" ", name).strip()
# remove leading dots
while len(name)>0 and name[0] == u".":
name = name[1:]
# remove trailing dots (Windows doesn't like them)
if name.endswith(u'.'):
name = name[:-1]
return name
def fixKey(key):
def fixByte(b):
return b ^ ((b ^ (b<<1) ^ (b<<2) ^ (b<<3) ^ (b<<4) ^ (b<<5) ^ (b<<6) ^ (b<<7) ^ 0x80) & 0x80)
return "".join([chr(fixByte(ord(a))) for a in key])
def deXOR(text, sp, table):
r=''
j = sp
for i in xrange(len(text)):
r += chr(ord(table[j]) ^ ord(text[i]))
j = j + 1
if j == len(table):
j = 0
return r
class EreaderProcessor(object):
def __init__(self, sect, user_key):
self.section_reader = sect.loadSection
data = self.section_reader(0)
version, = struct.unpack('>H', data[0:2])
self.version = version
logging.info('eReader file format version %s', version)
if version != 272 and version != 260 and version != 259:
raise ValueError('incorrect eReader version %d (error 1)' % version)
data = self.section_reader(1)
self.data = data
des = Des(fixKey(data[0:8]))
cookie_shuf, cookie_size = struct.unpack('>LL', des.decrypt(data[-8:]))
if cookie_shuf < 3 or cookie_shuf > 0x14 or cookie_size < 0xf0 or cookie_size > 0x200:
raise ValueError('incorrect eReader version (error 2)')
input = des.decrypt(data[-cookie_size:])
def unshuff(data, shuf):
r = [''] * len(data)
j = 0
for i in xrange(len(data)):
j = (j + shuf) % len(data)
r[j] = data[i]
assert len("".join(r)) == len(data)
return "".join(r)
r = unshuff(input[0:-8], cookie_shuf)
drm_sub_version = struct.unpack('>H', r[0:2])[0]
self.num_text_pages = struct.unpack('>H', r[2:4])[0] - 1
self.num_image_pages = struct.unpack('>H', r[26:26+2])[0]
self.first_image_page = struct.unpack('>H', r[24:24+2])[0]
# Default values
self.num_footnote_pages = 0
self.num_sidebar_pages = 0
self.first_footnote_page = -1
self.first_sidebar_page = -1
if self.version == 272:
self.num_footnote_pages = struct.unpack('>H', r[46:46+2])[0]
self.first_footnote_page = struct.unpack('>H', r[44:44+2])[0]
if (sect.bkType == "Book"):
self.num_sidebar_pages = struct.unpack('>H', r[38:38+2])[0]
self.first_sidebar_page = struct.unpack('>H', r[36:36+2])[0]
# self.num_bookinfo_pages = struct.unpack('>H', r[34:34+2])[0]
# self.first_bookinfo_page = struct.unpack('>H', r[32:32+2])[0]
# self.num_chapter_pages = struct.unpack('>H', r[22:22+2])[0]
# self.first_chapter_page = struct.unpack('>H', r[20:20+2])[0]
# self.num_link_pages = struct.unpack('>H', r[30:30+2])[0]
# self.first_link_page = struct.unpack('>H', r[28:28+2])[0]
# self.num_xtextsize_pages = struct.unpack('>H', r[54:54+2])[0]
# self.first_xtextsize_page = struct.unpack('>H', r[52:52+2])[0]
# **before** data record 1 was decrypted and unshuffled, it contained data
# to create an XOR table and which is used to fix footnote record 0, link records, chapter records, etc
self.xortable_offset = struct.unpack('>H', r[40:40+2])[0]
self.xortable_size = struct.unpack('>H', r[42:42+2])[0]
self.xortable = self.data[self.xortable_offset:self.xortable_offset + self.xortable_size]
else:
# Nothing needs to be done
pass
# self.num_bookinfo_pages = 0
# self.num_chapter_pages = 0
# self.num_link_pages = 0
# self.num_xtextsize_pages = 0
# self.first_bookinfo_page = -1
# self.first_chapter_page = -1
# self.first_link_page = -1
# self.first_xtextsize_page = -1
logging.debug('self.num_text_pages %d', self.num_text_pages)
logging.debug('self.num_footnote_pages %d, self.first_footnote_page %d', self.num_footnote_pages , self.first_footnote_page)
logging.debug('self.num_sidebar_pages %d, self.first_sidebar_page %d', self.num_sidebar_pages , self.first_sidebar_page)
self.flags = struct.unpack('>L', r[4:8])[0]
reqd_flags = (1<<9) | (1<<7) | (1<<10)
if (self.flags & reqd_flags) != reqd_flags:
print "Flags: 0x%X" % self.flags
raise ValueError('incompatible eReader file')
des = Des(fixKey(user_key))
if version == 259:
if drm_sub_version != 7:
raise ValueError('incorrect eReader version %d (error 3)' % drm_sub_version)
encrypted_key_sha = r[44:44+20]
encrypted_key = r[64:64+8]
elif version == 260:
if drm_sub_version != 13 and drm_sub_version != 11:
raise ValueError('incorrect eReader version %d (error 3)' % drm_sub_version)
if drm_sub_version == 13:
encrypted_key = r[44:44+8]
encrypted_key_sha = r[52:52+20]
else:
encrypted_key = r[64:64+8]
encrypted_key_sha = r[44:44+20]
elif version == 272:
encrypted_key = r[172:172+8]
encrypted_key_sha = r[56:56+20]
self.content_key = des.decrypt(encrypted_key)
if sha1(self.content_key).digest() != encrypted_key_sha:
raise ValueError('Incorrect Name and/or Credit Card')
def getNumImages(self):
return self.num_image_pages
def getImage(self, i):
sect = self.section_reader(self.first_image_page + i)
name = sect[4:4+32].strip('\0')
data = sect[62:]
return sanitizeFileName(unicode(name,'windows-1252')), data
# def getChapterNamePMLOffsetData(self):
# cv = ''
# if self.num_chapter_pages > 0:
# for i in xrange(self.num_chapter_pages):
# chaps = self.section_reader(self.first_chapter_page + i)
# j = i % self.xortable_size
# offname = deXOR(chaps, j, self.xortable)
# offset = struct.unpack('>L', offname[0:4])[0]
# name = offname[4:].strip('\0')
# cv += '%d|%s\n' % (offset, name)
# return cv
# def getLinkNamePMLOffsetData(self):
# lv = ''
# if self.num_link_pages > 0:
# for i in xrange(self.num_link_pages):
# links = self.section_reader(self.first_link_page + i)
# j = i % self.xortable_size
# offname = deXOR(links, j, self.xortable)
# offset = struct.unpack('>L', offname[0:4])[0]
# name = offname[4:].strip('\0')
# lv += '%d|%s\n' % (offset, name)
# return lv
# def getExpandedTextSizesData(self):
# ts = ''
# if self.num_xtextsize_pages > 0:
# tsize = deXOR(self.section_reader(self.first_xtextsize_page), 0, self.xortable)
# for i in xrange(self.num_text_pages):
# xsize = struct.unpack('>H', tsize[0:2])[0]
# ts += "%d\n" % xsize
# tsize = tsize[2:]
# return ts
# def getBookInfo(self):
# bkinfo = ''
# if self.num_bookinfo_pages > 0:
# info = self.section_reader(self.first_bookinfo_page)
# bkinfo = deXOR(info, 0, self.xortable)
# bkinfo = bkinfo.replace('\0','|')
# bkinfo += '\n'
# return bkinfo
def getText(self):
des = Des(fixKey(self.content_key))
r = ''
for i in xrange(self.num_text_pages):
logging.debug('get page %d', i)
r += zlib.decompress(des.decrypt(self.section_reader(1 + i)))
# now handle footnotes pages
if self.num_footnote_pages > 0:
r += '\n'
# the record 0 of the footnote section must pass through the Xor Table to make it useful
sect = self.section_reader(self.first_footnote_page)
fnote_ids = deXOR(sect, 0, self.xortable)
# the remaining records of the footnote sections need to be decoded with the content_key and zlib inflated
des = Des(fixKey(self.content_key))
for i in xrange(1,self.num_footnote_pages):
logging.debug('get footnotepage %d', i)
id_len = ord(fnote_ids[2])
id = fnote_ids[3:3+id_len]
fmarker = '<footnote id="%s">\n' % id
fmarker += zlib.decompress(des.decrypt(self.section_reader(self.first_footnote_page + i)))
fmarker += '\n</footnote>\n'
r += fmarker
fnote_ids = fnote_ids[id_len+4:]
# TODO: Handle dictionary index (?) pages - which are also marked as
# sidebar_pages (?). For now dictionary sidebars are ignored
# For dictionaries - record 0 is null terminated strings, followed by
# blocks of around 62000 bytes and a final block. Not sure of the
# encoding
# now handle sidebar pages
if self.num_sidebar_pages > 0:
r += '\n'
# the record 0 of the sidebar section must pass through the Xor Table to make it useful
sect = self.section_reader(self.first_sidebar_page)
sbar_ids = deXOR(sect, 0, self.xortable)
# the remaining records of the sidebar sections need to be decoded with the content_key and zlib inflated
des = Des(fixKey(self.content_key))
for i in xrange(1,self.num_sidebar_pages):
id_len = ord(sbar_ids[2])
id = sbar_ids[3:3+id_len]
smarker = '<sidebar id="%s">\n' % id
smarker += zlib.decompress(des.decrypt(self.section_reader(self.first_sidebar_page + i)))
smarker += '\n</sidebar>\n'
r += smarker
sbar_ids = sbar_ids[id_len+4:]
return r
def cleanPML(pml):
# Convert special characters to proper PML code. High ASCII start at (\x80, \a128) and go up to (\xff, \a255)
pml2 = pml
for k in xrange(128,256):
badChar = chr(k)
pml2 = pml2.replace(badChar, '\\a%03d' % k)
return pml2
def decryptBook(infile, outpath, make_pmlz, user_key):
bookname = os.path.splitext(os.path.basename(infile))[0]
if make_pmlz:
# outpath is actually pmlz name
pmlzname = outpath
outdir = tempfile.mkdtemp()
imagedirpath = os.path.join(outdir,u"images")
else:
pmlzname = None
outdir = outpath
imagedirpath = os.path.join(outdir,bookname + u"_img")
try:
if not os.path.exists(outdir):
os.makedirs(outdir)
print u"Decoding File"
sect = Sectionizer(infile, 'PNRdPPrs')
er = EreaderProcessor(sect, user_key)
if er.getNumImages() > 0:
print u"Extracting images"
if not os.path.exists(imagedirpath):
os.makedirs(imagedirpath)
for i in xrange(er.getNumImages()):
name, contents = er.getImage(i)
file(os.path.join(imagedirpath, name), 'wb').write(contents)
print u"Extracting pml"
pml_string = er.getText()
pmlfilename = bookname + ".pml"
file(os.path.join(outdir, pmlfilename),'wb').write(cleanPML(pml_string))
if pmlzname is not None:
import zipfile
import shutil
print u"Creating PMLZ file {0}".format(os.path.basename(pmlzname))
myZipFile = zipfile.ZipFile(pmlzname,'w',zipfile.ZIP_STORED, False)
list = os.listdir(outdir)
for filename in list:
localname = filename
filePath = os.path.join(outdir,filename)
if os.path.isfile(filePath):
myZipFile.write(filePath, localname)
elif os.path.isdir(filePath):
imageList = os.listdir(filePath)
localimgdir = os.path.basename(filePath)
for image in imageList:
localname = os.path.join(localimgdir,image)
imagePath = os.path.join(filePath,image)
if os.path.isfile(imagePath):
myZipFile.write(imagePath, localname)
myZipFile.close()
# remove temporary directory
shutil.rmtree(outdir, True)
print u"Output is {0}".format(pmlzname)
else :
print u"Output is in {0}".format(outdir)
print "done"
except ValueError, e:
print u"Error: {0}".format(e)
with open(infile,'rb') as infileobject:
bookdata = infileobject.read(58)
# Check for Zip
if bookdata[0:0+2] == "PK":
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"
except:
traceback.print_exc()
return 1
return 0
def usage():
print u"Converts DRMed eReader books to PML Source"
print u"Usage:"
print u" erdr2pml [options] infile.pdb [outpath] \"your name\" credit_card_number"
print u" "
print u"Options: "
print u" -h prints this message"
print u" -p create PMLZ instead of source folder"
print u" --make-pmlz create PMLZ instead of source folder"
print u" "
print u"Note:"
print u" if outpath is ommitted, creates source in 'infile_Source' folder"
print u" if outpath is ommitted and pmlz option, creates PMLZ 'infile.pmlz'"
print u" if source folder created, images are in infile_img folder"
print u" if pmlz file created, images are in images folder"
print u" It's enough to enter the last 8 digits of the credit card number"
return
def getuser_key(name,cc):
newname = "".join(c for c in name.lower() if c >= 'a' and c <= 'z' or c >= '0' and c <= '9')
cc = cc.replace(" ","")
return struct.pack('>LL', binascii.crc32(newname) & 0xffffffff,binascii.crc32(cc[-8:])& 0xffffffff)
def cli_main():
print u"eRdr2Pml v{0}. Copyright © 20092012 The Dark Reverser et al.".format(__version__)
return encryption
def main():
argv=unicode_argv()
try:
opts, args = getopt.getopt(argv[1:], "hp", ["make-pmlz"])
except getopt.GetoptError, err:
print err.args[0]
usage()
return 1
make_pmlz = False
for o, a in opts:
if o == "-h":
usage()
return 0
elif o == "-p":
make_pmlz = True
elif o == "--make-pmlz":
make_pmlz = True
if len(args)!=3 and len(args)!=4:
usage()
return 1
if len(args)==3:
infile, name, cc = args
if make_pmlz:
outpath = os.path.splitext(infile)[0] + u".pmlz"
else:
outpath = os.path.splitext(infile)[0] + u"_Source"
elif len(args)==4:
infile, outpath, name, cc = args
print getuser_key(name,cc).encode('hex')
return decryptBook(infile, outpath, make_pmlz, getuser_key(name,cc))
print encryption(argv[1])
return 0
if __name__ == "__main__":
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
sys.exit(cli_main())
sys.exit(main())

File diff suppressed because it is too large Load Diff

View File

@@ -1,63 +1,127 @@
#! /usr/bin/python
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
# For use with Topaz Scripts Version 2.6
import sys
import csv
import os
import math
import getopt
from struct import pack
from struct import unpack
class PParser(object):
def __init__(self, gd, flatxml, meta_array):
self.gd = gd
self.flatdoc = flatxml.split('\n')
self.docSize = len(self.flatdoc)
self.temp = []
class DocParser(object):
def __init__(self, flatxml, classlst, fileid, bookDir, gdict, fixedimage):
self.id = os.path.basename(fileid).replace('.dat','')
self.svgcount = 0
self.docList = flatxml.split('\n')
self.docSize = len(self.docList)
self.classList = {}
self.bookDir = bookDir
self.gdict = gdict
tmpList = classlst.split('\n')
for pclass in tmpList:
if pclass != '':
# remove the leading period from the css name
cname = pclass[1:]
self.classList[cname] = True
self.fixedimage = fixedimage
self.ocrtext = []
self.link_id = []
self.link_title = []
self.link_page = []
self.link_href = []
self.link_type = []
self.dehyphen_rootid = []
self.paracont_stemid = []
self.parastems_stemid = []
self.ph = -1
self.pw = -1
startpos = self.posinDoc('page.h') or self.posinDoc('book.h')
for p in startpos:
(name, argres) = self.lineinDoc(p)
self.ph = max(self.ph, int(argres))
startpos = self.posinDoc('page.w') or self.posinDoc('book.w')
for p in startpos:
(name, argres) = self.lineinDoc(p)
self.pw = max(self.pw, int(argres))
if self.ph <= 0:
self.ph = int(meta_array.get('pageHeight', '11000'))
if self.pw <= 0:
self.pw = int(meta_array.get('pageWidth', '8500'))
def getGlyph(self, gid):
result = ''
id='id="gl%d"' % gid
return self.gdict.lookup(id)
res = []
startpos = self.posinDoc('info.glyph.x')
for p in startpos:
argres = self.getDataatPos('info.glyph.x', p)
res.extend(argres)
self.gx = res
def glyphs_to_image(self, glyphList):
res = []
startpos = self.posinDoc('info.glyph.y')
for p in startpos:
argres = self.getDataatPos('info.glyph.y', p)
res.extend(argres)
self.gy = res
def extract(path, key):
b = path.find(key) + len(key)
e = path.find(' ',b)
return int(path[b:e])
svgDir = os.path.join(self.bookDir,'svg')
imgDir = os.path.join(self.bookDir,'img')
imgname = self.id + '_%04d.svg' % self.svgcount
imgfile = os.path.join(imgDir,imgname)
# get glyph information
gxList = self.getData('info.glyph.x',0,-1)
gyList = self.getData('info.glyph.y',0,-1)
gidList = self.getData('info.glyph.glyphID',0,-1)
gids = []
maxws = []
maxhs = []
xs = []
ys = []
gdefs = []
# get path defintions, positions, dimensions for each glyph
# that makes up the image, and find min x and min y to reposition origin
minx = -1
miny = -1
for j in glyphList:
gid = gidList[j]
gids.append(gid)
xs.append(gxList[j])
if minx == -1: minx = gxList[j]
else : minx = min(minx, gxList[j])
ys.append(gyList[j])
if miny == -1: miny = gyList[j]
else : miny = min(miny, gyList[j])
path = self.getGlyph(gid)
gdefs.append(path)
maxws.append(extract(path,'width='))
maxhs.append(extract(path,'height='))
# change the origin to minx, miny and calc max height and width
maxw = maxws[0] + xs[0] - minx
maxh = maxhs[0] + ys[0] - miny
for j in xrange(0, len(xs)):
xs[j] = xs[j] - minx
ys[j] = ys[j] - miny
maxw = max( maxw, (maxws[j] + xs[j]) )
maxh = max( maxh, (maxhs[j] + ys[j]) )
# open the image file for output
ifile = open(imgfile,'w')
ifile.write('<?xml version="1.0" standalone="no"?>\n')
ifile.write('<!DOCTYPE svg PUBLIC "-//W3C/DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
ifile.write('<svg width="%dpx" height="%dpx" viewBox="0 0 %d %d" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">\n' % (math.floor(maxw/10), math.floor(maxh/10), maxw, maxh))
ifile.write('<defs>\n')
for j in xrange(0,len(gdefs)):
ifile.write(gdefs[j])
ifile.write('</defs>\n')
for j in xrange(0,len(gids)):
ifile.write('<use xlink:href="#gl%d" x="%d" y="%d" />\n' % (gids[j], xs[j], ys[j]))
ifile.write('</svg>')
ifile.close()
return 0
res = []
startpos = self.posinDoc('info.glyph.glyphID')
for p in startpos:
argres = self.getDataatPos('info.glyph.glyphID', p)
res.extend(argres)
self.gid = res
# return tag at line pos in document
def lineinDoc(self, pos) :
if (pos >= 0) and (pos < self.docSize) :
item = self.flatdoc[pos]
item = self.docList[pos]
if item.find('=') >= 0:
(name, argres) = item.split('=',1)
else :
@@ -65,6 +129,7 @@ class PParser(object):
argres = ''
return name, argres
# find tag in doc if within pos to end inclusive
def findinDoc(self, tagpath, pos, end) :
result = None
@@ -74,7 +139,7 @@ class PParser(object):
end = min(self.docSize, end)
foundat = -1
for j in xrange(pos, end):
item = self.flatdoc[j]
item = self.docList[j]
if item.find('=') >= 0:
(name, argres) = item.split('=',1)
else :
@@ -86,6 +151,7 @@ class PParser(object):
break
return foundat, result
# return list of start positions for the tagpath
def posinDoc(self, tagpath):
startpos = []
@@ -98,152 +164,638 @@ class PParser(object):
pos = foundpos + 1
return startpos
def getData(self, path):
result = None
cnt = len(self.flatdoc)
for j in xrange(cnt):
item = self.flatdoc[j]
if item.find('=') >= 0:
(name, argt) = item.split('=')
argres = argt.split('|')
else:
name = item
argres = []
if (name.endswith(path)):
result = argres
break
if (len(argres) > 0) :
for j in xrange(0,len(argres)):
argres[j] = int(argres[j])
return result
def getDataatPos(self, path, pos):
result = None
item = self.flatdoc[pos]
if item.find('=') >= 0:
(name, argt) = item.split('=')
argres = argt.split('|')
else:
name = item
argres = []
if (len(argres) > 0) :
for j in xrange(0,len(argres)):
argres[j] = int(argres[j])
if (name.endswith(path)):
result = argres
return result
# returns a vector of integers for the tagpath
def getData(self, tagpath, pos, end):
argres=[]
(foundat, argt) = self.findinDoc(tagpath, pos, end)
if (argt != None) and (len(argt) > 0) :
argList = argt.split('|')
argres = [ int(strval) for strval in argList]
return argres
def getDataTemp(self, path):
result = None
cnt = len(self.temp)
for j in xrange(cnt):
item = self.temp[j]
if item.find('=') >= 0:
(name, argt) = item.split('=')
argres = argt.split('|')
else:
name = item
argres = []
if (name.endswith(path)):
result = argres
self.temp.pop(j)
break
if (len(argres) > 0) :
for j in xrange(0,len(argres)):
argres[j] = int(argres[j])
return result
def getImages(self):
# get the class
def getClass(self, pclass):
nclass = pclass
# class names are an issue given topaz may start them with numerals (not allowed),
# use a mix of cases (which cause some browsers problems), and actually
# attach numbers after "_reclustered*" to the end to deal classeses that inherit
# from a base class (but then not actually provide all of these _reclustereed
# classes in the stylesheet!
# so we clean this up by lowercasing, prepend 'cl-', and getting any baseclass
# that exists in the stylesheet first, and then adding this specific class
# after
# also some class names have spaces in them so need to convert to dashes
if nclass != None :
nclass = nclass.replace(' ','-')
classres = ''
nclass = nclass.lower()
nclass = 'cl-' + nclass
baseclass = ''
# graphic is the base class for captions
if nclass.find('cl-cap-') >=0 :
classres = 'graphic' + ' '
else :
# strip to find baseclass
p = nclass.find('_')
if p > 0 :
baseclass = nclass[0:p]
if baseclass in self.classList:
classres += baseclass + ' '
classres += nclass
nclass = classres
return nclass
# develop a sorted description of the starting positions of
# groups and regions on the page, as well as the page type
def PageDescription(self):
def compare(x, y):
(xtype, xval) = x
(ytype, yval) = y
if xval > yval:
return 1
if xval == yval:
return 0
return -1
result = []
self.temp = self.flatdoc
while (self.getDataTemp('img') != None):
h = self.getDataTemp('img.h')[0]
w = self.getDataTemp('img.w')[0]
x = self.getDataTemp('img.x')[0]
y = self.getDataTemp('img.y')[0]
src = self.getDataTemp('img.src')[0]
result.append('<image xlink:href="../img/img%04d.jpg" x="%d" y="%d" width="%d" height="%d" />\n' % (src, x, y, w, h))
return result
(pos, pagetype) = self.findinDoc('page.type',0,-1)
groupList = self.posinDoc('page.group')
groupregionList = self.posinDoc('page.group.region')
pageregionList = self.posinDoc('page.region')
# integrate into one list
for j in groupList:
result.append(('grpbeg',j))
for j in groupregionList:
result.append(('gregion',j))
for j in pageregionList:
result.append(('pregion',j))
result.sort(compare)
# insert group end and page end indicators
inGroup = False
j = 0
while True:
if j == len(result): break
rtype = result[j][0]
rval = result[j][1]
if not inGroup and (rtype == 'grpbeg') :
inGroup = True
j = j + 1
elif inGroup and (rtype in ('grpbeg', 'pregion')):
result.insert(j,('grpend',rval))
inGroup = False
else:
j = j + 1
if inGroup:
result.append(('grpend',-1))
result.append(('pageend', -1))
return pagetype, result
# build a description of the paragraph
def getParaDescription(self, start, end, regtype):
def getGlyphs(self):
result = []
if (self.gid != None) and (len(self.gid) > 0):
glyphs = []
for j in set(self.gid):
glyphs.append(j)
glyphs.sort()
for gid in glyphs:
id='id="gl%d"' % gid
path = self.gd.lookup(id)
if path:
result.append(id + ' ' + path)
return result
# paragraph
(pos, pclass) = self.findinDoc('paragraph.class',start,end)
pclass = self.getClass(pclass)
# if paragraph uses extratokens (extra glyphs) then make it fixed
(pos, extraglyphs) = self.findinDoc('paragraph.extratokens',start,end)
# build up a description of the paragraph in result and return it
# first check for the basic - all words paragraph
(pos, sfirst) = self.findinDoc('paragraph.firstWord',start,end)
(pos, slast) = self.findinDoc('paragraph.lastWord',start,end)
if (sfirst != None) and (slast != None) :
first = int(sfirst)
last = int(slast)
makeImage = (regtype == 'vertical') or (regtype == 'table')
makeImage = makeImage or (extraglyphs != None)
if self.fixedimage:
makeImage = makeImage or (regtype == 'fixed')
if (pclass != None):
makeImage = makeImage or (pclass.find('.inverted') >= 0)
if self.fixedimage :
makeImage = makeImage or (pclass.find('cl-f-') >= 0)
# before creating an image make sure glyph info exists
gidList = self.getData('info.glyph.glyphID',0,-1)
makeImage = makeImage & (len(gidList) > 0)
if not makeImage :
# standard all word paragraph
for wordnum in xrange(first, last):
result.append(('ocr', wordnum))
return pclass, result
# convert paragraph to svg image
# translate first and last word into first and last glyphs
# and generate inline image and include it
glyphList = []
firstglyphList = self.getData('word.firstGlyph',0,-1)
gidList = self.getData('info.glyph.glyphID',0,-1)
firstGlyph = firstglyphList[first]
if last < len(firstglyphList):
lastGlyph = firstglyphList[last]
else :
lastGlyph = len(gidList)
# handle case of white sapce paragraphs with no actual glyphs in them
# by reverting to text based paragraph
if firstGlyph >= lastGlyph:
# revert to standard text based paragraph
for wordnum in xrange(first, last):
result.append(('ocr', wordnum))
return pclass, result
for glyphnum in xrange(firstGlyph, lastGlyph):
glyphList.append(glyphnum)
# include any extratokens if they exist
(pos, sfg) = self.findinDoc('extratokens.firstGlyph',start,end)
(pos, slg) = self.findinDoc('extratokens.lastGlyph',start,end)
if (sfg != None) and (slg != None):
for glyphnum in xrange(int(sfg), int(slg)):
glyphList.append(glyphnum)
num = self.svgcount
self.glyphs_to_image(glyphList)
self.svgcount += 1
result.append(('svg', num))
return pclass, result
# this type of paragraph may be made up of multiple spans, inline
# word monograms (images), and words with semantic meaning,
# plus glyphs used to form starting letter of first word
# need to parse this type line by line
line = start + 1
word_class = ''
# if end is -1 then we must search to end of document
if end == -1 :
end = self.docSize
# seems some xml has last* coming before first* so we have to
# handle any order
sp_first = -1
sp_last = -1
gl_first = -1
gl_last = -1
ws_first = -1
ws_last = -1
word_class = ''
word_semantic_type = ''
while (line < end) :
(name, argres) = self.lineinDoc(line)
if name.endswith('span.firstWord') :
sp_first = int(argres)
elif name.endswith('span.lastWord') :
sp_last = int(argres)
elif name.endswith('word.firstGlyph') :
gl_first = int(argres)
elif name.endswith('word.lastGlyph') :
gl_last = int(argres)
elif name.endswith('word_semantic.firstWord'):
ws_first = int(argres)
elif name.endswith('word_semantic.lastWord'):
ws_last = int(argres)
elif name.endswith('word.class'):
# we only handle spaceafter word class
try:
(cname, space) = argres.split('-',1)
if space == '' : space = '0'
if (cname == 'spaceafter') and (int(space) > 0) :
word_class = 'sa'
except:
pass
elif name.endswith('word.img.src'):
result.append(('img' + word_class, int(argres)))
word_class = ''
elif name.endswith('region.img.src'):
result.append(('img' + word_class, int(argres)))
if (sp_first != -1) and (sp_last != -1):
for wordnum in xrange(sp_first, sp_last):
result.append(('ocr', wordnum))
sp_first = -1
sp_last = -1
if (gl_first != -1) and (gl_last != -1):
glyphList = []
for glyphnum in xrange(gl_first, gl_last):
glyphList.append(glyphnum)
num = self.svgcount
self.glyphs_to_image(glyphList)
self.svgcount += 1
result.append(('svg', num))
gl_first = -1
gl_last = -1
if (ws_first != -1) and (ws_last != -1):
for wordnum in xrange(ws_first, ws_last):
result.append(('ocr', wordnum))
ws_first = -1
ws_last = -1
line += 1
return pclass, result
def convert2SVG(gdict, flat_xml, pageid, previd, nextid, svgDir, raw, meta_array, scaledpi):
mlst = []
pp = PParser(gdict, flat_xml, meta_array)
mlst.append('<?xml version="1.0" standalone="no"?>\n')
if (raw):
mlst.append('<!DOCTYPE svg PUBLIC "-//W3C/DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
mlst.append('<svg width="%fin" height="%fin" viewBox="0 0 %d %d" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">\n' % (pp.pw / scaledpi, pp.ph / scaledpi, pp.pw -1, pp.ph -1))
mlst.append('<title>Page %d - %s by %s</title>\n' % (pageid, meta_array['Title'],meta_array['Authors']))
else:
mlst.append('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n')
mlst.append('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" ><head>\n')
mlst.append('<title>Page %d - %s by %s</title>\n' % (pageid, meta_array['Title'],meta_array['Authors']))
mlst.append('<script><![CDATA[\n')
mlst.append('function gd(){var p=window.location.href.replace(/^.*\?dpi=(\d+).*$/i,"$1");return p;}\n')
mlst.append('var dpi=%d;\n' % scaledpi)
if (previd) :
mlst.append('var prevpage="page%04d.xhtml";\n' % (previd))
if (nextid) :
mlst.append('var nextpage="page%04d.xhtml";\n' % (nextid))
mlst.append('var pw=%d;var ph=%d;' % (pp.pw, pp.ph))
mlst.append('function zoomin(){dpi=dpi*(0.8);setsize();}\n')
mlst.append('function zoomout(){dpi=dpi*1.25;setsize();}\n')
mlst.append('function setsize(){var svg=document.getElementById("svgimg");var prev=document.getElementById("prevsvg");var next=document.getElementById("nextsvg");var width=(pw/dpi)+"in";var height=(ph/dpi)+"in";svg.setAttribute("width",width);svg.setAttribute("height",height);prev.setAttribute("height",height);prev.setAttribute("width","50px");next.setAttribute("height",height);next.setAttribute("width","50px");}\n')
mlst.append('function ppage(){window.location.href=prevpage+"?dpi="+Math.round(dpi);}\n')
mlst.append('function npage(){window.location.href=nextpage+"?dpi="+Math.round(dpi);}\n')
mlst.append('var gt=gd();if(gt>0){dpi=gt;}\n')
mlst.append('window.onload=setsize;\n')
mlst.append(']]></script>\n')
mlst.append('</head>\n')
mlst.append('<body onLoad="setsize();" style="background-color:#777;text-align:center;">\n')
mlst.append('<div style="white-space:nowrap;">\n')
if previd == None:
mlst.append('<a href="javascript:ppage();"><svg id="prevsvg" viewBox="0 0 100 300" xmlns="http://www.w3.org/2000/svg" version="1.1" style="background-color:#777"></svg></a>\n')
def buildParagraph(self, pclass, pdesc, type, regtype) :
parares = ''
sep =''
classres = ''
if pclass :
classres = ' class="' + pclass + '"'
br_lb = (regtype == 'fixed') or (regtype == 'chapterheading') or (regtype == 'vertical')
handle_links = len(self.link_id) > 0
if (type == 'full') or (type == 'begin') :
parares += '<p' + classres + '>'
if (type == 'end'):
parares += ' '
lstart = len(parares)
cnt = len(pdesc)
for j in xrange( 0, cnt) :
(wtype, num) = pdesc[j]
if wtype == 'ocr' :
try:
word = self.ocrtext[num]
except:
word = ""
sep = ' '
if handle_links:
link = self.link_id[num]
if (link > 0):
linktype = self.link_type[link-1]
title = self.link_title[link-1]
if (title == "") or (parares.rfind(title) < 0):
title=parares[lstart:]
if linktype == 'external' :
linkhref = self.link_href[link-1]
linkhtml = '<a href="%s">' % linkhref
else :
if len(self.link_page) >= link :
ptarget = self.link_page[link-1] - 1
linkhtml = '<a href="#page%04d">' % ptarget
else :
# just link to the current page
linkhtml = '<a href="#' + self.id + '">'
linkhtml += title + '</a>'
pos = parares.rfind(title)
if pos >= 0:
parares = parares[0:pos] + linkhtml + parares[pos+len(title):]
else :
parares += linkhtml
lstart = len(parares)
if word == '_link_' : word = ''
elif (link < 0) :
if word == '_link_' : word = ''
if word == '_lb_':
if ((num-1) in self.dehyphen_rootid ) or handle_links:
word = ''
sep = ''
elif br_lb :
word = '<br />\n'
sep = ''
else :
word = '\n'
sep = ''
if num in self.dehyphen_rootid :
word = word[0:-1]
sep = ''
parares += word + sep
elif wtype == 'img' :
sep = ''
parares += '<img src="img/img%04d.jpg" alt="" />' % num
parares += sep
elif wtype == 'imgsa' :
sep = ' '
parares += '<img src="img/img%04d.jpg" alt="" />' % num
parares += sep
elif wtype == 'svg' :
sep = ''
parares += '<img src="img/' + self.id + '_%04d.svg" alt="" />' % num
parares += sep
if len(sep) > 0 : parares = parares[0:-1]
if (type == 'full') or (type == 'end') :
parares += '</p>'
return parares
def buildTOCEntry(self, pdesc) :
parares = ''
sep =''
tocentry = ''
handle_links = len(self.link_id) > 0
lstart = 0
cnt = len(pdesc)
for j in xrange( 0, cnt) :
(wtype, num) = pdesc[j]
if wtype == 'ocr' :
word = self.ocrtext[num]
sep = ' '
if handle_links:
link = self.link_id[num]
if (link > 0):
linktype = self.link_type[link-1]
title = self.link_title[link-1]
title = title.rstrip('. ')
alt_title = parares[lstart:]
alt_title = alt_title.strip()
# now strip off the actual printed page number
alt_title = alt_title.rstrip('01234567890ivxldIVXLD-.')
alt_title = alt_title.rstrip('. ')
# skip over any external links - can't have them in a books toc
if linktype == 'external' :
title = ''
alt_title = ''
linkpage = ''
else :
if len(self.link_page) >= link :
ptarget = self.link_page[link-1] - 1
linkpage = '%04d' % ptarget
else :
# just link to the current page
linkpage = self.id[4:]
if len(alt_title) >= len(title):
title = alt_title
if title != '' and linkpage != '':
tocentry += title + '|' + linkpage + '\n'
lstart = len(parares)
if word == '_link_' : word = ''
elif (link < 0) :
if word == '_link_' : word = ''
if word == '_lb_':
word = ''
sep = ''
if num in self.dehyphen_rootid :
word = word[0:-1]
sep = ''
parares += word + sep
else :
continue
return tocentry
# walk the document tree collecting the information needed
# to build an html page using the ocrText
def process(self):
tocinfo = ''
hlst = []
# get the ocr text
(pos, argres) = self.findinDoc('info.word.ocrText',0,-1)
if argres : self.ocrtext = argres.split('|')
# get information to dehyphenate the text
self.dehyphen_rootid = self.getData('info.dehyphen.rootID',0,-1)
# determine if first paragraph is continued from previous page
(pos, self.parastems_stemid) = self.findinDoc('info.paraStems.stemID',0,-1)
first_para_continued = (self.parastems_stemid != None)
# determine if last paragraph is continued onto the next page
(pos, self.paracont_stemid) = self.findinDoc('info.paraCont.stemID',0,-1)
last_para_continued = (self.paracont_stemid != None)
# collect link ids
self.link_id = self.getData('info.word.link_id',0,-1)
# collect link destination page numbers
self.link_page = self.getData('info.links.page',0,-1)
# collect link types (container versus external)
(pos, argres) = self.findinDoc('info.links.type',0,-1)
if argres : self.link_type = argres.split('|')
# collect link destinations
(pos, argres) = self.findinDoc('info.links.href',0,-1)
if argres : self.link_href = argres.split('|')
# collect link titles
(pos, argres) = self.findinDoc('info.links.title',0,-1)
if argres :
self.link_title = argres.split('|')
else:
mlst.append('<a href="javascript:ppage();"><svg id="prevsvg" viewBox="0 0 100 300" xmlns="http://www.w3.org/2000/svg" version="1.1" style="background-color:#777"><polygon points="5,150,95,5,95,295" fill="#AAAAAA" /></svg></a>\n')
self.link_title.append('')
mlst.append('<a href="javascript:npage();"><svg id="svgimg" viewBox="0 0 %d %d" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" style="background-color:#FFF;border:1px solid black;">' % (pp.pw, pp.ph))
if (pp.gid != None):
mlst.append('<defs>\n')
gdefs = pp.getGlyphs()
for j in xrange(0,len(gdefs)):
mlst.append(gdefs[j])
mlst.append('</defs>\n')
img = pp.getImages()
if (img != None):
for j in xrange(0,len(img)):
mlst.append(img[j])
if (pp.gid != None):
for j in xrange(0,len(pp.gid)):
mlst.append('<use xlink:href="#gl%d" x="%d" y="%d" />\n' % (pp.gid[j], pp.gx[j], pp.gy[j]))
if (img == None or len(img) == 0) and (pp.gid == None or len(pp.gid) == 0):
xpos = "%d" % (pp.pw // 3)
ypos = "%d" % (pp.ph // 3)
mlst.append('<text x="' + xpos + '" y="' + ypos + '" font-size="' + meta_array['fontSize'] + '" font-family="Helvetica" stroke="black">This page intentionally left blank.</text>\n')
if (raw) :
mlst.append('</svg>')
else :
mlst.append('</svg></a>\n')
if nextid == None:
mlst.append('<a href="javascript:npage();"><svg id="nextsvg" viewBox="0 0 100 300" xmlns="http://www.w3.org/2000/svg" version="1.1" style="background-color:#777"></svg></a>\n')
else :
mlst.append('<a href="javascript:npage();"><svg id="nextsvg" viewBox="0 0 100 300" xmlns="http://www.w3.org/2000/svg" version="1.1" style="background-color:#777"><polygon points="5,5,5,295,95,150" fill="#AAAAAA" /></svg></a>\n')
mlst.append('</div>\n')
mlst.append('<div><a href="javascript:zoomin();">zoom in</a> - <a href="javascript:zoomout();">zoom out</a></div>\n')
mlst.append('</body>\n')
mlst.append('</html>\n')
return "".join(mlst)
# get a descriptions of the starting points of the regions
# and groups on the page
(pagetype, pageDesc) = self.PageDescription()
regcnt = len(pageDesc) - 1
anchorSet = False
breakSet = False
inGroup = False
# process each region on the page and convert what you can to html
for j in xrange(regcnt):
(etype, start) = pageDesc[j]
(ntype, end) = pageDesc[j+1]
# set anchor for link target on this page
if not anchorSet and not first_para_continued:
hlst.append('<div style="visibility: hidden; height: 0; width: 0;" id="')
hlst.append(self.id + '" title="pagetype_' + pagetype + '"></div>\n')
anchorSet = True
# handle groups of graphics with text captions
if (etype == 'grpbeg'):
(pos, grptype) = self.findinDoc('group.type', start, end)
if grptype != None:
if grptype == 'graphic':
gcstr = ' class="' + grptype + '"'
hlst.append('<div' + gcstr + '>')
inGroup = True
elif (etype == 'grpend'):
if inGroup:
hlst.append('</div>\n')
inGroup = False
else:
(pos, regtype) = self.findinDoc('region.type',start,end)
if regtype == 'graphic' :
(pos, simgsrc) = self.findinDoc('img.src',start,end)
if simgsrc:
if inGroup:
hlst.append('<img src="img/img%04d.jpg" alt="" />' % int(simgsrc))
else:
hlst.append('<div class="graphic"><img src="img/img%04d.jpg" alt="" /></div>' % int(simgsrc))
elif regtype == 'chapterheading' :
(pclass, pdesc) = self.getParaDescription(start,end, regtype)
if not breakSet:
hlst.append('<div style="page-break-after: always;">&nbsp;</div>\n')
breakSet = True
tag = 'h1'
if pclass and (len(pclass) >= 7):
if pclass[3:7] == 'ch1-' : tag = 'h1'
if pclass[3:7] == 'ch2-' : tag = 'h2'
if pclass[3:7] == 'ch3-' : tag = 'h3'
hlst.append('<' + tag + ' class="' + pclass + '">')
else:
hlst.append('<' + tag + '>')
hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype))
hlst.append('</' + tag + '>')
elif (regtype == 'text') or (regtype == 'fixed') or (regtype == 'insert') or (regtype == 'listitem'):
ptype = 'full'
# check to see if this is a continution from the previous page
if first_para_continued :
ptype = 'end'
first_para_continued = False
(pclass, pdesc) = self.getParaDescription(start,end, regtype)
if pclass and (len(pclass) >= 6) and (ptype == 'full'):
tag = 'p'
if pclass[3:6] == 'h1-' : tag = 'h4'
if pclass[3:6] == 'h2-' : tag = 'h5'
if pclass[3:6] == 'h3-' : tag = 'h6'
hlst.append('<' + tag + ' class="' + pclass + '">')
hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype))
hlst.append('</' + tag + '>')
else :
hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype))
elif (regtype == 'tocentry') :
ptype = 'full'
if first_para_continued :
ptype = 'end'
first_para_continued = False
(pclass, pdesc) = self.getParaDescription(start,end, regtype)
tocinfo += self.buildTOCEntry(pdesc)
hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype))
elif (regtype == 'vertical') or (regtype == 'table') :
ptype = 'full'
if inGroup:
ptype = 'middle'
if first_para_continued :
ptype = 'end'
first_para_continued = False
(pclass, pdesc) = self.getParaDescription(start, end, regtype)
hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype))
elif (regtype == 'synth_fcvr.center'):
(pos, simgsrc) = self.findinDoc('img.src',start,end)
if simgsrc:
hlst.append('<div class="graphic"><img src="img/img%04d.jpg" alt="" /></div>' % int(simgsrc))
else :
print ' Making region type', regtype,
(pos, temp) = self.findinDoc('paragraph',start,end)
(pos2, temp) = self.findinDoc('span',start,end)
if pos != -1 or pos2 != -1:
print ' a "text" region'
orig_regtype = regtype
regtype = 'fixed'
ptype = 'full'
# check to see if this is a continution from the previous page
if first_para_continued :
ptype = 'end'
first_para_continued = False
(pclass, pdesc) = self.getParaDescription(start,end, regtype)
if not pclass:
if orig_regtype.endswith('.right') : pclass = 'cl-right'
elif orig_regtype.endswith('.center') : pclass = 'cl-center'
elif orig_regtype.endswith('.left') : pclass = 'cl-left'
elif orig_regtype.endswith('.justify') : pclass = 'cl-justify'
if pclass and (ptype == 'full') and (len(pclass) >= 6):
tag = 'p'
if pclass[3:6] == 'h1-' : tag = 'h4'
if pclass[3:6] == 'h2-' : tag = 'h5'
if pclass[3:6] == 'h3-' : tag = 'h6'
hlst.append('<' + tag + ' class="' + pclass + '">')
hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype))
hlst.append('</' + tag + '>')
else :
hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype))
else :
print ' a "graphic" region'
(pos, simgsrc) = self.findinDoc('img.src',start,end)
if simgsrc:
hlst.append('<div class="graphic"><img src="img/img%04d.jpg" alt="" /></div>' % int(simgsrc))
htmlpage = "".join(hlst)
if last_para_continued :
if htmlpage[-4:] == '</p>':
htmlpage = htmlpage[0:-4]
last_para_continued = False
return htmlpage, tocinfo
def convert2HTML(flatxml, classlst, fileid, bookDir, gdict, fixedimage):
# create a document parser
dp = DocParser(flatxml, classlst, fileid, bookDir, gdict, fixedimage)
htmlpage, tocinfo = dp.process()
return htmlpage, tocinfo

View File

@@ -1,148 +1,82 @@
#! /usr/bin/python
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
class Unbuffered:
def __init__(self, stream):
self.stream = stream
def write(self, data):
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
import sys
sys.stdout=Unbuffered(sys.stdout)
import csv
import os
import getopt
from struct import pack
from struct import unpack
class TpzDRMError(Exception):
pass
# local support routines
if 'calibre' in sys.modules:
inCalibre = True
else:
inCalibre = False
if inCalibre :
from calibre_plugins.dedrm import convert2xml
from calibre_plugins.dedrm import flatxml2html
from calibre_plugins.dedrm import flatxml2svg
from calibre_plugins.dedrm import stylexml2css
else :
import convert2xml
import flatxml2html
import flatxml2svg
import stylexml2css
# global switch
buildXML = False
# Get a 7 bit encoded number from a file
def readEncodedNumber(file):
flag = False
c = file.read(1)
if (len(c) == 0):
return None
data = ord(c)
if data == 0xFF:
flag = True
c = file.read(1)
if (len(c) == 0):
return None
data = ord(c)
if data >= 0x80:
datax = (data & 0x7F)
while data >= 0x80 :
c = file.read(1)
if (len(c) == 0):
return None
data = ord(c)
datax = (datax <<7) + (data & 0x7F)
data = datax
if flag:
data = -data
return data
# Get a length prefixed string from the file
def lengthPrefixString(data):
return encodeNumber(len(data))+data
def readString(file):
stringLength = readEncodedNumber(file)
if (stringLength == None):
return None
sv = file.read(stringLength)
if (len(sv) != stringLength):
return ""
return unpack(str(stringLength)+"s",sv)[0]
def getMetaArray(metaFile):
# parse the meta file
result = {}
fo = file(metaFile,'rb')
size = readEncodedNumber(fo)
for i in xrange(size):
tag = readString(fo)
value = readString(fo)
result[tag] = value
# print tag, value
fo.close()
return result
# dictionary of all text strings by index value
class Dictionary(object):
def __init__(self, dictFile):
self.filename = dictFile
self.size = 0
self.fo = file(dictFile,'rb')
self.stable = []
self.size = readEncodedNumber(self.fo)
for i in xrange(self.size):
self.stable.append(self.escapestr(readString(self.fo)))
self.pos = 0
def escapestr(self, str):
str = str.replace('&','&amp;')
str = str.replace('<','&lt;')
str = str.replace('>','&gt;')
str = str.replace('=','&#61;')
return str
def lookup(self,val):
if ((val >= 0) and (val < self.size)) :
self.pos = val
return self.stable[self.pos]
else:
print "Error: %d outside of string table limits" % val
raise TpzDRMError('outside or string table limits')
# sys.exit(-1)
def getSize(self):
return self.size
def getPos(self):
return self.pos
class PageDimParser(object):
def __init__(self, flatxml):
class PParser(object):
def __init__(self, gd, flatxml, meta_array):
self.gd = gd
self.flatdoc = flatxml.split('\n')
# find tag if within pos to end inclusive
self.docSize = len(self.flatdoc)
self.temp = []
self.ph = -1
self.pw = -1
startpos = self.posinDoc('page.h') or self.posinDoc('book.h')
for p in startpos:
(name, argres) = self.lineinDoc(p)
self.ph = max(self.ph, int(argres))
startpos = self.posinDoc('page.w') or self.posinDoc('book.w')
for p in startpos:
(name, argres) = self.lineinDoc(p)
self.pw = max(self.pw, int(argres))
if self.ph <= 0:
self.ph = int(meta_array.get('pageHeight', '11000'))
if self.pw <= 0:
self.pw = int(meta_array.get('pageWidth', '8500'))
res = []
startpos = self.posinDoc('info.glyph.x')
for p in startpos:
argres = self.getDataatPos('info.glyph.x', p)
res.extend(argres)
self.gx = res
res = []
startpos = self.posinDoc('info.glyph.y')
for p in startpos:
argres = self.getDataatPos('info.glyph.y', p)
res.extend(argres)
self.gy = res
res = []
startpos = self.posinDoc('info.glyph.glyphID')
for p in startpos:
argres = self.getDataatPos('info.glyph.glyphID', p)
res.extend(argres)
self.gid = res
# return tag at line pos in document
def lineinDoc(self, pos) :
if (pos >= 0) and (pos < self.docSize) :
item = self.flatdoc[pos]
if item.find('=') >= 0:
(name, argres) = item.split('=',1)
else :
name = item
argres = ''
return name, argres
# find tag in doc if within pos to end inclusive
def findinDoc(self, tagpath, pos, end) :
result = None
docList = self.flatdoc
cnt = len(docList)
if end == -1 :
end = cnt
end = self.docSize
else:
end = min(cnt,end)
end = min(self.docSize, end)
foundat = -1
for j in xrange(pos, end):
item = docList[j]
item = self.flatdoc[j]
if item.find('=') >= 0:
(name, argres) = item.split('=')
(name, argres) = item.split('=',1)
else :
name = item
argres = ''
@@ -151,44 +85,19 @@ class PageDimParser(object):
foundat = j
break
return foundat, result
def process(self):
(pos, sph) = self.findinDoc('page.h',0,-1)
(pos, spw) = self.findinDoc('page.w',0,-1)
if (sph == None): sph = '-1'
if (spw == None): spw = '-1'
return sph, spw
def getPageDim(flatxml):
# create a document parser
dp = PageDimParser(flatxml)
(ph, pw) = dp.process()
return ph, pw
# return list of start positions for the tagpath
def posinDoc(self, tagpath):
startpos = []
pos = 0
res = ""
while res != None :
(foundpos, res) = self.findinDoc(tagpath, pos, -1)
if res != None :
startpos.append(foundpos)
pos = foundpos + 1
return startpos
class GParser(object):
def __init__(self, flatxml):
self.flatdoc = flatxml.split('\n')
self.dpi = 1440
self.gh = self.getData('info.glyph.h')
self.gw = self.getData('info.glyph.w')
self.guse = self.getData('info.glyph.use')
if self.guse :
self.count = len(self.guse)
else :
self.count = 0
self.gvtx = self.getData('info.glyph.vtx')
self.glen = self.getData('info.glyph.len')
self.gdpi = self.getData('info.glyph.dpi')
self.vx = self.getData('info.vtx.x')
self.vy = self.getData('info.vtx.y')
self.vlen = self.getData('info.len.n')
if self.vlen :
self.glen.append(len(self.vlen))
elif self.glen:
self.glen.append(0)
if self.vx :
self.gvtx.append(len(self.vx))
elif self.gvtx :
self.gvtx.append(0)
def getData(self, path):
result = None
cnt = len(self.flatdoc)
@@ -200,522 +109,141 @@ class GParser(object):
else:
name = item
argres = []
if (name == path):
if (name.endswith(path)):
result = argres
break
if (len(argres) > 0) :
for j in xrange(0,len(argres)):
argres[j] = int(argres[j])
return result
def getGlyphDim(self, gly):
if self.gdpi[gly] == 0:
return 0, 0
maxh = (self.gh[gly] * self.dpi) / self.gdpi[gly]
maxw = (self.gw[gly] * self.dpi) / self.gdpi[gly]
return maxh, maxw
def getPath(self, gly):
path = ''
if (gly < 0) or (gly >= self.count):
return path
tx = self.vx[self.gvtx[gly]:self.gvtx[gly+1]]
ty = self.vy[self.gvtx[gly]:self.gvtx[gly+1]]
p = 0
for k in xrange(self.glen[gly], self.glen[gly+1]):
if (p == 0):
zx = tx[0:self.vlen[k]+1]
zy = ty[0:self.vlen[k]+1]
def getDataatPos(self, path, pos):
result = None
item = self.flatdoc[pos]
if item.find('=') >= 0:
(name, argt) = item.split('=')
argres = argt.split('|')
else:
name = item
argres = []
if (len(argres) > 0) :
for j in xrange(0,len(argres)):
argres[j] = int(argres[j])
if (name.endswith(path)):
result = argres
return result
def getDataTemp(self, path):
result = None
cnt = len(self.temp)
for j in xrange(cnt):
item = self.temp[j]
if item.find('=') >= 0:
(name, argt) = item.split('=')
argres = argt.split('|')
else:
zx = tx[self.vlen[k-1]+1:self.vlen[k]+1]
zy = ty[self.vlen[k-1]+1:self.vlen[k]+1]
p += 1
j = 0
while ( j < len(zx) ):
if (j == 0):
# Start Position.
path += 'M %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly])
elif (j <= len(zx)-3):
# Cubic Bezier Curve
path += 'C %d %d %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[j+1] * self.dpi / self.gdpi[gly], zy[j+1] * self.dpi / self.gdpi[gly], zx[j+2] * self.dpi / self.gdpi[gly], zy[j+2] * self.dpi / self.gdpi[gly])
j += 2
elif (j == len(zx)-2):
# Cubic Bezier Curve to Start Position
path += 'C %d %d %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[j+1] * self.dpi / self.gdpi[gly], zy[j+1] * self.dpi / self.gdpi[gly], zx[0] * self.dpi / self.gdpi[gly], zy[0] * self.dpi / self.gdpi[gly])
j += 1
elif (j == len(zx)-1):
# Quadratic Bezier Curve to Start Position
path += 'Q %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[0] * self.dpi / self.gdpi[gly], zy[0] * self.dpi / self.gdpi[gly])
name = item
argres = []
if (name.endswith(path)):
result = argres
self.temp.pop(j)
break
if (len(argres) > 0) :
for j in xrange(0,len(argres)):
argres[j] = int(argres[j])
return result
j += 1
path += 'z'
return path
def getImages(self):
result = []
self.temp = self.flatdoc
while (self.getDataTemp('img') != None):
h = self.getDataTemp('img.h')[0]
w = self.getDataTemp('img.w')[0]
x = self.getDataTemp('img.x')[0]
y = self.getDataTemp('img.y')[0]
src = self.getDataTemp('img.src')[0]
result.append('<image xlink:href="../img/img%04d.jpg" x="%d" y="%d" width="%d" height="%d" />\n' % (src, x, y, w, h))
return result
def getGlyphs(self):
result = []
if (self.gid != None) and (len(self.gid) > 0):
glyphs = []
for j in set(self.gid):
glyphs.append(j)
glyphs.sort()
for gid in glyphs:
id='id="gl%d"' % gid
path = self.gd.lookup(id)
if path:
result.append(id + ' ' + path)
return result
# dictionary of all text strings by index value
class GlyphDict(object):
def __init__(self):
self.gdict = {}
def lookup(self, id):
# id='id="gl%d"' % val
if id in self.gdict:
return self.gdict[id]
return None
def addGlyph(self, val, path):
id='id="gl%d"' % val
self.gdict[id] = path
def generateBook(bookDir, raw, fixedimage):
# sanity check Topaz file extraction
if not os.path.exists(bookDir) :
print "Can not find directory with unencrypted book"
return 1
dictFile = os.path.join(bookDir,'dict0000.dat')
if not os.path.exists(dictFile) :
print "Can not find dict0000.dat file"
return 1
pageDir = os.path.join(bookDir,'page')
if not os.path.exists(pageDir) :
print "Can not find page directory in unencrypted book"
return 1
imgDir = os.path.join(bookDir,'img')
if not os.path.exists(imgDir) :
print "Can not find image directory in unencrypted book"
return 1
glyphsDir = os.path.join(bookDir,'glyphs')
if not os.path.exists(glyphsDir) :
print "Can not find glyphs directory in unencrypted book"
return 1
metaFile = os.path.join(bookDir,'metadata0000.dat')
if not os.path.exists(metaFile) :
print "Can not find metadata0000.dat in unencrypted book"
return 1
svgDir = os.path.join(bookDir,'svg')
if not os.path.exists(svgDir) :
os.makedirs(svgDir)
if buildXML:
xmlDir = os.path.join(bookDir,'xml')
if not os.path.exists(xmlDir) :
os.makedirs(xmlDir)
otherFile = os.path.join(bookDir,'other0000.dat')
if not os.path.exists(otherFile) :
print "Can not find other0000.dat in unencrypted book"
return 1
print "Updating to color images if available"
spath = os.path.join(bookDir,'color_img')
dpath = os.path.join(bookDir,'img')
filenames = os.listdir(spath)
filenames = sorted(filenames)
for filename in filenames:
imgname = filename.replace('color','img')
sfile = os.path.join(spath,filename)
dfile = os.path.join(dpath,imgname)
imgdata = file(sfile,'rb').read()
file(dfile,'wb').write(imgdata)
print "Creating cover.jpg"
isCover = False
cpath = os.path.join(bookDir,'img')
cpath = os.path.join(cpath,'img0000.jpg')
if os.path.isfile(cpath):
cover = file(cpath, 'rb').read()
cpath = os.path.join(bookDir,'cover.jpg')
file(cpath, 'wb').write(cover)
isCover = True
print 'Processing Dictionary'
dict = Dictionary(dictFile)
print 'Processing Meta Data and creating OPF'
meta_array = getMetaArray(metaFile)
# replace special chars in title and authors like & < >
title = meta_array.get('Title','No Title Provided')
title = title.replace('&','&amp;')
title = title.replace('<','&lt;')
title = title.replace('>','&gt;')
meta_array['Title'] = title
authors = meta_array.get('Authors','No Authors Provided')
authors = authors.replace('&','&amp;')
authors = authors.replace('<','&lt;')
authors = authors.replace('>','&gt;')
meta_array['Authors'] = authors
if buildXML:
xname = os.path.join(xmlDir, 'metadata.xml')
mlst = []
for key in meta_array:
mlst.append('<meta name="' + key + '" content="' + meta_array[key] + '" />\n')
metastr = "".join(mlst)
mlst = None
file(xname, 'wb').write(metastr)
print 'Processing StyleSheet'
# get some scaling info from metadata to use while processing styles
# and first page info
fontsize = '135'
if 'fontSize' in meta_array:
fontsize = meta_array['fontSize']
# also get the size of a normal text page
# get the total number of pages unpacked as a safety check
filenames = os.listdir(pageDir)
numfiles = len(filenames)
spage = '1'
if 'firstTextPage' in meta_array:
spage = meta_array['firstTextPage']
pnum = int(spage)
if pnum >= numfiles or pnum < 0:
# metadata is wrong so just select a page near the front
# 10% of the book to get a normal text page
pnum = int(0.10 * numfiles)
# print "first normal text page is", spage
# get page height and width from first text page for use in stylesheet scaling
pname = 'page%04d.dat' % (pnum + 1)
fname = os.path.join(pageDir,pname)
flat_xml = convert2xml.fromData(dict, fname)
(ph, pw) = getPageDim(flat_xml)
if (ph == '-1') or (ph == '0') : ph = '11000'
if (pw == '-1') or (pw == '0') : pw = '8500'
meta_array['pageHeight'] = ph
meta_array['pageWidth'] = pw
if 'fontSize' not in meta_array.keys():
meta_array['fontSize'] = fontsize
# process other.dat for css info and for map of page files to svg images
# this map is needed because some pages actually are made up of multiple
# pageXXXX.xml files
xname = os.path.join(bookDir, 'style.css')
flat_xml = convert2xml.fromData(dict, otherFile)
# extract info.original.pid to get original page information
pageIDMap = {}
pageidnums = stylexml2css.getpageIDMap(flat_xml)
if len(pageidnums) == 0:
filenames = os.listdir(pageDir)
numfiles = len(filenames)
for k in range(numfiles):
pageidnums.append(k)
# create a map from page ids to list of page file nums to process for that page
for i in range(len(pageidnums)):
id = pageidnums[i]
if id in pageIDMap.keys():
pageIDMap[id].append(i)
else:
pageIDMap[id] = [i]
# now get the css info
cssstr , classlst = stylexml2css.convert2CSS(flat_xml, fontsize, ph, pw)
file(xname, 'wb').write(cssstr)
if buildXML:
xname = os.path.join(xmlDir, 'other0000.xml')
file(xname, 'wb').write(convert2xml.getXML(dict, otherFile))
print 'Processing Glyphs'
gd = GlyphDict()
filenames = os.listdir(glyphsDir)
filenames = sorted(filenames)
glyfname = os.path.join(svgDir,'glyphs.svg')
glyfile = open(glyfname, 'w')
glyfile.write('<?xml version="1.0" standalone="no"?>\n')
glyfile.write('<!DOCTYPE svg PUBLIC "-//W3C/DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
glyfile.write('<svg width="512" height="512" viewBox="0 0 511 511" xmlns="http://www.w3.org/2000/svg" version="1.1">\n')
glyfile.write('<title>Glyphs for %s</title>\n' % meta_array['Title'])
glyfile.write('<defs>\n')
counter = 0
for filename in filenames:
# print ' ', filename
print '.',
fname = os.path.join(glyphsDir,filename)
flat_xml = convert2xml.fromData(dict, fname)
if buildXML:
xname = os.path.join(xmlDir, filename.replace('.dat','.xml'))
file(xname, 'wb').write(convert2xml.getXML(dict, fname))
gp = GParser(flat_xml)
for i in xrange(0, gp.count):
path = gp.getPath(i)
maxh, maxw = gp.getGlyphDim(i)
fullpath = '<path id="gl%d" d="%s" fill="black" /><!-- width=%d height=%d -->\n' % (counter * 256 + i, path, maxw, maxh)
glyfile.write(fullpath)
gd.addGlyph(counter * 256 + i, fullpath)
counter += 1
glyfile.write('</defs>\n')
glyfile.write('</svg>\n')
glyfile.close()
print " "
# start up the html
# also build up tocentries while processing html
htmlFileName = "book.html"
hlst = []
hlst.append('<?xml version="1.0" encoding="utf-8"?>\n')
hlst.append('<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.1 Strict//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11-strict.dtd">\n')
hlst.append('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">\n')
hlst.append('<head>\n')
hlst.append('<meta http-equiv="content-type" content="text/html; charset=utf-8"/>\n')
hlst.append('<title>' + meta_array['Title'] + ' by ' + meta_array['Authors'] + '</title>\n')
hlst.append('<meta name="Author" content="' + meta_array['Authors'] + '" />\n')
hlst.append('<meta name="Title" content="' + meta_array['Title'] + '" />\n')
if 'ASIN' in meta_array:
hlst.append('<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n')
if 'GUID' in meta_array:
hlst.append('<meta name="GUID" content="' + meta_array['GUID'] + '" />\n')
hlst.append('<link href="style.css" rel="stylesheet" type="text/css" />\n')
hlst.append('</head>\n<body>\n')
print 'Processing Pages'
# Books are at 1440 DPI. This is rendering at twice that size for
# readability when rendering to the screen.
scaledpi = 1440.0
filenames = os.listdir(pageDir)
filenames = sorted(filenames)
numfiles = len(filenames)
xmllst = []
elst = []
for filename in filenames:
# print ' ', filename
print ".",
fname = os.path.join(pageDir,filename)
flat_xml = convert2xml.fromData(dict, fname)
# keep flat_xml for later svg processing
xmllst.append(flat_xml)
if buildXML:
xname = os.path.join(xmlDir, filename.replace('.dat','.xml'))
file(xname, 'wb').write(convert2xml.getXML(dict, fname))
# first get the html
pagehtml, tocinfo = flatxml2html.convert2HTML(flat_xml, classlst, fname, bookDir, gd, fixedimage)
elst.append(tocinfo)
hlst.append(pagehtml)
# finish up the html string and output it
hlst.append('</body>\n</html>\n')
htmlstr = "".join(hlst)
hlst = None
file(os.path.join(bookDir, htmlFileName), 'wb').write(htmlstr)
print " "
print 'Extracting Table of Contents from Amazon OCR'
# first create a table of contents file for the svg images
tlst = []
tlst.append('<?xml version="1.0" encoding="utf-8"?>\n')
tlst.append('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n')
tlst.append('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" >')
tlst.append('<head>\n')
tlst.append('<title>' + meta_array['Title'] + '</title>\n')
tlst.append('<meta name="Author" content="' + meta_array['Authors'] + '" />\n')
tlst.append('<meta name="Title" content="' + meta_array['Title'] + '" />\n')
if 'ASIN' in meta_array:
tlst.append('<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n')
if 'GUID' in meta_array:
tlst.append('<meta name="GUID" content="' + meta_array['GUID'] + '" />\n')
tlst.append('</head>\n')
tlst.append('<body>\n')
tlst.append('<h2>Table of Contents</h2>\n')
start = pageidnums[0]
def convert2SVG(gdict, flat_xml, pageid, previd, nextid, svgDir, raw, meta_array, scaledpi):
mlst = []
pp = PParser(gdict, flat_xml, meta_array)
mlst.append('<?xml version="1.0" standalone="no"?>\n')
if (raw):
startname = 'page%04d.svg' % start
mlst.append('<!DOCTYPE svg PUBLIC "-//W3C/DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
mlst.append('<svg width="%fin" height="%fin" viewBox="0 0 %d %d" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">\n' % (pp.pw / scaledpi, pp.ph / scaledpi, pp.pw -1, pp.ph -1))
mlst.append('<title>Page %d - %s by %s</title>\n' % (pageid, meta_array['Title'],meta_array['Authors']))
else:
startname = 'page%04d.xhtml' % start
tlst.append('<h3><a href="' + startname + '">Start of Book</a></h3>\n')
# build up a table of contents for the svg xhtml output
tocentries = "".join(elst)
elst = None
toclst = tocentries.split('\n')
toclst.pop()
for entry in toclst:
print entry
title, pagenum = entry.split('|')
id = pageidnums[int(pagenum)]
if (raw):
fname = 'page%04d.svg' % id
mlst.append('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n')
mlst.append('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" ><head>\n')
mlst.append('<title>Page %d - %s by %s</title>\n' % (pageid, meta_array['Title'],meta_array['Authors']))
mlst.append('<script><![CDATA[\n')
mlst.append('function gd(){var p=window.location.href.replace(/^.*\?dpi=(\d+).*$/i,"$1");return p;}\n')
mlst.append('var dpi=%d;\n' % scaledpi)
if (previd) :
mlst.append('var prevpage="page%04d.xhtml";\n' % (previd))
if (nextid) :
mlst.append('var nextpage="page%04d.xhtml";\n' % (nextid))
mlst.append('var pw=%d;var ph=%d;' % (pp.pw, pp.ph))
mlst.append('function zoomin(){dpi=dpi*(0.8);setsize();}\n')
mlst.append('function zoomout(){dpi=dpi*1.25;setsize();}\n')
mlst.append('function setsize(){var svg=document.getElementById("svgimg");var prev=document.getElementById("prevsvg");var next=document.getElementById("nextsvg");var width=(pw/dpi)+"in";var height=(ph/dpi)+"in";svg.setAttribute("width",width);svg.setAttribute("height",height);prev.setAttribute("height",height);prev.setAttribute("width","50px");next.setAttribute("height",height);next.setAttribute("width","50px");}\n')
mlst.append('function ppage(){window.location.href=prevpage+"?dpi="+Math.round(dpi);}\n')
mlst.append('function npage(){window.location.href=nextpage+"?dpi="+Math.round(dpi);}\n')
mlst.append('var gt=gd();if(gt>0){dpi=gt;}\n')
mlst.append('window.onload=setsize;\n')
mlst.append(']]></script>\n')
mlst.append('</head>\n')
mlst.append('<body onLoad="setsize();" style="background-color:#777;text-align:center;">\n')
mlst.append('<div style="white-space:nowrap;">\n')
if previd == None:
mlst.append('<a href="javascript:ppage();"><svg id="prevsvg" viewBox="0 0 100 300" xmlns="http://www.w3.org/2000/svg" version="1.1" style="background-color:#777"></svg></a>\n')
else:
fname = 'page%04d.xhtml' % id
tlst.append('<h3><a href="'+ fname + '">' + title + '</a></h3>\n')
tlst.append('</body>\n')
tlst.append('</html>\n')
tochtml = "".join(tlst)
file(os.path.join(svgDir, 'toc.xhtml'), 'wb').write(tochtml)
mlst.append('<a href="javascript:ppage();"><svg id="prevsvg" viewBox="0 0 100 300" xmlns="http://www.w3.org/2000/svg" version="1.1" style="background-color:#777"><polygon points="5,150,95,5,95,295" fill="#AAAAAA" /></svg></a>\n')
# now create index_svg.xhtml that points to all required files
slst = []
slst.append('<?xml version="1.0" encoding="utf-8"?>\n')
slst.append('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n')
slst.append('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" >')
slst.append('<head>\n')
slst.append('<title>' + meta_array['Title'] + '</title>\n')
slst.append('<meta name="Author" content="' + meta_array['Authors'] + '" />\n')
slst.append('<meta name="Title" content="' + meta_array['Title'] + '" />\n')
if 'ASIN' in meta_array:
slst.append('<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n')
if 'GUID' in meta_array:
slst.append('<meta name="GUID" content="' + meta_array['GUID'] + '" />\n')
slst.append('</head>\n')
slst.append('<body>\n')
print "Building svg images of each book page"
slst.append('<h2>List of Pages</h2>\n')
slst.append('<div>\n')
idlst = sorted(pageIDMap.keys())
numids = len(idlst)
cnt = len(idlst)
previd = None
for j in range(cnt):
pageid = idlst[j]
if j < cnt - 1:
nextid = idlst[j+1]
else:
nextid = None
print '.',
pagelst = pageIDMap[pageid]
flst = []
for page in pagelst:
flst.append(xmllst[page])
flat_svg = "".join(flst)
flst=None
svgxml = flatxml2svg.convert2SVG(gd, flat_svg, pageid, previd, nextid, svgDir, raw, meta_array, scaledpi)
if (raw) :
pfile = open(os.path.join(svgDir,'page%04d.svg' % pageid),'w')
slst.append('<a href="svg/page%04d.svg">Page %d</a>\n' % (pageid, pageid))
mlst.append('<a href="javascript:npage();"><svg id="svgimg" viewBox="0 0 %d %d" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" style="background-color:#FFF;border:1px solid black;">' % (pp.pw, pp.ph))
if (pp.gid != None):
mlst.append('<defs>\n')
gdefs = pp.getGlyphs()
for j in xrange(0,len(gdefs)):
mlst.append(gdefs[j])
mlst.append('</defs>\n')
img = pp.getImages()
if (img != None):
for j in xrange(0,len(img)):
mlst.append(img[j])
if (pp.gid != None):
for j in xrange(0,len(pp.gid)):
mlst.append('<use xlink:href="#gl%d" x="%d" y="%d" />\n' % (pp.gid[j], pp.gx[j], pp.gy[j]))
if (img == None or len(img) == 0) and (pp.gid == None or len(pp.gid) == 0):
xpos = "%d" % (pp.pw // 3)
ypos = "%d" % (pp.ph // 3)
mlst.append('<text x="' + xpos + '" y="' + ypos + '" font-size="' + meta_array['fontSize'] + '" font-family="Helvetica" stroke="black">This page intentionally left blank.</text>\n')
if (raw) :
mlst.append('</svg>')
else :
mlst.append('</svg></a>\n')
if nextid == None:
mlst.append('<a href="javascript:npage();"><svg id="nextsvg" viewBox="0 0 100 300" xmlns="http://www.w3.org/2000/svg" version="1.1" style="background-color:#777"></svg></a>\n')
else :
pfile = open(os.path.join(svgDir,'page%04d.xhtml' % pageid), 'w')
slst.append('<a href="svg/page%04d.xhtml">Page %d</a>\n' % (pageid, pageid))
previd = pageid
pfile.write(svgxml)
pfile.close()
counter += 1
slst.append('</div>\n')
slst.append('<h2><a href="svg/toc.xhtml">Table of Contents</a></h2>\n')
slst.append('</body>\n</html>\n')
svgindex = "".join(slst)
slst = None
file(os.path.join(bookDir, 'index_svg.xhtml'), 'wb').write(svgindex)
print " "
# build the opf file
opfname = os.path.join(bookDir, 'book.opf')
olst = []
olst.append('<?xml version="1.0" encoding="utf-8"?>\n')
olst.append('<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="guid_id">\n')
# adding metadata
olst.append(' <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">\n')
if 'GUID' in meta_array:
olst.append(' <dc:identifier opf:scheme="GUID" id="guid_id">' + meta_array['GUID'] + '</dc:identifier>\n')
if 'ASIN' in meta_array:
olst.append(' <dc:identifier opf:scheme="ASIN">' + meta_array['ASIN'] + '</dc:identifier>\n')
if 'oASIN' in meta_array:
olst.append(' <dc:identifier opf:scheme="oASIN">' + meta_array['oASIN'] + '</dc:identifier>\n')
olst.append(' <dc:title>' + meta_array['Title'] + '</dc:title>\n')
olst.append(' <dc:creator opf:role="aut">' + meta_array['Authors'] + '</dc:creator>\n')
olst.append(' <dc:language>en</dc:language>\n')
olst.append(' <dc:date>' + meta_array['UpdateTime'] + '</dc:date>\n')
if isCover:
olst.append(' <meta name="cover" content="bookcover"/>\n')
olst.append(' </metadata>\n')
olst.append('<manifest>\n')
olst.append(' <item id="book" href="book.html" media-type="application/xhtml+xml"/>\n')
olst.append(' <item id="stylesheet" href="style.css" media-type="text/css"/>\n')
# adding image files to manifest
filenames = os.listdir(imgDir)
filenames = sorted(filenames)
for filename in filenames:
imgname, imgext = os.path.splitext(filename)
if imgext == '.jpg':
imgext = 'jpeg'
if imgext == '.svg':
imgext = 'svg+xml'
olst.append(' <item id="' + imgname + '" href="img/' + filename + '" media-type="image/' + imgext + '"/>\n')
if isCover:
olst.append(' <item id="bookcover" href="cover.jpg" media-type="image/jpeg" />\n')
olst.append('</manifest>\n')
# adding spine
olst.append('<spine>\n <itemref idref="book" />\n</spine>\n')
if isCover:
olst.append(' <guide>\n')
olst.append(' <reference href="cover.jpg" type="cover" title="Cover"/>\n')
olst.append(' </guide>\n')
olst.append('</package>\n')
opfstr = "".join(olst)
olst = None
file(opfname, 'wb').write(opfstr)
print 'Processing Complete'
return 0
def usage():
print "genbook.py generates a book from the extract Topaz Files"
print "Usage:"
print " genbook.py [-r] [-h [--fixed-image] <bookDir> "
print " "
print "Options:"
print " -h : help - print this usage message"
print " -r : generate raw svg files (not wrapped in xhtml)"
print " --fixed-image : genearate any Fixed Area as an svg image in the html"
print " "
def main(argv):
bookDir = ''
if len(argv) == 0:
argv = sys.argv
try:
opts, args = getopt.getopt(argv[1:], "rh:",["fixed-image"])
except getopt.GetoptError, err:
print str(err)
usage()
return 1
if len(opts) == 0 and len(args) == 0 :
usage()
return 1
raw = 0
fixedimage = True
for o, a in opts:
if o =="-h":
usage()
return 0
if o =="-r":
raw = 1
if o =="--fixed-image":
fixedimage = True
bookDir = args[0]
rv = generateBook(bookDir, raw, fixedimage)
return rv
if __name__ == '__main__':
sys.exit(main(''))
mlst.append('<a href="javascript:npage();"><svg id="nextsvg" viewBox="0 0 100 300" xmlns="http://www.w3.org/2000/svg" version="1.1" style="background-color:#777"><polygon points="5,5,5,295,95,150" fill="#AAAAAA" /></svg></a>\n')
mlst.append('</div>\n')
mlst.append('<div><a href="javascript:zoomin();">zoom in</a> - <a href="javascript:zoomout();">zoom out</a></div>\n')
mlst.append('</body>\n')
mlst.append('</html>\n')
return "".join(mlst)

File diff suppressed because it is too large Load Diff

View File

@@ -3,47 +3,54 @@
from __future__ import with_statement
# ignoblekeygen.pyw, version 2.5
# Copyright © 2009-2010 i♥cabbages
# ignobleepub.pyw, version 3.8
# Copyright © 2009-2010 by i♥cabbages
# Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/>
# Modified 20102013 by some_updates, DiapDealer and Apprentice Alf
# Windows users: Before running this program, you must first install Python.
# We recommend ActiveState Python 2.7.X for Windows (x86) from
# http://www.activestate.com/activepython/downloads.
# You must also install PyCrypto from
# http://www.voidspace.org.uk/python/modules.shtml#pycrypto
# (make certain to install the version for Python 2.7).
# Then save this script file as ignoblekeygen.pyw and double-click on it to run it.
# 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 ignoblekeygen.pyw. You can run this
# program from the command line (python ignoblekeygen.pyw) or by double-clicking
# Mac OS X users: Save this script file as ineptepub.pyw. You can run this
# program from the command line (pythonw ineptepub.pyw) or by double-clicking
# it when it has been associated with PythonLauncher.
# Revision history:
# 1 - Initial release
# 2 - Add OS X support by using OpenSSL when available (taken/modified from ineptepub v5)
# 2.1 - Allow Windows versions of libcrypto to be found
# 2.2 - On Windows try PyCrypto first and then OpenSSL next
# 2.3 - Modify interface to allow use of import
# 2.4 - Improvements to UI and now works in plugins
# 2.5 - Additional improvement for unicode and plugin support
# 2.6 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 2.7 - Work if TkInter is missing
# 2 - Added OS X support by using OpenSSL when available
# 3 - screen out improper key lengths to prevent segfaults on Linux
# 3.1 - Allow Windows versions of libcrypto to be found
# 3.2 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml
# 3.3 - On Windows try PyCrypto first, OpenSSL next
# 3.4 - Modify interface to allow use with import
# 3.5 - Fix for potential problem with PyCrypto
# 3.6 - Revised to allow use in calibre plugins to eliminate need for duplicate code
# 3.7 - Tweaked to match ineptepub more closely
# 3.8 - Fixed to retain zip file metadata (e.g. file modification date)
# 3.9 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 4.0 - Work if TkInter is missing
"""
Generate Barnes & Noble EPUB user key from name and credit card number.
Decrypt Barnes & Noble encrypted ePub books.
"""
__license__ = 'GPL v3'
__version__ = "2.7"
__version__ = "4.0"
import sys
import os
import hashlib
import traceback
import zlib
import zipfile
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
from contextlib import closing
import xml.etree.ElementTree as etree
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
@@ -75,8 +82,8 @@ def unicode_argv():
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
# as a list of Unicode strings and encode them as utf-8
# characters with '?'.
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
@@ -97,9 +104,7 @@ def unicode_argv():
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"ignoblekeygen.py"]
return [u"ineptepub.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
@@ -140,26 +145,29 @@ def _load_crypto_libcrypto():
func.argtypes = argtypes
return func
AES_set_encrypt_key = F(c_int, 'AES_set_encrypt_key',
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
[c_char_p, c_int, AES_KEY_p])
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
c_int])
class AES(object):
def __init__(self, userkey, iv):
def __init__(self, userkey):
self._blocksize = len(userkey)
self._iv = iv
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
raise IGNOBLEError('AES improper key used')
return
key = self._key = AES_KEY()
rv = AES_set_encrypt_key(userkey, len(userkey) * 8, key)
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
if rv < 0:
raise IGNOBLEError('Failed to initialize AES Encrypt key')
raise IGNOBLEError('Failed to initialize AES key')
def encrypt(self, data):
def decrypt(self, data):
out = create_string_buffer(len(data))
rv = AES_cbc_encrypt(data, out, len(data), self._key, self._iv, 1)
iv = ("\x00" * self._blocksize)
rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
if rv == 0:
raise IGNOBLEError('AES encryption failed')
raise IGNOBLEError('AES decryption failed')
return out.raw
return AES
@@ -168,11 +176,11 @@ def _load_crypto_pycrypto():
from Crypto.Cipher import AES as _AES
class AES(object):
def __init__(self, key, iv):
self._aes = _AES.new(key, _AES.MODE_CBC, iv)
def __init__(self, key):
self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16)
def encrypt(self, data):
return self._aes.encrypt(data)
def decrypt(self, data):
return self._aes.decrypt(data)
return AES
@@ -191,29 +199,123 @@ def _load_crypto():
AES = _load_crypto()
def normalize_name(name):
return ''.join(x for x in name.lower() if x != ' ')
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
NSMAP = {'adept': 'http://ns.adobe.com/adept',
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
class Decryptor(object):
def __init__(self, bookkey, encryption):
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
self._aes = AES(bookkey)
encryption = etree.fromstring(encryption)
self._encrypted = encrypted = set()
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
enc('CipherReference'))
for elem in encryption.findall(expr):
path = elem.get('URI', None)
if path is not None:
path = path.encode('utf-8')
encrypted.add(path)
def 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')
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
name = normalize_name(name) + '\x00'
ccn = normalize_name(ccn) + '\x00'
name_sha = hashlib.sha1(name).digest()[:16]
ccn_sha = hashlib.sha1(ccn).digest()[:16]
both_sha = hashlib.sha1(name + ccn).digest()
aes = AES(ccn_sha, name_sha)
crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c))
userkey = hashlib.sha1(crypt).digest()
return userkey.encode('base64')
def decrypt(self, path, data):
if path in self._encrypted:
data = self._aes.decrypt(data)[16:]
data = data[:-ord(data[-1])]
data = self.decompress(data)
return data
# check file to make check whether it's probably an Adobe Adept encrypted ePub
def ignobleBook(inpath):
with closing(ZipFile(open(inpath, 'rb'))) as inf:
namelist = set(inf.namelist())
if 'META-INF/rights.xml' not in namelist or \
'META-INF/encryption.xml' not in namelist:
return False
try:
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
expr = './/%s' % (adept('encryptedKey'),)
bookkey = ''.join(rights.findtext(expr))
if len(bookkey) == 64:
return True
except:
# if we couldn't check, assume it is
return True
return False
def decryptBook(keyb64, inpath, outpath):
if AES is None:
raise IGNOBLEError(u"PyCrypto or OpenSSL must be installed.")
key = keyb64.decode('base64')[:16]
aes = AES(key)
with closing(ZipFile(open(inpath, 'rb'))) as inf:
namelist = set(inf.namelist())
if 'META-INF/rights.xml' not in namelist or \
'META-INF/encryption.xml' not in namelist:
print u"{0:s} is DRM-free.".format(os.path.basename(inpath))
return 1
for name in META_NAMES:
namelist.remove(name)
try:
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
expr = './/%s' % (adept('encryptedKey'),)
bookkey = ''.join(rights.findtext(expr))
if len(bookkey) != 64:
print u"{0:s} is not a secure Barnes & Noble ePub.".format(os.path.basename(inpath))
return 1
bookkey = aes.decrypt(bookkey.decode('base64'))
bookkey = bookkey[:-ord(bookkey[-1])]
encryption = inf.read('META-INF/encryption.xml')
decryptor = Decryptor(bookkey[-16:], encryption)
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
zi = ZipInfo('mimetype')
zi.compress_type=ZIP_STORED
try:
# if the mimetype is present, get its info, including time-stamp
oldzi = inf.getinfo('mimetype')
# copy across fields to be preserved
zi.date_time = oldzi.date_time
zi.comment = oldzi.comment
zi.extra = oldzi.extra
zi.internal_attr = oldzi.internal_attr
# external attributes are dependent on the create system, so copy both.
zi.external_attr = oldzi.external_attr
zi.create_system = oldzi.create_system
except:
pass
outf.writestr(zi, inf.read('mimetype'))
for path in namelist:
data = inf.read(path)
zi = ZipInfo(path)
zi.compress_type=ZIP_DEFLATED
try:
# get the file info, including time-stamp
oldzi = inf.getinfo(path)
# copy across useful fields
zi.date_time = oldzi.date_time
zi.comment = oldzi.comment
zi.extra = oldzi.extra
zi.internal_attr = oldzi.internal_attr
# external attributes are dependent on the create system, so copy both.
zi.external_attr = oldzi.external_attr
zi.create_system = oldzi.create_system
except:
pass
outf.writestr(zi, decryptor.decrypt(path, data))
except:
print u"Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc())
return 2
return 0
def cli_main():
@@ -221,19 +323,15 @@ def cli_main():
sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv()
progname = os.path.basename(argv[0])
if AES is None:
print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \
"separately. Read the top-of-script comment for details." % \
(progname,)
return 1
if len(argv) != 4:
print u"usage: {0} <Name> <CC#> <keyfileout.b64>".format(progname)
print u"usage: {0} <keyfile.b64> <inbook.epub> <outbook.epub>".format(progname)
return 1
name, ccn, keypath = argv[1:]
userkey = generate_key(name, ccn)
open(keypath,'wb').write(userkey)
return 0
keypath, inpath, outpath = argv[1:]
userkey = open(keypath,'rb').read()
result = decryptBook(userkey, inpath, outpath)
if result == 0:
print u"Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath))
return result
def gui_main():
try:
@@ -247,28 +345,33 @@ def gui_main():
class DecryptionDialog(Tkinter.Frame):
def __init__(self, root):
Tkinter.Frame.__init__(self, root, border=5)
self.status = Tkinter.Label(self, text=u"Enter parameters")
self.status = Tkinter.Label(self, text=u"Select files for decryption")
self.status.pack(fill=Tkconstants.X, expand=1)
body = Tkinter.Frame(self)
body.pack(fill=Tkconstants.X, expand=1)
sticky = Tkconstants.E + Tkconstants.W
body.grid_columnconfigure(1, weight=2)
Tkinter.Label(body, text=u"Account Name").grid(row=0)
self.name = Tkinter.Entry(body, width=40)
self.name.grid(row=0, column=1, sticky=sticky)
Tkinter.Label(body, text=u"CC#").grid(row=1)
self.ccn = Tkinter.Entry(body, width=40)
self.ccn.grid(row=1, column=1, sticky=sticky)
Tkinter.Label(body, text=u"Output file").grid(row=2)
self.keypath = Tkinter.Entry(body, width=40)
self.keypath.grid(row=2, column=1, sticky=sticky)
self.keypath.insert(2, u"bnepubkey.b64")
Tkinter.Label(body, text=u"Key file").grid(row=0)
self.keypath = Tkinter.Entry(body, width=30)
self.keypath.grid(row=0, column=1, sticky=sticky)
if os.path.exists(u"bnepubkey.b64"):
self.keypath.insert(0, u"bnepubkey.b64")
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
button.grid(row=0, column=2)
Tkinter.Label(body, text=u"Input file").grid(row=1)
self.inpath = Tkinter.Entry(body, width=30)
self.inpath.grid(row=1, column=1, sticky=sticky)
button = Tkinter.Button(body, text=u"...", command=self.get_inpath)
button.grid(row=1, column=2)
Tkinter.Label(body, text=u"Output file").grid(row=2)
self.outpath = Tkinter.Entry(body, width=30)
self.outpath.grid(row=2, column=1, sticky=sticky)
button = Tkinter.Button(body, text=u"...", command=self.get_outpath)
button.grid(row=2, column=2)
buttons = Tkinter.Frame(self)
buttons.pack()
botton = Tkinter.Button(
buttons, text=u"Generate", width=10, command=self.generate)
buttons, text=u"Decrypt", width=10, command=self.decrypt)
botton.pack(side=Tkconstants.LEFT)
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
button = Tkinter.Button(
@@ -276,8 +379,8 @@ def gui_main():
button.pack(side=Tkconstants.RIGHT)
def get_keypath(self):
keypath = tkFileDialog.asksaveasfilename(
parent=None, title=u"Select B&N ePub key file to produce",
keypath = tkFileDialog.askopenfilename(
parent=None, title=u"Select Barnes & Noble \'.b64\' key file",
defaultextension=u".b64",
filetypes=[('base64-encoded files', '.b64'),
('All Files', '.*')])
@@ -287,37 +390,56 @@ def gui_main():
self.keypath.insert(0, keypath)
return
def generate(self):
name = self.name.get()
ccn = self.ccn.get()
def get_inpath(self):
inpath = tkFileDialog.askopenfilename(
parent=None, title=u"Select B&N-encrypted ePub file to decrypt",
defaultextension=u".epub", filetypes=[('ePub files', '.epub')])
if inpath:
inpath = os.path.normpath(inpath)
self.inpath.delete(0, Tkconstants.END)
self.inpath.insert(0, inpath)
return
def get_outpath(self):
outpath = tkFileDialog.asksaveasfilename(
parent=None, title=u"Select unencrypted ePub file to produce",
defaultextension=u".epub", filetypes=[('ePub files', '.epub')])
if outpath:
outpath = os.path.normpath(outpath)
self.outpath.delete(0, Tkconstants.END)
self.outpath.insert(0, outpath)
return
def decrypt(self):
keypath = self.keypath.get()
if not name:
self.status['text'] = u"Name not specified"
inpath = self.inpath.get()
outpath = self.outpath.get()
if not keypath or not os.path.exists(keypath):
self.status['text'] = u"Specified key file does not exist"
return
if not ccn:
self.status['text'] = u"Credit card number not specified"
if not inpath or not os.path.exists(inpath):
self.status['text'] = u"Specified input file does not exist"
return
if not keypath:
self.status['text'] = u"Output keyfile path not specified"
if not outpath:
self.status['text'] = u"Output file not specified"
return
self.status['text'] = u"Generating..."
if inpath == outpath:
self.status['text'] = u"Must have different input and output files"
return
userkey = open(keypath,'rb').read()
self.status['text'] = u"Decrypting..."
try:
userkey = generate_key(name, ccn)
decrypt_status = decryptBook(userkey, inpath, outpath)
except Exception, e:
self.status['text'] = u"Error: (0}".format(e.args[0])
self.status['text'] = u"Error: {0}".format(e.args[0])
return
open(keypath,'wb').write(userkey)
self.status['text'] = u"Keyfile successfully generated"
if decrypt_status == 0:
self.status['text'] = u"File successfully decrypted"
else:
self.status['text'] = u"The was an error decrypting the file."
root = Tkinter.Tk()
if AES is None:
root.withdraw()
tkMessageBox.showerror(
"Ignoble EPUB Keyfile Generator",
"This script requires OpenSSL or PyCrypto, which must be installed "
"separately. Read the top-of-script comment for details.")
return 1
root.title(u"Barnes & Noble ePub Keyfile Generator v.{0}".format(__version__))
root.title(u"Barnes & Noble ePub Decrypter v.{0}".format(__version__))
root.resizable(True, False)
root.minsize(300, 0)
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)

View File

@@ -0,0 +1,336 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import with_statement
# ignoblekey.py
# Copyright © 2015 Apprentice Alf and Apprentice Harper
# Based on kindlekey.py, Copyright © 2010-2013 by some_updates and Apprentice Alf
# Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/>
# Revision history:
# 1.0 - Initial release
# 1.1 - remove duplicates and return last key as single key
"""
Get Barnes & Noble EPUB user key from nook Studio log file
"""
__license__ = 'GPL v3'
__version__ = "1.1"
import sys
import os
import hashlib
import getopt
import re
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
# encoded using "replace" before writing them.
class SafeUnbuffered:
def __init__(self, stream):
self.stream = stream
self.encoding = stream.encoding
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
try:
from calibre.constants import iswindows, isosx
except:
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
def unicode_argv():
if iswindows:
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
# as a list of Unicode strings and encode them as utf-8
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"ignoblekey.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
class DrmException(Exception):
pass
# Locate all of the nookStudy/nook for PC/Mac log file and return as list
def getNookLogFiles():
logFiles = []
found = False
if iswindows:
import _winreg as winreg
# some 64 bit machines do not have the proper registry key for some reason
# or the python interface to the 32 vs 64 bit registry is broken
paths = set()
if 'LOCALAPPDATA' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
if os.path.isdir(path):
paths.add(path)
if 'USERPROFILE' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
path = winreg.ExpandEnvironmentStrings(u"%USERPROFILE%")+u"\\AppData\\Local"
if os.path.isdir(path):
paths.add(path)
path = winreg.ExpandEnvironmentStrings(u"%USERPROFILE%")+u"\\AppData\\Roaming"
if os.path.isdir(path):
paths.add(path)
# User Shell Folders show take precedent over Shell Folders if present
try:
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\")
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
if os.path.isdir(path):
paths.add(path)
except WindowsError:
pass
try:
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\")
path = winreg.QueryValueEx(regkey, 'AppData')[0]
if os.path.isdir(path):
paths.add(path)
except WindowsError:
pass
try:
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
if os.path.isdir(path):
paths.add(path)
except WindowsError:
pass
try:
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
path = winreg.QueryValueEx(regkey, 'AppData')[0]
if os.path.isdir(path):
paths.add(path)
except WindowsError:
pass
for path in paths:
# look for nookStudy log file
logpath = path +'\\Barnes & Noble\\NOOKstudy\\logs\\BNClientLog.txt'
if os.path.isfile(logpath):
found = True
print('Found nookStudy log file: ' + logpath.encode('ascii','ignore'))
logFiles.append(logpath)
else:
home = os.getenv('HOME')
# check for BNClientLog.txt in various locations
testpath = home + '/Library/Application Support/Barnes & Noble/DesktopReader/logs/BNClientLog.txt'
if os.path.isfile(testpath):
logFiles.append(testpath)
print('Found nookStudy log file: ' + testpath)
found = True
testpath = home + '/Library/Application Support/Barnes & Noble/DesktopReader/indices/BNClientLog.txt'
if os.path.isfile(testpath):
logFiles.append(testpath)
print('Found nookStudy log file: ' + testpath)
found = True
testpath = home + '/Library/Application Support/Barnes & Noble/BNDesktopReader/logs/BNClientLog.txt'
if os.path.isfile(testpath):
logFiles.append(testpath)
print('Found nookStudy log file: ' + testpath)
found = True
testpath = home + '/Library/Application Support/Barnes & Noble/BNDesktopReader/indices/BNClientLog.txt'
if os.path.isfile(testpath):
logFiles.append(testpath)
print('Found nookStudy log file: ' + testpath)
found = True
if not found:
print('No nook Study log files have been found.')
return logFiles
# Extract CCHash key(s) from log file
def getKeysFromLog(kLogFile):
keys = []
regex = re.compile("ccHash: \"(.{28})\"");
for line in open(kLogFile):
for m in regex.findall(line):
keys.append(m)
return keys
# interface for calibre plugin
def nookkeys(files = []):
keys = []
if files == []:
files = getNookLogFiles()
for file in files:
fileKeys = getKeysFromLog(file)
if fileKeys:
print u"Found {0} keys in the Nook Study log files".format(len(fileKeys))
keys.extend(fileKeys)
return list(set(keys))
# interface for Python DeDRM
# returns single key or multiple keys, depending on path or file passed in
def getkey(outpath, files=[]):
keys = nookkeys(files)
if len(keys) > 0:
if not os.path.isdir(outpath):
outfile = outpath
with file(outfile, 'w') as keyfileout:
keyfileout.write(keys[-1])
print u"Saved a key to {0}".format(outfile)
else:
keycount = 0
for key in keys:
while True:
keycount += 1
outfile = os.path.join(outpath,u"nookkey{0:d}.b64".format(keycount))
if not os.path.exists(outfile):
break
with file(outfile, 'w') as keyfileout:
keyfileout.write(key)
print u"Saved a key to {0}".format(outfile)
return True
return False
def usage(progname):
print u"Finds the nook Study encryption keys."
print u"Keys are saved to the current directory, or a specified output directory."
print u"If a file name is passed instead of a directory, only the first key is saved, in that file."
print u"Usage:"
print u" {0:s} [-h] [-k <logFile>] [<outpath>]".format(progname)
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv()
progname = os.path.basename(argv[0])
print u"{0} v{1}\nCopyright © 2015 Apprentice Alf".format(progname,__version__)
try:
opts, args = getopt.getopt(argv[1:], "hk:")
except getopt.GetoptError, err:
print u"Error in options or arguments: {0}".format(err.args[0])
usage(progname)
sys.exit(2)
files = []
for o, a in opts:
if o == "-h":
usage(progname)
sys.exit(0)
if o == "-k":
files = [a]
if len(args) > 1:
usage(progname)
sys.exit(2)
if len(args) == 1:
# save to the specified file or directory
outpath = args[0]
if not os.path.isabs(outpath):
outpath = os.path.abspath(outpath)
else:
# save to the same directory as the script
outpath = os.path.dirname(argv[0])
# make sure the outpath is the
outpath = os.path.realpath(os.path.normpath(outpath))
if not getkey(outpath, files):
print u"Could not retrieve nook Study key."
return 0
def gui_main():
try:
import Tkinter
import Tkconstants
import tkMessageBox
import traceback
except:
return cli_main()
class ExceptionDialog(Tkinter.Frame):
def __init__(self, root, text):
Tkinter.Frame.__init__(self, root, border=5)
label = Tkinter.Label(self, text=u"Unexpected error:",
anchor=Tkconstants.W, justify=Tkconstants.LEFT)
label.pack(fill=Tkconstants.X, expand=0)
self.text = Tkinter.Text(self)
self.text.pack(fill=Tkconstants.BOTH, expand=1)
self.text.insert(Tkconstants.END, text)
argv=unicode_argv()
root = Tkinter.Tk()
root.withdraw()
progpath, progname = os.path.split(argv[0])
success = False
try:
keys = nookkeys()
keycount = 0
for key in keys:
print key
while True:
keycount += 1
outfile = os.path.join(progpath,u"nookkey{0:d}.b64".format(keycount))
if not os.path.exists(outfile):
break
with file(outfile, 'w') as keyfileout:
keyfileout.write(key)
success = True
tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile))
except DrmException, e:
tkMessageBox.showerror(progname, u"Error: {0}".format(str(e)))
except Exception:
root.wm_state('normal')
root.title(progname)
text = traceback.format_exc()
ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1)
root.mainloop()
if not success:
return 1
return 0
if __name__ == '__main__':
if len(sys.argv) > 1:
sys.exit(cli_main())
sys.exit(gui_main())

View File

@@ -0,0 +1,258 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import with_statement
# ignoblekeyfetch.pyw, version 1.1
# Copyright © 2015 Apprentice Harper
# Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/>
# Based on discoveries by "Nobody You Know"
# Code partly based on ignoblekeygen.py by several people.
# Windows users: Before running this program, you must first install Python.
# We recommend ActiveState Python 2.7.X for Windows from
# http://www.activestate.com/activepython/downloads.
# Then save this script file as ignoblekeyfetch.pyw and double-click on it to run it.
#
# Mac OS X users: Save this script file as ignoblekeyfetch.pyw. You can run this
# program from the command line (python ignoblekeyfetch.pyw) or by double-clicking
# it when it has been associated with PythonLauncher.
# Revision history:
# 1.0 - Initial version
# 1.1 - Try second URL if first one fails
"""
Fetch Barnes & Noble EPUB user key from B&N servers using email and password
"""
__license__ = 'GPL v3'
__version__ = "1.1"
import sys
import os
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
# encoded using "replace" before writing them.
class SafeUnbuffered:
def __init__(self, stream):
self.stream = stream
self.encoding = stream.encoding
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
try:
from calibre.constants import iswindows, isosx
except:
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
def unicode_argv():
if iswindows:
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
# as a list of Unicode strings and encode them as utf-8
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"ignoblekeyfetch.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
class IGNOBLEError(Exception):
pass
def fetch_key(email, password):
# change email and password to utf-8 if unicode
if type(email)==unicode:
email = email.encode('utf-8')
if type(password)==unicode:
password = password.encode('utf-8')
import random
random = "%030x" % random.randrange(16**30)
import urllib, urllib2, re
# try the URL from nook for PC
fetch_url = "https://cart4.barnesandnoble.com/services/service.aspx?Version=2&acctPassword="
fetch_url += urllib.quote(password,'')+"&devID=PC_BN_2.5.6.9575_"+random+"&emailAddress="
fetch_url += urllib.quote(email,"")+"&outFormat=5&schema=1&service=1&stage=deviceHashB"
#print fetch_url
found = ''
try:
req = urllib2.Request(fetch_url)
response = urllib2.urlopen(req)
the_page = response.read()
#print the_page
found = re.search('ccHash>(.+?)</ccHash', the_page).group(1)
except:
found = ''
if len(found)!=28:
# try the URL from android devices
fetch_url = "https://cart4.barnesandnoble.com/services/service.aspx?Version=2&acctPassword="
fetch_url += urllib.quote(password,'')+"&devID=hobbes_9.3.50818_"+random+"&emailAddress="
fetch_url += urllib.quote(email,"")+"&outFormat=5&schema=1&service=1&stage=deviceHashB"
#print fetch_url
found = ''
try:
req = urllib2.Request(fetch_url)
response = urllib2.urlopen(req)
the_page = response.read()
#print the_page
found = re.search('ccHash>(.+?)</ccHash', the_page).group(1)
except:
found = ''
return found
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv()
progname = os.path.basename(argv[0])
if len(argv) != 4:
print u"usage: {0} <email> <password> <keyfileout.b64>".format(progname)
return 1
email, password, keypath = argv[1:]
userkey = fetch_key(email, password)
if len(userkey) == 28:
open(keypath,'wb').write(userkey)
return 0
print u"Failed to fetch key."
return 1
def gui_main():
try:
import Tkinter
import tkFileDialog
import Tkconstants
import tkMessageBox
import traceback
except:
return cli_main()
class DecryptionDialog(Tkinter.Frame):
def __init__(self, root):
Tkinter.Frame.__init__(self, root, border=5)
self.status = Tkinter.Label(self, text=u"Enter parameters")
self.status.pack(fill=Tkconstants.X, expand=1)
body = Tkinter.Frame(self)
body.pack(fill=Tkconstants.X, expand=1)
sticky = Tkconstants.E + Tkconstants.W
body.grid_columnconfigure(1, weight=2)
Tkinter.Label(body, text=u"Account email address").grid(row=0)
self.name = Tkinter.Entry(body, width=40)
self.name.grid(row=0, column=1, sticky=sticky)
Tkinter.Label(body, text=u"Account password").grid(row=1)
self.ccn = Tkinter.Entry(body, width=40)
self.ccn.grid(row=1, column=1, sticky=sticky)
Tkinter.Label(body, text=u"Output file").grid(row=2)
self.keypath = Tkinter.Entry(body, width=40)
self.keypath.grid(row=2, column=1, sticky=sticky)
self.keypath.insert(2, u"bnepubkey.b64")
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
button.grid(row=2, column=2)
buttons = Tkinter.Frame(self)
buttons.pack()
botton = Tkinter.Button(
buttons, text=u"Fetch", width=10, command=self.generate)
botton.pack(side=Tkconstants.LEFT)
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
button = Tkinter.Button(
buttons, text=u"Quit", width=10, command=self.quit)
button.pack(side=Tkconstants.RIGHT)
def get_keypath(self):
keypath = tkFileDialog.asksaveasfilename(
parent=None, title=u"Select B&N ePub key file to produce",
defaultextension=u".b64",
filetypes=[('base64-encoded files', '.b64'),
('All Files', '.*')])
if keypath:
keypath = os.path.normpath(keypath)
self.keypath.delete(0, Tkconstants.END)
self.keypath.insert(0, keypath)
return
def generate(self):
email = self.name.get()
password = self.ccn.get()
keypath = self.keypath.get()
if not email:
self.status['text'] = u"Email address not given"
return
if not password:
self.status['text'] = u"Account password not given"
return
if not keypath:
self.status['text'] = u"Output keyfile path not set"
return
self.status['text'] = u"Fetching..."
try:
userkey = fetch_key(email, password)
except Exception, e:
self.status['text'] = u"Error: {0}".format(e.args[0])
return
if len(userkey) == 28:
open(keypath,'wb').write(userkey)
self.status['text'] = u"Keyfile fetched successfully"
else:
self.status['text'] = u"Keyfile fetch failed."
root = Tkinter.Tk()
root.title(u"Barnes & Noble ePub Keyfile Fetch v.{0}".format(__version__))
root.resizable(True, False)
root.minsize(300, 0)
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
root.mainloop()
return 0
if __name__ == '__main__':
if len(sys.argv) > 1:
sys.exit(cli_main())
sys.exit(gui_main())

View File

@@ -3,56 +3,48 @@
from __future__ import with_statement
# ineptepub.pyw, version 6.1
# Copyright © 2009-2010 by i♥cabbages
# ignoblekeygen.pyw, version 2.5
# Copyright © 2009-2010 i♥cabbages
# Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/>
# Modified 20102013 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.
# Windows users: Before running this program, you must first install Python.
# We recommend ActiveState Python 2.7.X for Windows (x86) from
# http://www.activestate.com/activepython/downloads.
# You must also install PyCrypto from
# http://www.voidspace.org.uk/python/modules.shtml#pycrypto
# (make certain to install the version for Python 2.7).
# Then save this script file as ignoblekeygen.pyw and double-click on it to run it.
#
# Mac OS X users: Save this script file as ineptepub.pyw. You can run this
# program from the command line (pythonw ineptepub.pyw) or by double-clicking
# Mac OS X users: Save this script file as ignoblekeygen.pyw. You can run this
# program from the command line (python ignoblekeygen.pyw) or by double-clicking
# it when it has been associated with PythonLauncher.
# Revision history:
# 1 - Initial release
# 2 - Rename to INEPT, fix exit code
# 5 - Version bump to avoid (?) confusion;
# Improve OS X support by using OpenSSL when available
# 5.1 - Improve OpenSSL error checking
# 5.2 - Fix ctypes error causing segfaults on some systems
# 5.3 - add support for OpenSSL on Windows, fix bug with some versions of libcrypto 0.9.8 prior to path level o
# 5.4 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml
# 5.5 - On Windows try PyCrypto first, OpenSSL next
# 5.6 - Modify interface to allow use with import
# 5.7 - Fix for potential problem with PyCrypto
# 5.8 - Revised to allow use in calibre plugins to eliminate need for duplicate code
# 5.9 - Fixed to retain zip file metadata (e.g. file modification date)
# 6.0 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 6.1 - Work if TkInter is missing
# 2 - Add OS X support by using OpenSSL when available (taken/modified from ineptepub v5)
# 2.1 - Allow Windows versions of libcrypto to be found
# 2.2 - On Windows try PyCrypto first and then OpenSSL next
# 2.3 - Modify interface to allow use of import
# 2.4 - Improvements to UI and now works in plugins
# 2.5 - Additional improvement for unicode and plugin support
# 2.6 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 2.7 - Work if TkInter is missing
# 2.8 - Fix bug in stand-alone use (import tkFileDialog)
"""
Decrypt Adobe Digital Editions encrypted ePub books.
Generate Barnes & Noble EPUB user key from name and credit card number.
"""
__license__ = 'GPL v3'
__version__ = "6.1"
__version__ = "2.8"
import sys
import os
import traceback
import zlib
import zipfile
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
from contextlib import closing
import xml.etree.ElementTree as etree
import hashlib
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
@@ -84,8 +76,8 @@ def unicode_argv():
# 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 '?'.
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
# as a list of Unicode strings and encode them as utf-8
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
@@ -106,7 +98,9 @@ def unicode_argv():
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
return [u"ineptepub.py"]
# 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:
@@ -114,7 +108,7 @@ def unicode_argv():
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
class ADEPTError(Exception):
class IGNOBLEError(Exception):
pass
def _load_crypto_libcrypto():
@@ -128,19 +122,14 @@ def _load_crypto_libcrypto():
libcrypto = find_library('crypto')
if libcrypto is None:
raise ADEPTError('libcrypto not found')
raise IGNOBLEError('libcrypto not found')
libcrypto = CDLL(libcrypto)
RSA_NO_PADDING = 3
AES_MAXNR = 14
c_char_pp = POINTER(c_char_p)
c_int_p = POINTER(c_int)
class RSA(Structure):
pass
RSA_p = POINTER(RSA)
class AES_KEY(Structure):
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
('rounds', c_int)]
@@ -152,312 +141,80 @@ def _load_crypto_libcrypto():
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',
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 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):
def __init__(self, userkey, iv):
self._blocksize = len(userkey)
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
raise ADEPTError('AES improper key used')
return
self._iv = iv
key = self._key = AES_KEY()
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
rv = AES_set_encrypt_key(userkey, len(userkey) * 8, key)
if rv < 0:
raise ADEPTError('Failed to initialize AES key')
raise IGNOBLEError('Failed to initialize AES Encrypt key')
def decrypt(self, data):
def encrypt(self, data):
out = create_string_buffer(len(data))
iv = ("\x00" * self._blocksize)
rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
rv = AES_cbc_encrypt(data, out, len(data), self._key, self._iv, 1)
if rv == 0:
raise ADEPTError('AES decryption failed')
raise IGNOBLEError('AES encryption failed')
return out.raw
return (AES, RSA)
return AES
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 __init__(self, key, iv):
self._aes = _AES.new(key, _AES.MODE_CBC, iv)
def decrypt(self, data):
return self._aes.decrypt(data)
def encrypt(self, data):
return self._aes.encrypt(data)
class RSA(object):
def __init__(self, der):
key = ASN1Parser([ord(x) for x in der])
key = [key.getChild(x).value for x in xrange(1, 4)]
key = [self.bytesToNumber(v) for v in key]
self._rsa = _RSA.construct(key)
def bytesToNumber(self, bytes):
total = 0L
for byte in bytes:
total = (total << 8) + byte
return total
def decrypt(self, data):
return self._rsa.decrypt(data)
return (AES, RSA)
return AES
def _load_crypto():
AES = RSA = None
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, RSA = loader()
AES = loader()
break
except (ImportError, ADEPTError):
except (ImportError, IGNOBLEError):
pass
return (AES, RSA)
return AES
AES, RSA = _load_crypto()
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#'}
def normalize_name(name):
return ''.join(x for x in name.lower() if x != ' ')
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 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')
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
name = normalize_name(name) + '\x00'
ccn = normalize_name(ccn) + '\x00'
name_sha = hashlib.sha1(name).digest()[:16]
ccn_sha = hashlib.sha1(ccn).digest()[:16]
both_sha = hashlib.sha1(name + ccn).digest()
aes = AES(ccn_sha, name_sha)
crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c))
userkey = hashlib.sha1(crypt).digest()
return userkey.encode('base64')
# check file to make check whether it's probably an Adobe Adept encrypted ePub
def adeptBook(inpath):
with closing(ZipFile(open(inpath, 'rb'))) as inf:
namelist = set(inf.namelist())
if 'META-INF/rights.xml' not in namelist or \
'META-INF/encryption.xml' not in namelist:
return False
try:
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
expr = './/%s' % (adept('encryptedKey'),)
bookkey = ''.join(rights.findtext(expr))
if len(bookkey) == 172:
return True
except:
# if we couldn't check, assume it is
return True
return False
def decryptBook(userkey, inpath, outpath):
if AES is None:
raise ADEPTError(u"PyCrypto or OpenSSL must be installed.")
rsa = RSA(userkey)
with closing(ZipFile(open(inpath, 'rb'))) as inf:
namelist = set(inf.namelist())
if 'META-INF/rights.xml' not in namelist or \
'META-INF/encryption.xml' not in namelist:
print u"{0:s} is DRM-free.".format(os.path.basename(inpath))
return 1
for name in META_NAMES:
namelist.remove(name)
try:
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
expr = './/%s' % (adept('encryptedKey'),)
bookkey = ''.join(rights.findtext(expr))
if len(bookkey) != 172:
print u"{0:s} is not a secure Adobe Adept ePub.".format(os.path.basename(inpath))
return 1
bookkey = rsa.decrypt(bookkey.decode('base64'))
# Padded as per RSAES-PKCS1-v1_5
if bookkey[-17] != '\x00':
print u"Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath))
return 2
encryption = inf.read('META-INF/encryption.xml')
decryptor = Decryptor(bookkey[-16:], encryption)
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
zi = ZipInfo('mimetype')
zi.compress_type=ZIP_STORED
try:
# if the mimetype is present, get its info, including time-stamp
oldzi = inf.getinfo('mimetype')
# copy across fields to be preserved
zi.date_time = oldzi.date_time
zi.comment = oldzi.comment
zi.extra = oldzi.extra
zi.internal_attr = oldzi.internal_attr
# external attributes are dependent on the create system, so copy both.
zi.external_attr = oldzi.external_attr
zi.create_system = oldzi.create_system
except:
pass
outf.writestr(zi, inf.read('mimetype'))
for path in namelist:
data = inf.read(path)
zi = ZipInfo(path)
zi.compress_type=ZIP_DEFLATED
try:
# get the file info, including time-stamp
oldzi = inf.getinfo(path)
# copy across useful fields
zi.date_time = oldzi.date_time
zi.comment = oldzi.comment
zi.extra = oldzi.extra
zi.internal_attr = oldzi.internal_attr
# external attributes are dependent on the create system, so copy both.
zi.external_attr = oldzi.external_attr
zi.create_system = oldzi.create_system
except:
pass
outf.writestr(zi, decryptor.decrypt(path, data))
except:
print u"Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc())
return 2
return 0
def cli_main():
@@ -465,21 +222,26 @@ def cli_main():
sys.stderr=SafeUnbuffered(sys.stderr)
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)
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
keypath, inpath, outpath = argv[1:]
userkey = open(keypath,'rb').read()
result = decryptBook(userkey, inpath, outpath)
if result == 0:
print u"Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath))
return result
if len(argv) != 4:
print u"usage: {0} <Name> <CC#> <keyfileout.b64>".format(progname)
return 1
name, ccn, keypath = argv[1:]
userkey = generate_key(name, ccn)
open(keypath,'wb').write(userkey)
return 0
def gui_main():
try:
import Tkinter
import Tkconstants
import tkMessageBox
import tkFileDialog
import traceback
except:
return cli_main()
@@ -487,33 +249,28 @@ def gui_main():
class DecryptionDialog(Tkinter.Frame):
def __init__(self, root):
Tkinter.Frame.__init__(self, root, border=5)
self.status = Tkinter.Label(self, text=u"Select files for decryption")
self.status = Tkinter.Label(self, text=u"Enter parameters")
self.status.pack(fill=Tkconstants.X, expand=1)
body = Tkinter.Frame(self)
body.pack(fill=Tkconstants.X, expand=1)
sticky = Tkconstants.E + Tkconstants.W
body.grid_columnconfigure(1, weight=2)
Tkinter.Label(body, text=u"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"Account Name").grid(row=0)
self.name = Tkinter.Entry(body, width=40)
self.name.grid(row=0, column=1, sticky=sticky)
Tkinter.Label(body, text=u"CC#").grid(row=1)
self.ccn = Tkinter.Entry(body, width=40)
self.ccn.grid(row=1, column=1, sticky=sticky)
Tkinter.Label(body, text=u"Output file").grid(row=2)
self.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)
self.keypath = Tkinter.Entry(body, width=40)
self.keypath.grid(row=2, column=1, sticky=sticky)
self.keypath.insert(2, u"bnepubkey.b64")
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
button.grid(row=2, column=2)
buttons = Tkinter.Frame(self)
buttons.pack()
botton = Tkinter.Button(
buttons, text=u"Decrypt", width=10, command=self.decrypt)
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(
@@ -521,10 +278,10 @@ def gui_main():
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'),
keypath = tkFileDialog.asksaveasfilename(
parent=None, title=u"Select B&N ePub key file to produce",
defaultextension=u".b64",
filetypes=[('base64-encoded files', '.b64'),
('All Files', '.*')])
if keypath:
keypath = os.path.normpath(keypath)
@@ -532,56 +289,37 @@ def gui_main():
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):
def generate(self):
name = self.name.get()
ccn = self.ccn.get()
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"
if not name:
self.status['text'] = u"Name not specified"
return
if not inpath or not os.path.exists(inpath):
self.status['text'] = u"Specified input file does not exist"
if not ccn:
self.status['text'] = u"Credit card number not specified"
return
if not outpath:
self.status['text'] = u"Output file not specified"
if not keypath:
self.status['text'] = u"Output keyfile path 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..."
self.status['text'] = u"Generating..."
try:
decrypt_status = decryptBook(userkey, inpath, outpath)
userkey = generate_key(name, ccn)
except Exception, e:
self.status['text'] = u"Error: {0}".format(e.args[0])
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."
open(keypath,'wb').write(userkey)
self.status['text'] = u"Keyfile successfully generated"
root = Tkinter.Tk()
root.title(u"Adobe Adept ePub Decrypter v.{0}".format(__version__))
if AES is None:
root.withdraw()
tkMessageBox.showerror(
"Ignoble EPUB Keyfile Generator",
"This script requires OpenSSL or PyCrypto, which must be installed "
"separately. Read the top-of-script comment for details.")
return 1
root.title(u"Barnes & Noble ePub Keyfile Generator v.{0}".format(__version__))
root.resizable(True, False)
root.minsize(300, 0)
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,266 +2,337 @@
# -*- coding: utf-8 -*-
from __future__ import with_statement
import sys
import os, csv
import binascii
import zlib
# k4mobidedrm.py, version 5.3
# Copyright © 2009-2015 by ApprenticeHarper et al.
# engine to remove drm from Kindle and Mobipocket ebooks
# for personal use for archiving and converting your ebooks
# PLEASE DO NOT PIRATE EBOOKS!
# We want all authors and publishers, and ebook stores to live
# long and prosperous lives but at the same time we just want to
# be able to read OUR books on whatever device we want and to keep
# readable for a long, long time
# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
# unswindle, DarkReverser, ApprenticeAlf, 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.
# 4.9 - Missed some invalid characters in cleanup_name
# 5.0 - Extraction of info from Kindle for PC/Mac moved into kindlekey.py
# - tweaked GetDecryptedBook interface to leave passed parameters unchanged
# 5.1 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 5.2 - Fixed error in command line processing of unicode arguments
# 5.3 - Changed Android support to allow passing of backup .ab files
__version__ = '5.3'
import sys, os, re
import csv
import getopt
import re
from struct import pack, unpack, unpack_from
import traceback
import time
import htmlentitydefs
import json
class DrmException(Exception):
pass
global charMap1
global charMap3
global charMap4
if 'calibre' in sys.modules:
inCalibre = True
else:
inCalibre = False
if inCalibre:
from calibre_plugins.dedrm import mobidedrm
from calibre_plugins.dedrm import topazextract
from calibre_plugins.dedrm import kgenpids
from calibre_plugins.dedrm import androidkindlekey
else:
import mobidedrm
import topazextract
import kgenpids
import androidkindlekey
# 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 '?'.
charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M'
charMap3 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
charMap4 = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
# crypto digestroutines
import hashlib
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
def MD5(message):
ctx = hashlib.md5()
ctx.update(message)
return ctx.digest()
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
def SHA1(message):
ctx = hashlib.sha1()
ctx.update(message)
return ctx.digest()
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"mobidedrm.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
# cleanup unicode filenames
# borrowed from calibre from calibre/src/calibre/__init__.py
# added in removal of control (<32) chars
# and removal of . at start and end
# and with some (heavily edited) code from Paul Durrant's kindlenamer.py
def cleanup_name(name):
# substitute filename unfriendly characters
name = name.replace(u"<",u"[").replace(u">",u"]").replace(u" : ",u" ").replace(u": ",u" ").replace(u":",u"").replace(u"/",u"_").replace(u"\\",u"_").replace(u"|",u"_").replace(u"\"",u"\'").replace(u"*",u"_").replace(u"?",u"")
# delete control characters
name = u"".join(char for char in name if ord(char)>=32)
# white space to single space, delete leading and trailing while space
name = re.sub(ur"\s", u" ", name).strip()
# remove leading dots
while len(name)>0 and name[0] == u".":
name = name[1:]
# remove trailing dots (Windows doesn't like them)
if name.endswith(u'.'):
name = name[:-1]
return name
# Encode the bytes in data with the characters in map
def encode(data, map):
result = ''
for char in data:
value = ord(char)
Q = (value ^ 0x80) // len(map)
R = value % len(map)
result += map[Q]
result += map[R]
return result
# 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)
# Hash the bytes in data and then encode the digest with the characters in map
def encodeHash(data,map):
return encode(MD5(data),map)
def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime = time.time()):
# handle the obvious cases at the beginning
if not os.path.isfile(infile):
raise DrmException(u"Input file does not exist.")
# Decode the string in data with the characters in map. Returns the decoded bytes
def decode(data,map):
result = ''
for i in range (0,len(data)-1,2):
high = map.find(data[i])
low = map.find(data[i+1])
if (high == -1) or (low == -1) :
break
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
result += pack('B',value)
return result
mobi = True
magic3 = open(infile,'rb').read(3)
if magic3 == 'TPZ':
mobi = False
#
# PID generation routines
#
if mobi:
mb = mobidedrm.MobiBook(infile)
else:
mb = topazextract.TopazBook(infile)
# Returns two bit at offset from a bit field
def getTwoBitsFromBitField(bitField,offset):
byteNumber = offset // 4
bitPosition = 6 - 2*(offset % 4)
return ord(bitField[byteNumber]) >> bitPosition & 3
bookname = unescape(mb.getBookTitle())
print u"Decrypting {1} ebook: {0}".format(bookname, mb.getBookType())
# Returns the six bits at offset from a bit field
def getSixBitsFromBitField(bitField,offset):
offset *= 3
value = (getTwoBitsFromBitField(bitField,offset) <<4) + (getTwoBitsFromBitField(bitField,offset+1) << 2) +getTwoBitsFromBitField(bitField,offset+2)
return value
# 8 bits to six bits encoding from hash to generate PID string
def encodePID(hash):
global charMap3
PID = ''
for position in range (0,8):
PID += charMap3[getSixBitsFromBitField(hash,position)]
return PID
# Encryption table used to generate the device PID
def generatePidEncryptionTable() :
table = []
for counter1 in range (0,0x100):
value = counter1
for counter2 in range (0,8):
if (value & 1 == 0) :
value = value >> 1
else :
value = value >> 1
value = value ^ 0xEDB88320
table.append(value)
return table
# Seed value used to generate the device PID
def generatePidSeed(table,dsn) :
value = 0
for counter in range (0,4) :
index = (ord(dsn[counter]) ^ value) &0xFF
value = (value >> 8) ^ table[index]
return value
# Generate the device PID
def generateDevicePID(table,dsn,nbRoll):
global charMap4
seed = generatePidSeed(table,dsn)
pidAscii = ''
pid = [(seed >>24) &0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF,(seed>>24) & 0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF]
index = 0
for counter in range (0,nbRoll):
pid[index] = pid[index] ^ ord(dsn[counter])
index = (index+1) %8
for counter in range (0,8):
index = ((((pid[counter] >>5) & 3) ^ pid[counter]) & 0x1f) + (pid[counter] >> 7)
pidAscii += charMap4[index]
return pidAscii
def crc32(s):
return (~binascii.crc32(s,-1))&0xFFFFFFFF
# convert from 8 digit PID to 10 digit PID with checksum
def checksumPid(s):
global charMap4
crc = crc32(s)
crc = crc ^ (crc >> 16)
res = s
l = len(charMap4)
for i in (0,1):
b = crc & 0xff
pos = (b // l) ^ (b % l)
res += charMap4[pos%l]
crc >>= 8
return res
# old kindle serial number to fixed pid
def pidFromSerial(s, l):
global charMap4
crc = crc32(s)
arr1 = [0]*l
for i in xrange(len(s)):
arr1[i%l] ^= ord(s[i])
crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff]
for i in xrange(l):
arr1[i] ^= crc_bytes[i&3]
pid = ""
for i in xrange(l):
b = arr1[i] & 0xff
pid+=charMap4[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))]
return pid
# Parse the EXTH header records and use the Kindle serial number to calculate the book pid.
def getKindlePids(rec209, token, serialnum):
pids=[]
if isinstance(serialnum,unicode):
serialnum = serialnum.encode('ascii')
# Compute book PID
pidHash = SHA1(serialnum+rec209+token)
bookPID = encodePID(pidHash)
bookPID = checksumPid(bookPID)
pids.append(bookPID)
# compute fixed pid for old pre 2.5 firmware update pid as well
kindlePID = pidFromSerial(serialnum, 7) + "*"
kindlePID = checksumPid(kindlePID)
pids.append(kindlePID)
return pids
# parse the Kindleinfo file to calculate the book pid.
keynames = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber']
def getK4Pids(rec209, token, kindleDatabase):
global charMap1
pids = []
# copy list of pids
totalpids = list(pids)
# extend list of serials with serials from android databases
for aFile in androidFiles:
serials.extend(androidkindlekey.get_serials(aFile))
# extend PID list with book-specific PIDs from seriala and kDatabases
md1, md2 = mb.getPIDMetaInfo()
totalpids.extend(kgenpids.getPidList(md1, md2, serials, kDatabases))
# remove any duplicates
totalpid = list(set(totalpids))
print u"Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(totalpids))
try:
# Get the Mazama Random number
MazamaRandomNumber = (kindleDatabase[1])['MazamaRandomNumber'].decode('hex').encode('ascii')
mb.processBook(totalpids)
except:
mb.cleanup
raise
# Get the kindle account token
kindleAccountToken = (kindleDatabase[1])['kindle.account.tokens'].decode('hex').encode('ascii')
print u"Decryption succeeded after {0:.1f} seconds".format(time.time()-starttime)
return mb
# Get the IDString used to decode the Kindle Info file
IDString = (kindleDatabase[1])['IDString'].decode('hex').encode('ascii')
# Get the UserName stored when the Kindle Info file was decoded
UserName = (kindleDatabase[1])['UserName'].decode('hex').encode('ascii')
except KeyError:
print u"Keys not found in the database {0}.".format(kindleDatabase[0])
return pids
# Get the ID string used
encodedIDString = encodeHash(IDString,charMap1)
# Get the current user name
encodedUsername = encodeHash(UserName,charMap1)
# concat, hash and encode to calculate the DSN
DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1)
# Compute the device PID (for which I can tell, is used for nothing).
table = generatePidEncryptionTable()
devicePID = generateDevicePID(table,DSN,4)
devicePID = checksumPid(devicePID)
pids.append(devicePID)
# Compute book PIDs
# book pid
pidHash = SHA1(DSN+kindleAccountToken+rec209+token)
bookPID = encodePID(pidHash)
bookPID = checksumPid(bookPID)
pids.append(bookPID)
# variant 1
pidHash = SHA1(kindleAccountToken+rec209+token)
bookPID = encodePID(pidHash)
bookPID = checksumPid(bookPID)
pids.append(bookPID)
# variant 2
pidHash = SHA1(DSN+rec209+token)
bookPID = encodePID(pidHash)
bookPID = checksumPid(bookPID)
pids.append(bookPID)
return pids
def getPidList(md1, md2, serials=[], kDatabases=[]):
pidlst = []
if kDatabases is None:
kDatabases = []
if serials is None:
serials = []
for kDatabase in kDatabases:
# kDatabaseFiles is a list of files created by kindlekey
def decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids):
starttime = time.time()
kDatabases = []
for dbfile in kDatabaseFiles:
kindleDatabase = {}
try:
pidlst.extend(getK4Pids(md1, md2, kDatabase))
with open(dbfile, 'r') as keyfilein:
kindleDatabase = json.loads(keyfilein.read())
kDatabases.append([dbfile,kindleDatabase])
except Exception, e:
print u"Error getting PIDs from database {0}: {1}".format(kDatabase[0],e.args[0])
print u"Error getting database from file {0:s}: {1:s}".format(dbfile,e)
traceback.print_exc()
for serialnum in serials:
try:
pidlst.extend(getKindlePids(md1, md2, serialnum))
except Exception, e:
print u"Error getting PIDs from serial number {0}: {1}".format(serialnum ,e.args[0])
traceback.print_exc()
return pidlst
try:
book = GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime)
except Exception, e:
print u"Error decrypting book after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime)
traceback.print_exc()
return 1
# 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[:99]+"--"+outfilename[-49:]
outfilename = outfilename+u"_nodrm"
outfile = os.path.join(outdir, outfilename + book.getBookExtension())
book.getFile(outfile)
print u"Saved decrypted book {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename)
if book.getBookType()==u"Topaz":
zipname = os.path.join(outdir, outfilename + u"_SVG.zip")
book.getSVGZip(zipname)
print u"Saved SVG ZIP Archive for {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename)
# remove internal temporary directory of Topaz pieces
book.cleanup()
return 0
def usage(progname):
print u"Removes DRM protection from Mobipocket, Amazon KF8, Amazon Print Replica and Amazon Topaz ebooks"
print u"Usage:"
print u" {0} [-k <kindle.k4i>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] [ -a <AmazonSecureStorage.xml|backup.ab> ] <infile> <outdir>".format(progname)
#
# Main
#
def cli_main():
argv=unicode_argv()
progname = os.path.basename(argv[0])
print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2013 The Dark Reverser et al.".format(__version__)
try:
opts, args = getopt.getopt(argv[1:], "k:p:s:a:")
except getopt.GetoptError, err:
print u"Error in options or arguments: {0}".format(err.args[0])
usage(progname)
sys.exit(2)
if len(args)<2:
usage(progname)
sys.exit(2)
infile = args[0]
outdir = args[1]
kDatabaseFiles = []
androidFiles = []
serials = []
pids = []
for o, a in opts:
if o == "-k":
if a == None :
raise DrmException("Invalid parameter for -k")
kDatabaseFiles.append(a)
if o == "-p":
if a == None :
raise DrmException("Invalid parameter for -p")
pids = a.split(',')
if o == "-s":
if a == None :
raise DrmException("Invalid parameter for -s")
serials = a.split(',')
if o == '-a':
if a == None:
raise DrmException("Invalid parameter for -a")
androidFiles.append(a)
# try with built in Kindle Info files if not on Linux
k4 = not sys.platform.startswith('linux')
return decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids)
if __name__ == '__main__':
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
sys.exit(cli_main())

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,89 +1,541 @@
#!/usr/bin/env python
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
# -*- coding: utf-8 -*-
# implement just enough of des from openssl to make erdr2pml.py happy
# mobidedrm.py, version 0.38
# Copyright © 2008 The Dark Reverser
#
# Modified 20082012 by some_updates, DiapDealer and Apprentice Alf
def load_libcrypto():
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_char, c_int, c_long, \
Structure, c_ulong, create_string_buffer, cast
from ctypes.util import find_library
import sys
# This is a python script. You need a Python interpreter to run it.
# For example, ActiveState Python, which exists for windows.
#
# Changelog
# 0.01 - Initial version
# 0.02 - Huffdic compressed books were not properly decrypted
# 0.03 - Wasn't checking MOBI header length
# 0.04 - Wasn't sanity checking size of data record
# 0.05 - It seems that the extra data flags take two bytes not four
# 0.06 - And that low bit does mean something after all :-)
# 0.07 - The extra data flags aren't present in MOBI header < 0xE8 in size
# 0.08 - ...and also not in Mobi header version < 6
# 0.09 - ...but they are there with Mobi header version 6, header size 0xE4!
# 0.10 - Outputs unencrypted files as-is, so that when run as a Calibre
# import filter it works when importing unencrypted files.
# Also now handles encrypted files that don't need a specific PID.
# 0.11 - use autoflushed stdout and proper return values
# 0.12 - Fix for problems with metadata import as Calibre plugin, report errors
# 0.13 - Formatting fixes: retabbed file, removed trailing whitespace
# and extra blank lines, converted CR/LF pairs at ends of each line,
# and other cosmetic fixes.
# 0.14 - Working out when the extra data flags are present has been problematic
# Versions 7 through 9 have tried to tweak the conditions, but have been
# only partially successful. Closer examination of lots of sample
# files reveals that a confusion has arisen because trailing data entries
# are not encrypted, but it turns out that the multibyte entries
# in utf8 file are encrypted. (Although neither kind gets compressed.)
# This knowledge leads to a simplification of the test for the
# trailing data byte flags - version 5 and higher AND header size >= 0xE4.
# 0.15 - Now outputs 'heartbeat', and is also quicker for long files.
# 0.16 - And reverts to 'done' not 'done.' at the end for unswindle compatibility.
# 0.17 - added modifications to support its use as an imported python module
# both inside calibre and also in other places (ie K4DeDRM tools)
# 0.17a- disabled the standalone plugin feature since a plugin can not import
# a plugin
# 0.18 - It seems that multibyte entries aren't encrypted in a v7 file...
# Removed the disabled Calibre plug-in code
# Permit use of 8-digit PIDs
# 0.19 - It seems that multibyte entries aren't encrypted in a v6 file either.
# 0.20 - Correction: It seems that multibyte entries are encrypted in a v6 file.
# 0.21 - Added support for multiple pids
# 0.22 - revised structure to hold MobiBook as a class to allow an extended interface
# 0.23 - fixed problem with older files with no EXTH section
# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well
# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption
# 0.26 - Now enables Text-To-Speech flag and sets clipping limit to 100%
# 0.27 - Correct pid metadata token generation to match that used by skindle (Thank You Bart!)
# 0.28 - slight additional changes to metadata token generation (None -> '')
# 0.29 - It seems that the ideas about when multibyte trailing characters were
# included in the encryption were wrong. They are for DOC compressed
# files, but they are not for HUFF/CDIC compress files!
# 0.30 - Modified interface slightly to work better with new calibre plugin style
# 0.31 - The multibyte encrytion info is true for version 7 files too.
# 0.32 - Added support for "Print Replica" Kindle ebooks
# 0.33 - Performance improvements for large files (concatenation)
# 0.34 - Performance improvements in decryption (libalfcrypto)
# 0.35 - add interface to get mobi_version
# 0.36 - fixed problem with TEXtREAd and getBookTitle interface
# 0.37 - Fixed double announcement for stand-alone operation
# 0.38 - Unicode used wherever possible, cope with absent alfcrypto
# 0.39 - Fixed problem with TEXtREAd and getBookType interface
# 0.40 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 0.41 - Fixed potential unicode problem in command line calls
if sys.platform.startswith('win'):
libcrypto = find_library('libeay32')
__version__ = u"0.41"
import sys
import os
import struct
import binascii
try:
from alfcrypto import Pukall_Cipher
except:
print u"AlfCrypto not found. Using python PC1 implementation."
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
# encoded using "replace" before writing them.
class SafeUnbuffered:
def __init__(self, stream):
self.stream = stream
self.encoding = stream.encoding
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
def unicode_argv():
if iswindows:
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'.
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"mobidedrm.py"]
else:
libcrypto = find_library('crypto')
if libcrypto is None:
return None
libcrypto = CDLL(libcrypto)
# typedef struct DES_ks
# {
# union
# {
# DES_cblock cblock;
# /* make sure things are correct size on machines with
# * 8 byte longs */
# DES_LONG deslong[2];
# } ks[16];
# } DES_key_schedule;
# just create a big enough place to hold everything
# it will have alignment of structure so we should be okay (16 byte aligned?)
class DES_KEY_SCHEDULE(Structure):
_fields_ = [('DES_cblock1', c_char * 16),
('DES_cblock2', c_char * 16),
('DES_cblock3', c_char * 16),
('DES_cblock4', c_char * 16),
('DES_cblock5', c_char * 16),
('DES_cblock6', c_char * 16),
('DES_cblock7', c_char * 16),
('DES_cblock8', c_char * 16),
('DES_cblock9', c_char * 16),
('DES_cblock10', c_char * 16),
('DES_cblock11', c_char * 16),
('DES_cblock12', c_char * 16),
('DES_cblock13', c_char * 16),
('DES_cblock14', c_char * 16),
('DES_cblock15', c_char * 16),
('DES_cblock16', c_char * 16)]
DES_KEY_SCHEDULE_p = POINTER(DES_KEY_SCHEDULE)
def F(restype, name, argtypes):
func = getattr(libcrypto, name)
func.restype = restype
func.argtypes = argtypes
return func
DES_set_key = F(None, 'DES_set_key',[c_char_p, DES_KEY_SCHEDULE_p])
DES_ecb_encrypt = F(None, 'DES_ecb_encrypt',[c_char_p, c_char_p, DES_KEY_SCHEDULE_p, c_int])
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 DES(object):
def __init__(self, key):
if len(key) != 8 :
raise Exception('DES improper key used')
return
self.key = key
self.keyschedule = DES_KEY_SCHEDULE()
DES_set_key(self.key, self.keyschedule)
def desdecrypt(self, data):
ob = create_string_buffer(len(data))
DES_ecb_encrypt(data, ob, self.keyschedule, 0)
return ob.raw
def decrypt(self, data):
if not data:
return ''
i = 0
result = []
while i < len(data):
block = data[i:i+8]
processed_block = self.desdecrypt(block)
result.append(processed_block)
i += 8
return ''.join(result)
class DrmException(Exception):
pass
return DES
#
# MobiBook Utility Routines
#
# Implementation of Pukall Cipher 1
def PC1(key, src, decryption=True):
# if we can get it from alfcrypto, use that
try:
return Pukall_Cipher().PC1(key,src,decryption)
except NameError:
pass
except TypeError:
pass
# use slow python version, since Pukall_Cipher didn't load
sum1 = 0;
sum2 = 0;
keyXorVal = 0;
if len(key)!=16:
DrmException (u"PC1: Bad key length")
wkey = []
for i in xrange(8):
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
dst = ""
for i in xrange(len(src)):
temp1 = 0;
byteXorVal = 0;
for j in xrange(8):
temp1 ^= wkey[j]
sum2 = (sum2+j)*20021 + sum1
sum1 = (temp1*346)&0xFFFF
sum2 = (sum2+sum1)&0xFFFF
temp1 = (temp1*20021+1)&0xFFFF
byteXorVal ^= temp1 ^ sum2
curByte = ord(src[i])
if not decryption:
keyXorVal = curByte * 257;
curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
if decryption:
keyXorVal = curByte * 257;
for j in xrange(8):
wkey[j] ^= keyXorVal;
dst+=chr(curByte)
return dst
def checksumPid(s):
letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
crc = (~binascii.crc32(s,-1))&0xFFFFFFFF
crc = crc ^ (crc >> 16)
res = s
l = len(letters)
for i in (0,1):
b = crc & 0xff
pos = (b // l) ^ (b % l)
res += letters[pos%l]
crc >>= 8
return res
def getSizeOfTrailingDataEntries(ptr, size, flags):
def getSizeOfTrailingDataEntry(ptr, size):
bitpos, result = 0, 0
if size <= 0:
return result
while True:
v = ord(ptr[size-1])
result |= (v & 0x7F) << bitpos
bitpos += 7
size -= 1
if (v & 0x80) != 0 or (bitpos >= 28) or (size == 0):
return result
num = 0
testflags = flags >> 1
while testflags:
if testflags & 1:
num += getSizeOfTrailingDataEntry(ptr, size - num)
testflags >>= 1
# Check the low bit to see if there's multibyte data present.
# if multibyte data is included in the encryped data, we'll
# have already cleared this flag.
if flags & 1:
num += (ord(ptr[size - num - 1]) & 0x3) + 1
return num
class MobiBook:
def loadSection(self, section):
if (section + 1 == self.num_sections):
endoff = len(self.data_file)
else:
endoff = self.sections[section + 1][0]
off = self.sections[section][0]
return self.data_file[off:endoff]
def cleanup(self):
# to match function in Topaz book
pass
def __init__(self, infile):
print u"MobiDeDrm v{0:s}.\nCopyright © 2008-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(u"Invalid file format")
self.magic = self.header[0x3C:0x3C+8]
self.crypto_type = -1
# build up section offset and flag info
self.num_sections, = struct.unpack('>H', self.header[76:78])
self.sections = []
for i in xrange(self.num_sections):
offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', self.data_file[78+i*8:78+i*8+8])
flags, val = a1, a2<<16|a3<<8|a4
self.sections.append( (offset, flags, val) )
# parse information from section 0
self.sect = self.loadSection(0)
self.records, = struct.unpack('>H', self.sect[0x8:0x8+2])
self.compression, = struct.unpack('>H', self.sect[0x0:0x0+2])
# det default values before PalmDoc test
self.print_replica = False
self.extra_data_flags = 0
self.meta_array = {}
self.mobi_length = 0
self.mobi_codepage = 1252
self.mobi_version = -1
if self.magic == 'TEXtREAd':
print u"PalmDoc format book detected."
return
self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18])
self.mobi_codepage, = struct.unpack('>L',self.sect[0x1c:0x20])
self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C])
print u"MOBI header version {0:d}, header length {1:d}".format(self.mobi_version, self.mobi_length)
if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4])
print u"Extra Data Flags: {0:d}".format(self.extra_data_flags)
if (self.compression != 17480):
# multibyte utf8 data is included in the encryption for PalmDoc compression
# so clear that byte so that we leave it to be decrypted.
self.extra_data_flags &= 0xFFFE
# if exth region exists parse it for metadata array
try:
exth_flag, = struct.unpack('>L', self.sect[0x80:0x84])
exth = ''
if exth_flag & 0x40:
exth = self.sect[16 + self.mobi_length:]
if (len(exth) >= 12) and (exth[:4] == 'EXTH'):
nitems, = struct.unpack('>I', exth[8:12])
pos = 12
for i in xrange(nitems):
type, size = struct.unpack('>II', exth[pos: pos + 8])
content = exth[pos + 8: pos + size]
self.meta_array[type] = content
# reset the text to speech flag and clipping limit, if present
if type == 401 and size == 9:
# set clipping limit to 100%
self.patchSection(0, '\144', 16 + self.mobi_length + pos + 8)
elif type == 404 and size == 9:
# make sure text to speech is enabled
self.patchSection(0, '\0', 16 + self.mobi_length + pos + 8)
# print type, size, content, content.encode('hex')
pos += size
except:
pass
def getBookTitle(self):
codec_map = {
1252 : 'windows-1252',
65001 : 'utf-8',
}
title = ''
codec = 'windows-1252'
if self.magic == 'BOOKMOBI':
if 503 in self.meta_array:
title = self.meta_array[503]
else:
toff, tlen = struct.unpack('>II', self.sect[0x54:0x5c])
tend = toff + tlen
title = self.sect[toff:tend]
if self.mobi_codepage in codec_map.keys():
codec = codec_map[self.mobi_codepage]
if title == '':
title = self.header[:32]
title = title.split('\0')[0]
return unicode(title, codec)
def getPIDMetaInfo(self):
rec209 = ''
token = ''
if 209 in self.meta_array:
rec209 = self.meta_array[209]
data = rec209
# The 209 data comes in five byte groups. Interpret the last four bytes
# of each group as a big endian unsigned integer to get a key value
# if that key exists in the meta_array, append its contents to the token
for i in xrange(0,len(data),5):
val, = struct.unpack('>I',data[i+1:i+5])
sval = self.meta_array.get(val,'')
token += sval
return rec209, token
def patch(self, off, new):
self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):]
def patchSection(self, section, new, in_off = 0):
if (section + 1 == self.num_sections):
endoff = len(self.data_file)
else:
endoff = self.sections[section + 1][0]
off = self.sections[section][0]
assert off + in_off + len(new) <= endoff
self.patch(off + in_off, new)
def parseDRM(self, data, count, pidlist):
found_key = None
keyvec1 = '\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96'
for pid in pidlist:
bigpid = pid.ljust(16,'\0')
temp_key = PC1(keyvec1, bigpid, False)
temp_key_sum = sum(map(ord,temp_key)) & 0xff
found_key = None
for i in xrange(count):
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
if cksum == temp_key_sum:
cookie = PC1(temp_key, cookie)
ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
if verification == ver and (flags & 0x1F) == 1:
found_key = finalkey
break
if found_key != None:
break
if not found_key:
# Then try the default encoding that doesn't require a PID
pid = '00000000'
temp_key = keyvec1
temp_key_sum = sum(map(ord,temp_key)) & 0xff
for i in xrange(count):
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
if cksum == temp_key_sum:
cookie = PC1(temp_key, cookie)
ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
if verification == ver:
found_key = finalkey
break
return [found_key,pid]
def getFile(self, outpath):
file(outpath,'wb').write(self.mobi_data)
def getBookType(self):
if self.print_replica:
return u"Print Replica"
if self.mobi_version >= 8:
return u"Kindle Format 8"
if self.mobi_version >= 0:
return u"Mobipocket {0:d}".format(self.mobi_version)
return u"PalmDoc"
def getBookExtension(self):
if self.print_replica:
return u".azw4"
if self.mobi_version >= 8:
return u".azw3"
return u".mobi"
def processBook(self, pidlist):
crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2])
print u"Crypto Type is: {0:d}".format(crypto_type)
self.crypto_type = crypto_type
if crypto_type == 0:
print u"This book is not encrypted."
# we must still check for Print Replica
self.print_replica = (self.loadSection(1)[0:4] == '%MOP')
self.mobi_data = self.data_file
return
if crypto_type != 2 and crypto_type != 1:
raise DrmException(u"Cannot decode unknown Mobipocket encryption type {0:d}".format(crypto_type))
if 406 in self.meta_array:
data406 = self.meta_array[406]
val406, = struct.unpack('>Q',data406)
if val406 != 0:
raise DrmException(u"Cannot decode library or rented ebooks.")
goodpids = []
for pid in pidlist:
if len(pid)==10:
if checksumPid(pid[0:-2]) != pid:
print u"Warning: PID {0} has incorrect checksum, should have been {1}".format(pid,checksumPid(pid[0:-2]))
goodpids.append(pid[0:-2])
elif len(pid)==8:
goodpids.append(pid)
if self.crypto_type == 1:
t1_keyvec = 'QDCVEPMU675RUBSZ'
if self.magic == 'TEXtREAd':
bookkey_data = self.sect[0x0E:0x0E+16]
elif self.mobi_version < 0:
bookkey_data = self.sect[0x90:0x90+16]
else:
bookkey_data = self.sect[self.mobi_length+16:self.mobi_length+32]
pid = '00000000'
found_key = PC1(t1_keyvec, bookkey_data)
else :
# calculate the keys
drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16])
if drm_count == 0:
raise DrmException(u"Encryption not initialised. Must be opened with Mobipocket Reader first.")
found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids)
if not found_key:
raise DrmException(u"No key found in {0:d} keys tried.".format(len(goodpids)))
# kill the drm keys
self.patchSection(0, '\0' * drm_size, drm_ptr)
# kill the drm pointers
self.patchSection(0, '\xff' * 4 + '\0' * 12, 0xA8)
if pid=='00000000':
print u"File has default encryption, no specific key needed."
else:
print u"File is encoded with PID {0}.".format(checksumPid(pid))
# clear the crypto type
self.patchSection(0, "\0" * 2, 0xC)
# decrypt sections
print u"Decrypting. Please wait . . .",
mobidataList = []
mobidataList.append(self.data_file[:self.sections[1][0]])
for i in xrange(1, self.records+1):
data = self.loadSection(i)
extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags)
if i%100 == 0:
print u".",
# print "record %d, extra_size %d" %(i,extra_size)
decoded_data = PC1(found_key, data[0:len(data) - extra_size])
if i==1:
self.print_replica = (decoded_data[0:4] == '%MOP')
mobidataList.append(decoded_data)
if extra_size > 0:
mobidataList.append(data[-extra_size:])
if self.num_sections > self.records+1:
mobidataList.append(self.data_file[self.sections[self.records+1][0]:])
self.mobi_data = "".join(mobidataList)
print u"done"
return
def getUnencryptedBook(infile,pidlist):
if not os.path.isfile(infile):
raise DrmException(u"Input File Not Found.")
book = MobiBook(infile)
book.processBook(pidlist)
return book.mobi_data
def cli_main():
argv=unicode_argv()
progname = os.path.basename(argv[0])
if len(argv)<3 or len(argv)>4:
print u"MobiDeDrm v{0}.\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(progname)
return 1
else:
infile = argv[1]
outfile = argv[2]
if len(argv) is 4:
pidlist = argv[3].split(',')
else:
pidlist = []
try:
stripped_file = getUnencryptedBook(infile, pidlist)
file(outfile, 'wb').write(stripped_file)
except DrmException, e:
print u"MobiDeDRM v{0} Error: {0:s}".format(__version__,e.args[0])
return 1
return 0
if __name__ == '__main__':
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
sys.exit(cli_main())

View File

@@ -0,0 +1,89 @@
#!/usr/bin/env python
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
# implement just enough of des from openssl to make erdr2pml.py happy
def load_libcrypto():
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_char, c_int, c_long, \
Structure, c_ulong, create_string_buffer, cast
from ctypes.util import find_library
import sys
if sys.platform.startswith('win'):
libcrypto = find_library('libeay32')
else:
libcrypto = find_library('crypto')
if libcrypto is None:
return None
libcrypto = CDLL(libcrypto)
# typedef struct DES_ks
# {
# union
# {
# DES_cblock cblock;
# /* make sure things are correct size on machines with
# * 8 byte longs */
# DES_LONG deslong[2];
# } ks[16];
# } DES_key_schedule;
# just create a big enough place to hold everything
# it will have alignment of structure so we should be okay (16 byte aligned?)
class DES_KEY_SCHEDULE(Structure):
_fields_ = [('DES_cblock1', c_char * 16),
('DES_cblock2', c_char * 16),
('DES_cblock3', c_char * 16),
('DES_cblock4', c_char * 16),
('DES_cblock5', c_char * 16),
('DES_cblock6', c_char * 16),
('DES_cblock7', c_char * 16),
('DES_cblock8', c_char * 16),
('DES_cblock9', c_char * 16),
('DES_cblock10', c_char * 16),
('DES_cblock11', c_char * 16),
('DES_cblock12', c_char * 16),
('DES_cblock13', c_char * 16),
('DES_cblock14', c_char * 16),
('DES_cblock15', c_char * 16),
('DES_cblock16', c_char * 16)]
DES_KEY_SCHEDULE_p = POINTER(DES_KEY_SCHEDULE)
def F(restype, name, argtypes):
func = getattr(libcrypto, name)
func.restype = restype
func.argtypes = argtypes
return func
DES_set_key = F(None, 'DES_set_key',[c_char_p, DES_KEY_SCHEDULE_p])
DES_ecb_encrypt = F(None, 'DES_ecb_encrypt',[c_char_p, c_char_p, DES_KEY_SCHEDULE_p, c_int])
class DES(object):
def __init__(self, key):
if len(key) != 8 :
raise Exception('DES improper key used')
return
self.key = key
self.keyschedule = DES_KEY_SCHEDULE()
DES_set_key(self.key, self.keyschedule)
def desdecrypt(self, data):
ob = create_string_buffer(len(data))
DES_ecb_encrypt(data, ob, self.keyschedule, 0)
return ob.raw
def decrypt(self, data):
if not data:
return ''
i = 0
result = []
while i < len(data):
block = data[i:i+8]
processed_block = self.desdecrypt(block)
result.append(processed_block)
i += 8
return ''.join(result)
return DES

View File

@@ -1,292 +0,0 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement
__license__ = 'GPL v3'
# Standard Python modules.
import os, sys, re, hashlib
import json
import traceback
from calibre.utils.config import dynamic, config_dir, JSONConfig
from calibre_plugins.dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION
from calibre.constants import iswindows, isosx
class DeDRM_Prefs():
def __init__(self):
JSON_PATH = os.path.join(u"plugins", PLUGIN_NAME.strip().lower().replace(' ', '_') + '.json')
self.dedrmprefs = JSONConfig(JSON_PATH)
self.dedrmprefs.defaults['configured'] = False
self.dedrmprefs.defaults['bandnkeys'] = {}
self.dedrmprefs.defaults['adeptkeys'] = {}
self.dedrmprefs.defaults['ereaderkeys'] = {}
self.dedrmprefs.defaults['kindlekeys'] = {}
self.dedrmprefs.defaults['pids'] = []
self.dedrmprefs.defaults['serials'] = []
self.dedrmprefs.defaults['adobewineprefix'] = ""
self.dedrmprefs.defaults['kindlewineprefix'] = ""
# initialise
# we must actually set the prefs that are dictionaries and lists
# to empty dictionaries and lists, otherwise we are unable to add to them
# as then it just adds to the (memory only) dedrmprefs.defaults versions!
if self.dedrmprefs['bandnkeys'] == {}:
self.dedrmprefs['bandnkeys'] = {}
if self.dedrmprefs['adeptkeys'] == {}:
self.dedrmprefs['adeptkeys'] = {}
if self.dedrmprefs['ereaderkeys'] == {}:
self.dedrmprefs['ereaderkeys'] = {}
if self.dedrmprefs['kindlekeys'] == {}:
self.dedrmprefs['kindlekeys'] = {}
if self.dedrmprefs['pids'] == []:
self.dedrmprefs['pids'] = []
if self.dedrmprefs['serials'] == []:
self.dedrmprefs['serials'] = []
def __getitem__(self,kind = None):
if kind is not None:
return self.dedrmprefs[kind]
return self.dedrmprefs
def set(self, kind, value):
self.dedrmprefs[kind] = value
def writeprefs(self,value = True):
self.dedrmprefs['configured'] = value
def addnamedvaluetoprefs(self, prefkind, keyname, keyvalue):
try:
if keyvalue not in self.dedrmprefs[prefkind].values():
# ensure that the keyname is unique
# by adding a number (starting with 2) to the name if it is not
namecount = 1
newname = keyname
while newname in self.dedrmprefs[prefkind]:
namecount += 1
newname = "{0:s}_{1:d}".format(keyname,namecount)
# add to the preferences
self.dedrmprefs[prefkind][newname] = keyvalue
return (True, newname)
except:
traceback.print_exc()
pass
return (False, keyname)
def addvaluetoprefs(self, prefkind, prefsvalue):
# ensure the keyvalue isn't already in the preferences
try:
if prefsvalue not in self.dedrmprefs[prefkind]:
self.dedrmprefs[prefkind].append(prefsvalue)
return True
except:
traceback.print_exc()
return False
def convertprefs(always = False):
def parseIgnobleString(keystuff):
from calibre_plugins.dedrm.ignoblekeygen import generate_key
userkeys = []
ar = keystuff.split(':')
for keystring in ar:
try:
name, ccn = keystring.split(',')
# Generate Barnes & Noble EPUB user key from name and credit card number.
keyname = u"{0}_{1}".format(name.strip(),ccn.strip()[-4:])
keyvalue = generate_key(name, ccn)
userkeys.append([keyname,keyvalue])
except Exception, e:
traceback.print_exc()
print e.args[0]
pass
return userkeys
def parseeReaderString(keystuff):
from calibre_plugins.dedrm.erdr2pml import getuser_key
userkeys = []
ar = keystuff.split(':')
for keystring in ar:
try:
name, cc = keystring.split(',')
# Generate eReader user key from name and credit card number.
keyname = u"{0}_{1}".format(name.strip(),cc.strip()[-4:])
keyvalue = getuser_key(name,cc).encode('hex')
userkeys.append([keyname,keyvalue])
except Exception, e:
traceback.print_exc()
print e.args[0]
pass
return userkeys
def parseKindleString(keystuff):
pids = []
serials = []
ar = keystuff.split(',')
for keystring in ar:
keystring = str(keystring).strip().replace(" ","")
if len(keystring) == 10 or len(keystring) == 8 and keystring not in pids:
pids.append(keystring)
elif len(keystring) == 16 and keystring[0] == 'B' and keystring not in serials:
serials.append(keystring)
return (pids,serials)
def getConfigFiles(extension, encoding = None):
# get any files with extension 'extension' in the config dir
userkeys = []
files = [f for f in os.listdir(config_dir) if f.endswith(extension)]
for filename in files:
try:
fpath = os.path.join(config_dir, filename)
key = os.path.splitext(filename)[0]
value = open(fpath, 'rb').read()
if encoding is not None:
value = value.encode(encoding)
userkeys.append([key,value])
except:
traceback.print_exc()
pass
return userkeys
dedrmprefs = DeDRM_Prefs()
if (not always) and dedrmprefs['configured']:
# We've already converted old preferences,
# and we're not being forced to do it again, so just return
return
print u"{0} v{1}: Importing configuration data from old DeDRM plugins".format(PLUGIN_NAME, PLUGIN_VERSION)
IGNOBLEPLUGINNAME = "Ignoble Epub DeDRM"
EREADERPLUGINNAME = "eReader PDB 2 PML"
OLDKINDLEPLUGINNAME = "K4PC, K4Mac, Kindle Mobi and Topaz DeDRM"
# get prefs from older tools
kindleprefs = JSONConfig(os.path.join(u"plugins", u"K4MobiDeDRM"))
ignobleprefs = JSONConfig(os.path.join(u"plugins", u"ignoble_epub_dedrm"))
# Handle the old ignoble plugin's customization string by converting the
# old string to stored keys... get that personal data out of plain sight.
from calibre.customize.ui import config
sc = config['plugin_customization']
val = sc.pop(IGNOBLEPLUGINNAME, None)
if val is not None:
print u"{0} v{1}: Converting old Ignoble plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION)
priorkeycount = len(dedrmprefs['bandnkeys'])
userkeys = parseIgnobleString(str(val))
for keypair in userkeys:
name = keypair[0]
value = keypair[1]
dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value)
addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount
print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from old Ignoble plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys")
# Make the json write all the prefs to disk
dedrmprefs.writeprefs(False)
# Handle the old eReader plugin's customization string by converting the
# old string to stored keys... get that personal data out of plain sight.
val = sc.pop(EREADERPLUGINNAME, None)
if val is not None:
print u"{0} v{1}: Converting old eReader plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION)
priorkeycount = len(dedrmprefs['ereaderkeys'])
userkeys = parseeReaderString(str(val))
for keypair in userkeys:
name = keypair[0]
value = keypair[1]
dedrmprefs.addnamedvaluetoprefs('ereaderkeys', name, value)
addedkeycount = len(dedrmprefs['ereaderkeys'])-priorkeycount
print u"{0} v{1}: {2:d} eReader {3} imported from old eReader plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys")
# Make the json write all the prefs to disk
dedrmprefs.writeprefs(False)
# get old Kindle plugin configuration string
val = sc.pop(OLDKINDLEPLUGINNAME, None)
if val is not None:
print u"{0} v{1}: Converting old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION)
priorpidcount = len(dedrmprefs['pids'])
priorserialcount = len(dedrmprefs['serials'])
pids, serials = parseKindleString(val)
for pid in pids:
dedrmprefs.addvaluetoprefs('pids',pid)
for serial in serials:
dedrmprefs.addvaluetoprefs('serials',serial)
addedpidcount = len(dedrmprefs['pids']) - priorpidcount
addedserialcount = len(dedrmprefs['serials']) - priorserialcount
print u"{0} v{1}: {2:d} {3} and {4:d} {5} imported from old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, u"PID" if addedpidcount==1 else u"PIDs", addedserialcount, u"serial number" if addedserialcount==1 else u"serial numbers")
# Make the json write all the prefs to disk
dedrmprefs.writeprefs(False)
# copy the customisations back into calibre preferences, as we've now removed the nasty plaintext
config['plugin_customization'] = sc
# get any .b64 files in the config dir
priorkeycount = len(dedrmprefs['bandnkeys'])
bandnfilekeys = getConfigFiles('.b64')
for keypair in bandnfilekeys:
name = keypair[0]
value = keypair[1]
dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value)
addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount
if addedkeycount > 0:
print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key file" if addedkeycount==1 else u"key files")
# Make the json write all the prefs to disk
dedrmprefs.writeprefs(False)
# get any .der files in the config dir
priorkeycount = len(dedrmprefs['adeptkeys'])
adeptfilekeys = getConfigFiles('.der','hex')
for keypair in adeptfilekeys:
name = keypair[0]
value = keypair[1]
dedrmprefs.addnamedvaluetoprefs('adeptkeys', name, value)
addedkeycount = len(dedrmprefs['adeptkeys'])-priorkeycount
if addedkeycount > 0:
print u"{0} v{1}: {2:d} Adobe Adept {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"keyfile" if addedkeycount==1 else u"keyfiles")
# Make the json write all the prefs to disk
dedrmprefs.writeprefs(False)
# get ignoble json prefs
if 'keys' in ignobleprefs:
priorkeycount = len(dedrmprefs['bandnkeys'])
for name in ignobleprefs['keys']:
value = ignobleprefs['keys'][name]
dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value)
addedkeycount = len(dedrmprefs['bandnkeys']) - priorkeycount
# no need to delete old prefs, since they contain no recoverable private data
if addedkeycount > 0:
print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from Ignoble plugin preferences.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys")
# Make the json write all the prefs to disk
dedrmprefs.writeprefs(False)
# get kindle json prefs
priorpidcount = len(dedrmprefs['pids'])
priorserialcount = len(dedrmprefs['serials'])
if 'pids' in kindleprefs:
pids, serials = parseKindleString(kindleprefs['pids'])
for pid in pids:
dedrmprefs.addvaluetoprefs('pids',pid)
if 'serials' in kindleprefs:
pids, serials = parseKindleString(kindleprefs['serials'])
for serial in serials:
dedrmprefs.addvaluetoprefs('serials',serial)
addedpidcount = len(dedrmprefs['pids']) - priorpidcount
if addedpidcount > 0:
print u"{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, u"PID" if addedpidcount==1 else u"PIDs")
addedserialcount = len(dedrmprefs['serials']) - priorserialcount
if addedserialcount > 0:
print u"{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedserialcount, u"serial number" if addedserialcount==1 else u"serial numbers")
try:
if 'wineprefix' in kindleprefs and kindleprefs['wineprefix'] != "":
dedrmprefs.set('adobewineprefix',kindleprefs['wineprefix'])
dedrmprefs.set('kindlewineprefix',kindleprefs['wineprefix'])
print u"{0} v{1}: WINEPREFIX (2) imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, kindleprefs['wineprefix'])
except:
traceback.print_exc()
# Make the json write all the prefs to disk
dedrmprefs.writeprefs()
print u"{0} v{1}: Finished setting up configuration data.".format(PLUGIN_NAME, PLUGIN_VERSION)

View File

@@ -0,0 +1,295 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement
__license__ = 'GPL v3'
# Standard Python modules.
import os, sys, re, hashlib
import json
import traceback
from calibre.utils.config import dynamic, config_dir, JSONConfig
from calibre_plugins.dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION
from calibre.constants import iswindows, isosx
class DeDRM_Prefs():
def __init__(self):
JSON_PATH = os.path.join(u"plugins", PLUGIN_NAME.strip().lower().replace(' ', '_') + '.json')
self.dedrmprefs = JSONConfig(JSON_PATH)
self.dedrmprefs.defaults['configured'] = False
self.dedrmprefs.defaults['bandnkeys'] = {}
self.dedrmprefs.defaults['adeptkeys'] = {}
self.dedrmprefs.defaults['ereaderkeys'] = {}
self.dedrmprefs.defaults['kindlekeys'] = {}
self.dedrmprefs.defaults['androidkeys'] = {}
self.dedrmprefs.defaults['pids'] = []
self.dedrmprefs.defaults['serials'] = []
self.dedrmprefs.defaults['adobewineprefix'] = ""
self.dedrmprefs.defaults['kindlewineprefix'] = ""
# initialise
# we must actually set the prefs that are dictionaries and lists
# to empty dictionaries and lists, otherwise we are unable to add to them
# as then it just adds to the (memory only) dedrmprefs.defaults versions!
if self.dedrmprefs['bandnkeys'] == {}:
self.dedrmprefs['bandnkeys'] = {}
if self.dedrmprefs['adeptkeys'] == {}:
self.dedrmprefs['adeptkeys'] = {}
if self.dedrmprefs['ereaderkeys'] == {}:
self.dedrmprefs['ereaderkeys'] = {}
if self.dedrmprefs['kindlekeys'] == {}:
self.dedrmprefs['kindlekeys'] = {}
if self.dedrmprefs['androidkeys'] == {}:
self.dedrmprefs['androidkeys'] = {}
if self.dedrmprefs['pids'] == []:
self.dedrmprefs['pids'] = []
if self.dedrmprefs['serials'] == []:
self.dedrmprefs['serials'] = []
def __getitem__(self,kind = None):
if kind is not None:
return self.dedrmprefs[kind]
return self.dedrmprefs
def set(self, kind, value):
self.dedrmprefs[kind] = value
def writeprefs(self,value = True):
self.dedrmprefs['configured'] = value
def addnamedvaluetoprefs(self, prefkind, keyname, keyvalue):
try:
if keyvalue not in self.dedrmprefs[prefkind].values():
# ensure that the keyname is unique
# by adding a number (starting with 2) to the name if it is not
namecount = 1
newname = keyname
while newname in self.dedrmprefs[prefkind]:
namecount += 1
newname = "{0:s}_{1:d}".format(keyname,namecount)
# add to the preferences
self.dedrmprefs[prefkind][newname] = keyvalue
return (True, newname)
except:
traceback.print_exc()
pass
return (False, keyname)
def addvaluetoprefs(self, prefkind, prefsvalue):
# ensure the keyvalue isn't already in the preferences
try:
if prefsvalue not in self.dedrmprefs[prefkind]:
self.dedrmprefs[prefkind].append(prefsvalue)
return True
except:
traceback.print_exc()
return False
def convertprefs(always = False):
def parseIgnobleString(keystuff):
from calibre_plugins.dedrm.ignoblekeygen import generate_key
userkeys = []
ar = keystuff.split(':')
for keystring in ar:
try:
name, ccn = keystring.split(',')
# Generate Barnes & Noble EPUB user key from name and credit card number.
keyname = u"{0}_{1}".format(name.strip(),ccn.strip()[-4:])
keyvalue = generate_key(name, ccn)
userkeys.append([keyname,keyvalue])
except Exception, e:
traceback.print_exc()
print e.args[0]
pass
return userkeys
def parseeReaderString(keystuff):
from calibre_plugins.dedrm.erdr2pml import getuser_key
userkeys = []
ar = keystuff.split(':')
for keystring in ar:
try:
name, cc = keystring.split(',')
# Generate eReader user key from name and credit card number.
keyname = u"{0}_{1}".format(name.strip(),cc.strip()[-4:])
keyvalue = getuser_key(name,cc).encode('hex')
userkeys.append([keyname,keyvalue])
except Exception, e:
traceback.print_exc()
print e.args[0]
pass
return userkeys
def parseKindleString(keystuff):
pids = []
serials = []
ar = keystuff.split(',')
for keystring in ar:
keystring = str(keystring).strip().replace(" ","")
if len(keystring) == 10 or len(keystring) == 8 and keystring not in pids:
pids.append(keystring)
elif len(keystring) == 16 and keystring[0] == 'B' and keystring not in serials:
serials.append(keystring)
return (pids,serials)
def getConfigFiles(extension, encoding = None):
# get any files with extension 'extension' in the config dir
userkeys = []
files = [f for f in os.listdir(config_dir) if f.endswith(extension)]
for filename in files:
try:
fpath = os.path.join(config_dir, filename)
key = os.path.splitext(filename)[0]
value = open(fpath, 'rb').read()
if encoding is not None:
value = value.encode(encoding)
userkeys.append([key,value])
except:
traceback.print_exc()
pass
return userkeys
dedrmprefs = DeDRM_Prefs()
if (not always) and dedrmprefs['configured']:
# We've already converted old preferences,
# and we're not being forced to do it again, so just return
return
print u"{0} v{1}: Importing configuration data from old DeDRM plugins".format(PLUGIN_NAME, PLUGIN_VERSION)
IGNOBLEPLUGINNAME = "Ignoble Epub DeDRM"
EREADERPLUGINNAME = "eReader PDB 2 PML"
OLDKINDLEPLUGINNAME = "K4PC, K4Mac, Kindle Mobi and Topaz DeDRM"
# get prefs from older tools
kindleprefs = JSONConfig(os.path.join(u"plugins", u"K4MobiDeDRM"))
ignobleprefs = JSONConfig(os.path.join(u"plugins", u"ignoble_epub_dedrm"))
# Handle the old ignoble plugin's customization string by converting the
# old string to stored keys... get that personal data out of plain sight.
from calibre.customize.ui import config
sc = config['plugin_customization']
val = sc.pop(IGNOBLEPLUGINNAME, None)
if val is not None:
print u"{0} v{1}: Converting old Ignoble plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION)
priorkeycount = len(dedrmprefs['bandnkeys'])
userkeys = parseIgnobleString(str(val))
for keypair in userkeys:
name = keypair[0]
value = keypair[1]
dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value)
addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount
print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from old Ignoble plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys")
# Make the json write all the prefs to disk
dedrmprefs.writeprefs(False)
# Handle the old eReader plugin's customization string by converting the
# old string to stored keys... get that personal data out of plain sight.
val = sc.pop(EREADERPLUGINNAME, None)
if val is not None:
print u"{0} v{1}: Converting old eReader plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION)
priorkeycount = len(dedrmprefs['ereaderkeys'])
userkeys = parseeReaderString(str(val))
for keypair in userkeys:
name = keypair[0]
value = keypair[1]
dedrmprefs.addnamedvaluetoprefs('ereaderkeys', name, value)
addedkeycount = len(dedrmprefs['ereaderkeys'])-priorkeycount
print u"{0} v{1}: {2:d} eReader {3} imported from old eReader plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys")
# Make the json write all the prefs to disk
dedrmprefs.writeprefs(False)
# get old Kindle plugin configuration string
val = sc.pop(OLDKINDLEPLUGINNAME, None)
if val is not None:
print u"{0} v{1}: Converting old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION)
priorpidcount = len(dedrmprefs['pids'])
priorserialcount = len(dedrmprefs['serials'])
pids, serials = parseKindleString(val)
for pid in pids:
dedrmprefs.addvaluetoprefs('pids',pid)
for serial in serials:
dedrmprefs.addvaluetoprefs('serials',serial)
addedpidcount = len(dedrmprefs['pids']) - priorpidcount
addedserialcount = len(dedrmprefs['serials']) - priorserialcount
print u"{0} v{1}: {2:d} {3} and {4:d} {5} imported from old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, u"PID" if addedpidcount==1 else u"PIDs", addedserialcount, u"serial number" if addedserialcount==1 else u"serial numbers")
# Make the json write all the prefs to disk
dedrmprefs.writeprefs(False)
# copy the customisations back into calibre preferences, as we've now removed the nasty plaintext
config['plugin_customization'] = sc
# get any .b64 files in the config dir
priorkeycount = len(dedrmprefs['bandnkeys'])
bandnfilekeys = getConfigFiles('.b64')
for keypair in bandnfilekeys:
name = keypair[0]
value = keypair[1]
dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value)
addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount
if addedkeycount > 0:
print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key file" if addedkeycount==1 else u"key files")
# Make the json write all the prefs to disk
dedrmprefs.writeprefs(False)
# get any .der files in the config dir
priorkeycount = len(dedrmprefs['adeptkeys'])
adeptfilekeys = getConfigFiles('.der','hex')
for keypair in adeptfilekeys:
name = keypair[0]
value = keypair[1]
dedrmprefs.addnamedvaluetoprefs('adeptkeys', name, value)
addedkeycount = len(dedrmprefs['adeptkeys'])-priorkeycount
if addedkeycount > 0:
print u"{0} v{1}: {2:d} Adobe Adept {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"keyfile" if addedkeycount==1 else u"keyfiles")
# Make the json write all the prefs to disk
dedrmprefs.writeprefs(False)
# get ignoble json prefs
if 'keys' in ignobleprefs:
priorkeycount = len(dedrmprefs['bandnkeys'])
for name in ignobleprefs['keys']:
value = ignobleprefs['keys'][name]
dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value)
addedkeycount = len(dedrmprefs['bandnkeys']) - priorkeycount
# no need to delete old prefs, since they contain no recoverable private data
if addedkeycount > 0:
print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from Ignoble plugin preferences.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys")
# Make the json write all the prefs to disk
dedrmprefs.writeprefs(False)
# get kindle json prefs
priorpidcount = len(dedrmprefs['pids'])
priorserialcount = len(dedrmprefs['serials'])
if 'pids' in kindleprefs:
pids, serials = parseKindleString(kindleprefs['pids'])
for pid in pids:
dedrmprefs.addvaluetoprefs('pids',pid)
if 'serials' in kindleprefs:
pids, serials = parseKindleString(kindleprefs['serials'])
for serial in serials:
dedrmprefs.addvaluetoprefs('serials',serial)
addedpidcount = len(dedrmprefs['pids']) - priorpidcount
if addedpidcount > 0:
print u"{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, u"PID" if addedpidcount==1 else u"PIDs")
addedserialcount = len(dedrmprefs['serials']) - priorserialcount
if addedserialcount > 0:
print u"{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedserialcount, u"serial number" if addedserialcount==1 else u"serial numbers")
try:
if 'wineprefix' in kindleprefs and kindleprefs['wineprefix'] != "":
dedrmprefs.set('adobewineprefix',kindleprefs['wineprefix'])
dedrmprefs.set('kindlewineprefix',kindleprefs['wineprefix'])
print u"{0} v{1}: WINEPREFIX (2) imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, kindleprefs['wineprefix'])
except:
traceback.print_exc()
# Make the json write all the prefs to disk
dedrmprefs.writeprefs()
print u"{0} v{1}: Finished setting up configuration data.".format(PLUGIN_NAME, PLUGIN_VERSION)

View File

@@ -0,0 +1,198 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
import sys
import os
import re
import ineptepub
import ignobleepub
import epubtest
import zipfix
import ineptpdf
import erdr2pml
import k4mobidedrm
import traceback
def decryptepub(infile, outdir, rscpath):
errlog = ''
# first fix the epub to make sure we do not get errors
name, ext = os.path.splitext(os.path.basename(infile))
bpath = os.path.dirname(infile)
zippath = os.path.join(bpath,name + '_temp.zip')
rv = zipfix.repairBook(infile, zippath)
if rv != 0:
print "Error while trying to fix epub"
return rv
# determine a good name for the output file
outfile = os.path.join(outdir, name + '_nodrm.epub')
rv = 1
# first try with the Adobe adept epub
if ineptepub.adeptBook(zippath):
# try with any keyfiles (*.der) in the rscpath
files = os.listdir(rscpath)
filefilter = re.compile("\.der$", re.IGNORECASE)
files = filter(filefilter.search, files)
if files:
for filename in files:
keypath = os.path.join(rscpath, filename)
userkey = open(keypath,'rb').read()
try:
rv = ineptepub.decryptBook(userkey, zippath, outfile)
if rv == 0:
print "Decrypted Adobe ePub with key file {0}".format(filename)
break
except Exception, e:
errlog += traceback.format_exc()
errlog += str(e)
rv = 1
# now try with ignoble epub
elif ignobleepub.ignobleBook(zippath):
# try with any keyfiles (*.b64) in the rscpath
files = os.listdir(rscpath)
filefilter = re.compile("\.b64$", re.IGNORECASE)
files = filter(filefilter.search, files)
if files:
for filename in files:
keypath = os.path.join(rscpath, filename)
userkey = open(keypath,'r').read()
#print userkey
try:
rv = ignobleepub.decryptBook(userkey, zippath, outfile)
if rv == 0:
print "Decrypted B&N ePub with key file {0}".format(filename)
break
except Exception, e:
errlog += traceback.format_exc()
errlog += str(e)
rv = 1
else:
encryption = epubtest.encryption(zippath)
if encryption == "Unencrypted":
print "{0} is not DRMed.".format(name)
rv = 0
else:
print "{0} has an unknown encryption.".format(name)
os.remove(zippath)
if rv != 0:
print errlog
return rv
def decryptpdf(infile, outdir, rscpath):
errlog = ''
rv = 1
# determine a good name for the output file
name, ext = os.path.splitext(os.path.basename(infile))
outfile = os.path.join(outdir, name + '_nodrm.pdf')
# try with any keyfiles (*.der) in the rscpath
files = os.listdir(rscpath)
filefilter = re.compile("\.der$", re.IGNORECASE)
files = filter(filefilter.search, files)
if files:
for filename in files:
keypath = os.path.join(rscpath, filename)
userkey = open(keypath,'rb').read()
try:
rv = ineptpdf.decryptBook(userkey, infile, outfile)
if rv == 0:
break
except Exception, e:
errlog += traceback.format_exc()
errlog += str(e)
rv = 1
if rv != 0:
print errlog
return rv
def decryptpdb(infile, outdir, rscpath):
outname = os.path.splitext(os.path.basename(infile))[0] + ".pmlz"
outpath = os.path.join(outdir, outname)
rv = 1
socialpath = os.path.join(rscpath,'sdrmlist.txt')
if os.path.exists(socialpath):
keydata = file(socialpath,'r').read()
keydata = keydata.rstrip(os.linesep)
ar = keydata.split(',')
for i in ar:
try:
name, cc8 = i.split(':')
except ValueError:
print ' Error parsing user supplied social drm data.'
return 1
try:
rv = erdr2pml.decryptBook(infile, outpath, True, erdr2pml.getuser_key(name, cc8))
except Exception, e:
errlog += traceback.format_exc()
errlog += str(e)
rv = 1
if rv == 0:
break
return rv
def decryptk4mobi(infile, outdir, rscpath):
rv = 1
pidnums = []
pidspath = os.path.join(rscpath,'pidlist.txt')
if os.path.exists(pidspath):
pidstr = file(pidspath,'r').read()
pidstr = pidstr.rstrip(os.linesep)
pidstr = pidstr.strip()
if pidstr != '':
pidnums = pidstr.split(',')
serialnums = []
serialnumspath = os.path.join(rscpath,'seriallist.txt')
if os.path.exists(serialnumspath):
serialstr = file(serialnumspath,'r').read()
serialstr = serialstr.rstrip(os.linesep)
serialstr = serialstr.strip()
if serialstr != '':
serialnums = serialstr.split(',')
kDatabaseFiles = []
files = os.listdir(rscpath)
filefilter = re.compile("\.k4i$", re.IGNORECASE)
files = filter(filefilter.search, files)
if files:
for filename in files:
dpath = os.path.join(rscpath,filename)
kDatabaseFiles.append(dpath)
androidFiles = []
files = os.listdir(rscpath)
filefilter = re.compile("\.ab$", re.IGNORECASE)
files = filter(filefilter.search, files)
if files:
for filename in files:
dpath = os.path.join(rscpath,filename)
androidFiles.append(dpath)
files = os.listdir(rscpath)
filefilter = re.compile("\.db$", re.IGNORECASE)
files = filter(filefilter.search, files)
if files:
for filename in files:
dpath = os.path.join(rscpath,filename)
androidFiles.append(dpath)
files = os.listdir(rscpath)
filefilter = re.compile("\.xml$", re.IGNORECASE)
files = filter(filefilter.search, files)
if files:
for filename in files:
dpath = os.path.join(rscpath,filename)
androidFiles.append(dpath)
try:
rv = k4mobidedrm.decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serialnums, pidnums)
except Exception, e:
errlog += traceback.format_exc()
errlog += str(e)
rv = 1
return rv

View File

@@ -0,0 +1,27 @@
#!/usr/bin/env python
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
import Tkinter
import Tkconstants
# basic scrolled text widget
class ScrolledText(Tkinter.Text):
def __init__(self, master=None, **kw):
self.frame = Tkinter.Frame(master)
self.vbar = Tkinter.Scrollbar(self.frame)
self.vbar.pack(side=Tkconstants.RIGHT, fill=Tkconstants.Y)
kw.update({'yscrollcommand': self.vbar.set})
Tkinter.Text.__init__(self, self.frame, **kw)
self.pack(side=Tkconstants.LEFT, fill=Tkconstants.BOTH, expand=True)
self.vbar['command'] = self.yview
# Copy geometry methods of self.frame without overriding Text
# methods = hack!
text_meths = vars(Tkinter.Text).keys()
methods = vars(Tkinter.Pack).keys() + vars(Tkinter.Grid).keys() + vars(Tkinter.Place).keys()
methods = set(methods).difference(text_meths)
for m in methods:
if m[0] != '_' and m != 'config' and m != 'configure':
setattr(self, m, getattr(self.frame, m))
def __str__(self):
return str(self.frame)

View File

@@ -0,0 +1,77 @@
#!/usr/bin/env python
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
import sys
import os, os.path
import shutil
class SimplePrefsError(Exception):
pass
class SimplePrefs(object):
def __init__(self, target, description):
self.prefs = {}
self.key2file={}
self.file2key={}
for keyfilemap in description:
[key, filename] = keyfilemap
self.key2file[key] = filename
self.file2key[filename] = key
self.target = target + 'Prefs'
if sys.platform.startswith('win'):
import _winreg as winreg
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
prefdir = path + os.sep + self.target
elif sys.platform.startswith('darwin'):
home = os.getenv('HOME')
prefdir = os.path.join(home,'Library','Preferences','org.' + self.target)
else:
# linux and various flavors of unix
home = os.getenv('HOME')
prefdir = os.path.join(home,'.' + self.target)
if not os.path.exists(prefdir):
os.makedirs(prefdir)
self.prefdir = prefdir
self.prefs['dir'] = self.prefdir
self._loadPreferences()
def _loadPreferences(self):
filenames = os.listdir(self.prefdir)
for filename in filenames:
if filename in self.file2key:
key = self.file2key[filename]
filepath = os.path.join(self.prefdir,filename)
if os.path.isfile(filepath):
try :
data = file(filepath,'rb').read()
self.prefs[key] = data
except Exception, e:
pass
def getPreferences(self):
return self.prefs
def setPreferences(self, newprefs={}):
if 'dir' not in newprefs:
raise SimplePrefsError('Error: Attempt to Set Preferences in unspecified directory')
if newprefs['dir'] != self.prefs['dir']:
raise SimplePrefsError('Error: Attempt to Set Preferences in unspecified directory')
for key in newprefs:
if key != 'dir':
if key in self.key2file:
filename = self.key2file[key]
filepath = os.path.join(self.prefdir,filename)
data = newprefs[key]
if data != None:
data = str(data)
if data == None or data == '':
if os.path.exists(filepath):
os.remove(filepath)
else:
try:
file(filepath,'wb').write(data)
except Exception, e:
pass
self.prefs = newprefs
return

View File

@@ -178,7 +178,12 @@ class DocParser(object):
if val == "":
val = 0
if not ((attr == 'hang') and (int(val) == 0)) :
if not ((attr == 'hang') and (int(val) == 0)):
try:
f = float(val)
except:
print "Warning: unrecognised val, ignoring"
val = 0
pv = float(val)/scale
cssargs[attr] = (self.attr_val_map[attr], pv)
keep = True

View File

@@ -0,0 +1,148 @@
#!/usr/bin/env python
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
import os, sys
import signal
import threading
import subprocess
from subprocess import Popen, PIPE, STDOUT
# **heavily** chopped up and modfied version of asyncproc.py
# to make it actually work on Windows as well as Mac/Linux
# For the original see:
# "http://www.lysator.liu.se/~bellman/download/"
# author is "Thomas Bellman <bellman@lysator.liu.se>"
# available under GPL version 3 or Later
# create an asynchronous subprocess whose output can be collected in
# a non-blocking manner
# What a mess! Have to use threads just to get non-blocking io
# in a cross-platform manner
# luckily all thread use is hidden within this class
class Process(object):
def __init__(self, *params, **kwparams):
if len(params) <= 3:
kwparams.setdefault('stdin', subprocess.PIPE)
if len(params) <= 4:
kwparams.setdefault('stdout', subprocess.PIPE)
if len(params) <= 5:
kwparams.setdefault('stderr', subprocess.PIPE)
self.__pending_input = []
self.__collected_outdata = []
self.__collected_errdata = []
self.__exitstatus = None
self.__lock = threading.Lock()
self.__inputsem = threading.Semaphore(0)
self.__quit = False
self.__process = subprocess.Popen(*params, **kwparams)
if self.__process.stdin:
self.__stdin_thread = threading.Thread(
name="stdin-thread",
target=self.__feeder, args=(self.__pending_input,
self.__process.stdin))
self.__stdin_thread.setDaemon(True)
self.__stdin_thread.start()
if self.__process.stdout:
self.__stdout_thread = threading.Thread(
name="stdout-thread",
target=self.__reader, args=(self.__collected_outdata,
self.__process.stdout))
self.__stdout_thread.setDaemon(True)
self.__stdout_thread.start()
if self.__process.stderr:
self.__stderr_thread = threading.Thread(
name="stderr-thread",
target=self.__reader, args=(self.__collected_errdata,
self.__process.stderr))
self.__stderr_thread.setDaemon(True)
self.__stderr_thread.start()
def pid(self):
return self.__process.pid
def kill(self, signal):
self.__process.send_signal(signal)
# check on subprocess (pass in 'nowait') to act like poll
def wait(self, flag):
if flag.lower() == 'nowait':
rc = self.__process.poll()
else:
rc = self.__process.wait()
if rc != None:
if self.__process.stdin:
self.closeinput()
if self.__process.stdout:
self.__stdout_thread.join()
if self.__process.stderr:
self.__stderr_thread.join()
return self.__process.returncode
def terminate(self):
if self.__process.stdin:
self.closeinput()
self.__process.terminate()
# thread gets data from subprocess stdout
def __reader(self, collector, source):
while True:
data = os.read(source.fileno(), 65536)
self.__lock.acquire()
collector.append(data)
self.__lock.release()
if data == "":
source.close()
break
return
# thread feeds data to subprocess stdin
def __feeder(self, pending, drain):
while True:
self.__inputsem.acquire()
self.__lock.acquire()
if not pending and self.__quit:
drain.close()
self.__lock.release()
break
data = pending.pop(0)
self.__lock.release()
drain.write(data)
# non-blocking read of data from subprocess stdout
def read(self):
self.__lock.acquire()
outdata = "".join(self.__collected_outdata)
del self.__collected_outdata[:]
self.__lock.release()
return outdata
# non-blocking read of data from subprocess stderr
def readerr(self):
self.__lock.acquire()
errdata = "".join(self.__collected_errdata)
del self.__collected_errdata[:]
self.__lock.release()
return errdata
# non-blocking write to stdin of subprocess
def write(self, data):
if self.__process.stdin is None:
raise ValueError("Writing to process with stdin not a pipe")
self.__lock.acquire()
self.__pending_input.append(data)
self.__inputsem.release()
self.__lock.release()
# close stdinput of subprocess
def closeinput(self):
self.__lock.acquire()
self.__quit = True
self.__inputsem.release()
self.__lock.release()

View File

@@ -1,148 +1,538 @@
#!/usr/bin/env python
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
# -*- coding: utf-8 -*-
import os, sys
import signal
import threading
import subprocess
from subprocess import Popen, PIPE, STDOUT
# topazextract.py
# Mostly written by some_updates based on code from many others
# **heavily** chopped up and modfied version of asyncproc.py
# to make it actually work on Windows as well as Mac/Linux
# For the original see:
# "http://www.lysator.liu.se/~bellman/download/"
# author is "Thomas Bellman <bellman@lysator.liu.se>"
# available under GPL version 3 or Later
# Changelog
# 4.9 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 5.0 - Fixed potential unicode problem with command line interface
# create an asynchronous subprocess whose output can be collected in
# a non-blocking manner
__version__ = '5.0'
# What a mess! Have to use threads just to get non-blocking io
# in a cross-platform manner
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
# luckily all thread use is hidden within this class
class Process(object):
def __init__(self, *params, **kwparams):
if len(params) <= 3:
kwparams.setdefault('stdin', subprocess.PIPE)
if len(params) <= 4:
kwparams.setdefault('stdout', subprocess.PIPE)
if len(params) <= 5:
kwparams.setdefault('stderr', subprocess.PIPE)
self.__pending_input = []
self.__collected_outdata = []
self.__collected_errdata = []
self.__exitstatus = None
self.__lock = threading.Lock()
self.__inputsem = threading.Semaphore(0)
self.__quit = False
self.__process = subprocess.Popen(*params, **kwparams)
if self.__process.stdin:
self.__stdin_thread = threading.Thread(
name="stdin-thread",
target=self.__feeder, args=(self.__pending_input,
self.__process.stdin))
self.__stdin_thread.setDaemon(True)
self.__stdin_thread.start()
if self.__process.stdout:
self.__stdout_thread = threading.Thread(
name="stdout-thread",
target=self.__reader, args=(self.__collected_outdata,
self.__process.stdout))
self.__stdout_thread.setDaemon(True)
self.__stdout_thread.start()
if self.__process.stderr:
self.__stderr_thread = threading.Thread(
name="stderr-thread",
target=self.__reader, args=(self.__collected_errdata,
self.__process.stderr))
self.__stderr_thread.setDaemon(True)
self.__stderr_thread.start()
def pid(self):
return self.__process.pid
def kill(self, signal):
self.__process.send_signal(signal)
# check on subprocess (pass in 'nowait') to act like poll
def wait(self, flag):
if flag.lower() == 'nowait':
rc = self.__process.poll()
else:
rc = self.__process.wait()
if rc != None:
if self.__process.stdin:
self.closeinput()
if self.__process.stdout:
self.__stdout_thread.join()
if self.__process.stderr:
self.__stderr_thread.join()
return self.__process.returncode
def terminate(self):
if self.__process.stdin:
self.closeinput()
self.__process.terminate()
# thread gets data from subprocess stdout
def __reader(self, collector, source):
while True:
data = os.read(source.fileno(), 65536)
self.__lock.acquire()
collector.append(data)
self.__lock.release()
if data == "":
source.close()
break
return
# thread feeds data to subprocess stdin
def __feeder(self, pending, drain):
while True:
self.__inputsem.acquire()
self.__lock.acquire()
if not pending and self.__quit:
drain.close()
self.__lock.release()
break
data = pending.pop(0)
self.__lock.release()
drain.write(data)
# non-blocking read of data from subprocess stdout
def read(self):
self.__lock.acquire()
outdata = "".join(self.__collected_outdata)
del self.__collected_outdata[:]
self.__lock.release()
return outdata
# non-blocking read of data from subprocess stderr
def readerr(self):
self.__lock.acquire()
errdata = "".join(self.__collected_errdata)
del self.__collected_errdata[:]
self.__lock.release()
return errdata
# non-blocking write to stdin of subprocess
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 self.__process.stdin is None:
raise ValueError("Writing to process with stdin not a pipe")
self.__lock.acquire()
self.__pending_input.append(data)
self.__inputsem.release()
self.__lock.release()
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)
# close stdinput of subprocess
def closeinput(self):
self.__lock.acquire()
self.__quit = True
self.__inputsem.release()
self.__lock.release()
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]
#global switch
debug = False
if 'calibre' in sys.modules:
inCalibre = True
from calibre_plugins.dedrm 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 != u"":
currentdir = os.path.join(currentdir,localname)
list = os.listdir(currentdir)
for file in list:
afilename = file
localfilePath = os.path.join(localname, afilename)
realfilePath = os.path.join(currentdir,file)
if os.path.isfile(realfilePath):
myzip.write(realfilePath, localfilePath)
elif os.path.isdir(realfilePath):
zipUpDir(myzip, tdir, localfilePath)
#
# Utility routines
#
# Get a 7 bit encoded number from file
def bookReadEncodedNumber(fo):
flag = False
data = ord(fo.read(1))
if data == 0xFF:
flag = True
data = ord(fo.read(1))
if data >= 0x80:
datax = (data & 0x7F)
while data >= 0x80 :
data = ord(fo.read(1))
datax = (datax <<7) + (data & 0x7F)
data = datax
if flag:
data = -data
return data
# Get a length prefixed string from file
def bookReadString(fo):
stringLength = bookReadEncodedNumber(fo)
return unpack(str(stringLength)+'s',fo.read(stringLength))[0]
#
# crypto routines
#
# Context initialisation for the Topaz Crypto
def topazCryptoInit(key):
return Topaz_Cipher().ctx_init(key)
# ctx1 = 0x0CAFFE19E
# for keyChar in key:
# keyByte = ord(keyChar)
# ctx2 = ctx1
# ctx1 = ((((ctx1 >>2) * (ctx1 >>7))&0xFFFFFFFF) ^ (keyByte * keyByte * 0x0F902007)& 0xFFFFFFFF )
# return [ctx1,ctx2]
# decrypt data with the context prepared by topazCryptoInit()
def topazCryptoDecrypt(data, ctx):
return Topaz_Cipher().decrypt(data, ctx)
# ctx1 = ctx[0]
# ctx2 = ctx[1]
# plainText = ""
# for dataChar in data:
# dataByte = ord(dataChar)
# m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF
# ctx2 = ctx1
# ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF)
# plainText += chr(m)
# return plainText
# Decrypt data with the PID
def decryptRecord(data,PID):
ctx = topazCryptoInit(PID)
return topazCryptoDecrypt(data, ctx)
# 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 DrmException(u"Didn't find PID magic numbers in record")
elif fields[1] != 8 or fields[3] != 8 :
raise DrmException(u"Record didn't contain correct length fields")
elif fields[2] != PID :
raise DrmException(u"Record didn't contain PID")
return fields[4]
# Decrypt all dkey records (contain the book PID)
def decryptDkeyRecords(data,PID):
nbKeyRecords = ord(data[0])
records = []
data = data[1:]
for i in range (0,nbKeyRecords):
length = ord(data[0])
try:
key = decryptDkeyRecord(data[1:length+1],PID)
records.append(key)
except DrmException:
pass
data = data[1+length:]
if len(records) == 0:
raise DrmException(u"BookKey Not Found")
return records
class TopazBook:
def __init__(self, filename):
self.fo = file(filename, 'rb')
self.outdir = tempfile.mkdtemp()
# self.outdir = 'rawdat'
self.bookPayloadOffset = 0
self.bookHeaderRecords = {}
self.bookMetadata = {}
self.bookKey = None
magic = unpack('4s',self.fo.read(4))[0]
if magic != 'TPZ0':
raise DrmException(u"Parse Error : Invalid Header, not a Topaz file")
self.parseTopazHeaders()
self.parseMetadata()
def parseTopazHeaders(self):
def bookReadHeaderRecordData():
# Read and return the data of one header record at the current book file position
# [[offset,decompressedLength,compressedLength],...]
nbValues = bookReadEncodedNumber(self.fo)
if debug: print "%d records in header " % nbValues,
values = []
for i in range (0,nbValues):
values.append([bookReadEncodedNumber(self.fo),bookReadEncodedNumber(self.fo),bookReadEncodedNumber(self.fo)])
return values
def parseTopazHeaderRecord():
# 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 DrmException(u"Parse Error : Invalid Header")
tag = bookReadString(self.fo)
record = bookReadHeaderRecordData()
return [tag,record]
nbRecords = bookReadEncodedNumber(self.fo)
if debug: print "Headers: %d" % nbRecords
for i in range (0,nbRecords):
result = parseTopazHeaderRecord()
if debug: print result[0], ": ", result[1]
self.bookHeaderRecords[result[0]] = result[1]
if ord(self.fo.read(1)) != 0x64 :
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])
tag = bookReadString(self.fo)
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))
if debug: print "Metadata Records: %d" % nbRecords
for i in range (0,nbRecords) :
keyval = bookReadString(self.fo)
content = bookReadString(self.fo)
if debug: print keyval
if debug: print content
self.bookMetadata[keyval] = content
return self.bookMetadata
def getPIDMetaInfo(self):
keysRecord = self.bookMetadata.get('keys','')
keysRecordRecord = ''
if keysRecord != '':
keylst = keysRecord.split(',')
for keyval in keylst:
keysRecordRecord += self.bookMetadata.get(keyval,'')
return keysRecord, keysRecordRecord
def getBookTitle(self):
title = ''
if 'Title' in self.bookMetadata:
title = self.bookMetadata['Title']
return title.decode('utf-8')
def setBookKey(self, key):
self.bookKey = key
def getBookPayloadRecord(self, name, index):
# Get a record in the book payload, given its name and index.
# decrypted and decompressed if necessary
encrypted = False
compressed = False
try:
recordOffset = self.bookHeaderRecords[name][index][0]
except:
raise DrmException("Parse Error : Invalid Record, record not found")
self.fo.seek(self.bookPayloadOffset + recordOffset)
tag = bookReadString(self.fo)
if tag != name :
raise DrmException("Parse Error : Invalid Record, record name doesn't match")
recordIndex = bookReadEncodedNumber(self.fo)
if recordIndex < 0 :
encrypted = True
recordIndex = -recordIndex -1
if recordIndex != index :
raise DrmException("Parse Error : Invalid Record, index doesn't match")
if (self.bookHeaderRecords[name][index][2] > 0):
compressed = True
record = self.fo.read(self.bookHeaderRecords[name][index][2])
else:
record = self.fo.read(self.bookHeaderRecords[name][index][1])
if encrypted:
if self.bookKey:
ctx = topazCryptoInit(self.bookKey)
record = topazCryptoDecrypt(record,ctx)
else :
raise DrmException("Error: Attempt to decrypt without bookKey")
if compressed:
record = zlib.decompress(record)
return record
def processBook(self, pidlst):
raw = 0
fixedimage=True
try:
keydata = self.getBookPayloadRecord('dkey', 0)
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 u"Successfully Extracted Topaz contents"
if inCalibre:
from calibre_plugins.dedrm import genbook
else:
import genbook
rv = genbook.generateBook(self.outdir, raw, fixedimage)
if rv == 0:
print u"Book Successfully generated."
return rv
# try each pid to decode the file
bookKey = None
for pid in pidlst:
# use 8 digit pids here
pid = pid[0:8]
print u"Trying: {0}".format(pid)
bookKeys = []
data = keydata
try:
bookKeys+=decryptDkeyRecords(data,pid)
except DrmException, e:
pass
else:
bookKey = bookKeys[0]
print u"Book Key Found! ({0})".format(bookKey.encode('hex'))
break
if not bookKey:
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 u"Successfully Extracted Topaz contents"
if inCalibre:
from calibre_plugins.dedrm import genbook
else:
import genbook
rv = genbook.generateBook(self.outdir, raw, fixedimage)
if rv == 0:
print u"Book Successfully generated"
return rv
def createBookDirectory(self):
outdir = self.outdir
# create output directory structure
if not os.path.exists(outdir):
os.makedirs(outdir)
destdir = os.path.join(outdir,u"img")
if not os.path.exists(destdir):
os.makedirs(destdir)
destdir = os.path.join(outdir,u"color_img")
if not os.path.exists(destdir):
os.makedirs(destdir)
destdir = os.path.join(outdir,u"page")
if not os.path.exists(destdir):
os.makedirs(destdir)
destdir = os.path.join(outdir,u"glyphs")
if not os.path.exists(destdir):
os.makedirs(destdir)
def extractFiles(self):
outdir = self.outdir
for headerRecord in self.bookHeaderRecords:
name = headerRecord
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])) :
fname = u"{0}{1:04d}{2}".format(name,index,ext)
destdir = outdir
if name == 'img':
destdir = os.path.join(outdir,u"img")
if name == 'color':
destdir = os.path.join(outdir,u"color_img")
if name == 'page':
destdir = os.path.join(outdir,u"page")
if name == 'glyphs':
destdir = os.path.join(outdir,u"glyphs")
outputFile = os.path.join(destdir,fname)
print u".",
record = self.getBookPayloadRecord(name,index)
if record != '':
file(outputFile, 'wb').write(record)
print u" "
def getFile(self, zipname):
htmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
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,u"index_svg.xhtml"),u"index_svg.xhtml")
zipUpDir(svgzip, self.outdir, u"svg")
zipUpDir(svgzip, self.outdir, u"img")
svgzip.close()
def cleanup(self):
if os.path.isdir(self.outdir):
shutil.rmtree(self.outdir, True)
def usage(progname):
print u"Removes DRM protection from Topaz ebooks and extracts the contents"
print u"Usage:"
print u" {0} [-k <kindle.k4i>] [-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"TopazExtract v{0}.".format(__version__)
try:
opts, args = getopt.getopt(argv[1:], "k:p:s:x")
except getopt.GetoptError, 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
infile = args[0]
outdir = args[1]
if not os.path.isfile(infile):
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
kDatabaseFiles = []
serials = []
pids = []
for o, a in opts:
if o == '-k':
if a == None :
raise DrmException("Invalid parameter for -k")
kDatabaseFiles.append(a)
if o == '-p':
if a == None :
raise DrmException("Invalid parameter for -p")
pids = a.split(',')
if o == '-s':
if a == None :
raise DrmException("Invalid parameter for -s")
serials = [serial.replace(" ","") for serial in a.split(',')]
bookname = os.path.splitext(os.path.basename(infile))[0]
tb = TopazBook(infile)
title = tb.getBookTitle()
print u"Processing Book: {0}".format(title)
md1, md2 = tb.getPIDMetaInfo()
pids.extend(kgenpids.getPidList(md1, md2, serials, kDatabaseFiles))
try:
print u"Decrypting Book"
tb.processBook(pids)
print u" Creating HTML ZIP Archive"
zipname = os.path.join(outdir, bookname + u"_nodrm.htmlz")
tb.getFile(zipname)
print u" Creating SVG ZIP Archive"
zipname = os.path.join(outdir, bookname + u"_SVG.zip")
tb.getSVGZip(zipname)
# removing internal temporary directory of pieces
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 u"Decryption failed\m{0}".format(traceback.format_exc())
try:
tb.cleanup()
except:
pass
return 1
return 0
if __name__ == '__main__':
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
sys.exit(cli_main())

View File

@@ -1,538 +1,39 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# topazextract.py
# Mostly written by some_updates based on code from many others
from __future__ import with_statement
# Changelog
# 4.9 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 5.0 - Fixed potential unicode problem with command line interface
__license__ = 'GPL v3'
__version__ = '5.0'
DETAILED_MESSAGE = \
'You have personal information stored in this plugin\'s customization '+ \
'string from a previous version of this plugin.\n\n'+ \
'This new version of the plugin can convert that info '+ \
'into key data that the new plugin can then use (which doesn\'t '+ \
'require personal information to be stored/displayed in an insecure '+ \
'manner like the old plugin did).\n\nIf you choose NOT to migrate this data at this time '+ \
'you will be prompted to save that personal data to a file elsewhere; and you\'ll have '+ \
'to manually re-configure this plugin with your information.\n\nEither way... ' + \
'this new version of the plugin will not be responsible for storing that personal '+ \
'info in plain sight any longer.'
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)
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"]
def uStrCmp (s1, s2, caseless=False):
import unicodedata as ud
str1 = s1 if isinstance(s1, unicode) else unicode(s1)
str2 = s2 if isinstance(s2, unicode) else unicode(s2)
if caseless:
return ud.normalize('NFC', str1.lower()) == ud.normalize('NFC', str2.lower())
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]
return ud.normalize('NFC', str1) == ud.normalize('NFC', str2)
#global switch
debug = False
if 'calibre' in sys.modules:
inCalibre = True
from calibre_plugins.dedrm 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 != u"":
currentdir = os.path.join(currentdir,localname)
list = os.listdir(currentdir)
for file in list:
afilename = file
localfilePath = os.path.join(localname, afilename)
realfilePath = os.path.join(currentdir,file)
if os.path.isfile(realfilePath):
myzip.write(realfilePath, localfilePath)
elif os.path.isdir(realfilePath):
zipUpDir(myzip, tdir, localfilePath)
#
# Utility routines
#
# Get a 7 bit encoded number from file
def bookReadEncodedNumber(fo):
flag = False
data = ord(fo.read(1))
if data == 0xFF:
flag = True
data = ord(fo.read(1))
if data >= 0x80:
datax = (data & 0x7F)
while data >= 0x80 :
data = ord(fo.read(1))
datax = (datax <<7) + (data & 0x7F)
data = datax
if flag:
data = -data
return data
# Get a length prefixed string from file
def bookReadString(fo):
stringLength = bookReadEncodedNumber(fo)
return unpack(str(stringLength)+'s',fo.read(stringLength))[0]
#
# crypto routines
#
# Context initialisation for the Topaz Crypto
def topazCryptoInit(key):
return Topaz_Cipher().ctx_init(key)
# ctx1 = 0x0CAFFE19E
# for keyChar in key:
# keyByte = ord(keyChar)
# ctx2 = ctx1
# ctx1 = ((((ctx1 >>2) * (ctx1 >>7))&0xFFFFFFFF) ^ (keyByte * keyByte * 0x0F902007)& 0xFFFFFFFF )
# return [ctx1,ctx2]
# decrypt data with the context prepared by topazCryptoInit()
def topazCryptoDecrypt(data, ctx):
return Topaz_Cipher().decrypt(data, ctx)
# ctx1 = ctx[0]
# ctx2 = ctx[1]
# plainText = ""
# for dataChar in data:
# dataByte = ord(dataChar)
# m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF
# ctx2 = ctx1
# ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF)
# plainText += chr(m)
# return plainText
# Decrypt data with the PID
def decryptRecord(data,PID):
ctx = topazCryptoInit(PID)
return topazCryptoDecrypt(data, ctx)
# 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 DrmException(u"Didn't find PID magic numbers in record")
elif fields[1] != 8 or fields[3] != 8 :
raise DrmException(u"Record didn't contain correct length fields")
elif fields[2] != PID :
raise DrmException(u"Record didn't contain PID")
return fields[4]
# Decrypt all dkey records (contain the book PID)
def decryptDkeyRecords(data,PID):
nbKeyRecords = ord(data[0])
records = []
data = data[1:]
for i in range (0,nbKeyRecords):
length = ord(data[0])
def parseCustString(keystuff):
userkeys = []
ar = keystuff.split(':')
for i in ar:
try:
key = decryptDkeyRecord(data[1:length+1],PID)
records.append(key)
except DrmException:
pass
data = data[1+length:]
if len(records) == 0:
raise DrmException(u"BookKey Not Found")
return records
class TopazBook:
def __init__(self, filename):
self.fo = file(filename, 'rb')
self.outdir = tempfile.mkdtemp()
# self.outdir = 'rawdat'
self.bookPayloadOffset = 0
self.bookHeaderRecords = {}
self.bookMetadata = {}
self.bookKey = None
magic = unpack('4s',self.fo.read(4))[0]
if magic != 'TPZ0':
raise DrmException(u"Parse Error : Invalid Header, not a Topaz file")
self.parseTopazHeaders()
self.parseMetadata()
def parseTopazHeaders(self):
def bookReadHeaderRecordData():
# Read and return the data of one header record at the current book file position
# [[offset,decompressedLength,compressedLength],...]
nbValues = bookReadEncodedNumber(self.fo)
if debug: print "%d records in header " % nbValues,
values = []
for i in range (0,nbValues):
values.append([bookReadEncodedNumber(self.fo),bookReadEncodedNumber(self.fo),bookReadEncodedNumber(self.fo)])
return values
def parseTopazHeaderRecord():
# 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 DrmException(u"Parse Error : Invalid Header")
tag = bookReadString(self.fo)
record = bookReadHeaderRecordData()
return [tag,record]
nbRecords = bookReadEncodedNumber(self.fo)
if debug: print "Headers: %d" % nbRecords
for i in range (0,nbRecords):
result = parseTopazHeaderRecord()
if debug: print result[0], ": ", result[1]
self.bookHeaderRecords[result[0]] = result[1]
if ord(self.fo.read(1)) != 0x64 :
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])
tag = bookReadString(self.fo)
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))
if debug: print "Metadata Records: %d" % nbRecords
for i in range (0,nbRecords) :
keyval = bookReadString(self.fo)
content = bookReadString(self.fo)
if debug: print keyval
if debug: print content
self.bookMetadata[keyval] = content
return self.bookMetadata
def getPIDMetaInfo(self):
keysRecord = self.bookMetadata.get('keys','')
keysRecordRecord = ''
if keysRecord != '':
keylst = keysRecord.split(',')
for keyval in keylst:
keysRecordRecord += self.bookMetadata.get(keyval,'')
return keysRecord, keysRecordRecord
def getBookTitle(self):
title = ''
if 'Title' in self.bookMetadata:
title = self.bookMetadata['Title']
return title.decode('utf-8')
def setBookKey(self, key):
self.bookKey = key
def getBookPayloadRecord(self, name, index):
# Get a record in the book payload, given its name and index.
# decrypted and decompressed if necessary
encrypted = False
compressed = False
try:
recordOffset = self.bookHeaderRecords[name][index][0]
except:
raise DrmException("Parse Error : Invalid Record, record not found")
self.fo.seek(self.bookPayloadOffset + recordOffset)
tag = bookReadString(self.fo)
if tag != name :
raise DrmException("Parse Error : Invalid Record, record name doesn't match")
recordIndex = bookReadEncodedNumber(self.fo)
if recordIndex < 0 :
encrypted = True
recordIndex = -recordIndex -1
if recordIndex != index :
raise DrmException("Parse Error : Invalid Record, index doesn't match")
if (self.bookHeaderRecords[name][index][2] > 0):
compressed = True
record = self.fo.read(self.bookHeaderRecords[name][index][2])
else:
record = self.fo.read(self.bookHeaderRecords[name][index][1])
if encrypted:
if self.bookKey:
ctx = topazCryptoInit(self.bookKey)
record = topazCryptoDecrypt(record,ctx)
else :
raise DrmException("Error: Attempt to decrypt without bookKey")
if compressed:
record = zlib.decompress(record)
return record
def processBook(self, pidlst):
raw = 0
fixedimage=True
try:
keydata = self.getBookPayloadRecord('dkey', 0)
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 u"Successfully Extracted Topaz contents"
if inCalibre:
from calibre_plugins.dedrm import genbook
else:
import genbook
rv = genbook.generateBook(self.outdir, raw, fixedimage)
if rv == 0:
print u"Book Successfully generated."
return rv
# try each pid to decode the file
bookKey = None
for pid in pidlst:
# use 8 digit pids here
pid = pid[0:8]
print u"Trying: {0}".format(pid)
bookKeys = []
data = keydata
try:
bookKeys+=decryptDkeyRecords(data,pid)
except DrmException, e:
pass
else:
bookKey = bookKeys[0]
print u"Book Key Found! ({0})".format(bookKey.encode('hex'))
break
if not bookKey:
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 u"Successfully Extracted Topaz contents"
if inCalibre:
from calibre_plugins.dedrm import genbook
else:
import genbook
rv = genbook.generateBook(self.outdir, raw, fixedimage)
if rv == 0:
print u"Book Successfully generated"
return rv
def createBookDirectory(self):
outdir = self.outdir
# create output directory structure
if not os.path.exists(outdir):
os.makedirs(outdir)
destdir = os.path.join(outdir,u"img")
if not os.path.exists(destdir):
os.makedirs(destdir)
destdir = os.path.join(outdir,u"color_img")
if not os.path.exists(destdir):
os.makedirs(destdir)
destdir = os.path.join(outdir,u"page")
if not os.path.exists(destdir):
os.makedirs(destdir)
destdir = os.path.join(outdir,u"glyphs")
if not os.path.exists(destdir):
os.makedirs(destdir)
def extractFiles(self):
outdir = self.outdir
for headerRecord in self.bookHeaderRecords:
name = headerRecord
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])) :
fname = u"{0}{1:04d}{2}".format(name,index,ext)
destdir = outdir
if name == 'img':
destdir = os.path.join(outdir,u"img")
if name == 'color':
destdir = os.path.join(outdir,u"color_img")
if name == 'page':
destdir = os.path.join(outdir,u"page")
if name == 'glyphs':
destdir = os.path.join(outdir,u"glyphs")
outputFile = os.path.join(destdir,fname)
print u".",
record = self.getBookPayloadRecord(name,index)
if record != '':
file(outputFile, 'wb').write(record)
print u" "
def getFile(self, zipname):
htmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
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,u"index_svg.xhtml"),u"index_svg.xhtml")
zipUpDir(svgzip, self.outdir, u"svg")
zipUpDir(svgzip, self.outdir, u"img")
svgzip.close()
def cleanup(self):
if os.path.isdir(self.outdir):
shutil.rmtree(self.outdir, True)
def usage(progname):
print u"Removes DRM protection from Topaz ebooks and extracts the contents"
print u"Usage:"
print u" {0} [-k <kindle.k4i>] [-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"TopazExtract v{0}.".format(__version__)
try:
opts, args = getopt.getopt(argv[1:], "k:p:s:x")
except getopt.GetoptError, 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
infile = args[0]
outdir = args[1]
if not os.path.isfile(infile):
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
kDatabaseFiles = []
serials = []
pids = []
for o, a in opts:
if o == '-k':
if a == None :
raise DrmException("Invalid parameter for -k")
kDatabaseFiles.append(a)
if o == '-p':
if a == None :
raise DrmException("Invalid parameter for -p")
pids = a.split(',')
if o == '-s':
if a == None :
raise DrmException("Invalid parameter for -s")
serials = [serial.replace(" ","") for serial in a.split(',')]
bookname = os.path.splitext(os.path.basename(infile))[0]
tb = TopazBook(infile)
title = tb.getBookTitle()
print u"Processing Book: {0}".format(title)
md1, md2 = tb.getPIDMetaInfo()
pids.extend(kgenpids.getPidList(md1, md2, serials, kDatabaseFiles))
try:
print u"Decrypting Book"
tb.processBook(pids)
print u" Creating HTML ZIP Archive"
zipname = os.path.join(outdir, bookname + u"_nodrm.htmlz")
tb.getFile(zipname)
print u" Creating SVG ZIP Archive"
zipname = os.path.join(outdir, bookname + u"_SVG.zip")
tb.getSVGZip(zipname)
# removing internal temporary directory of pieces
tb.cleanup()
except DrmException, e:
print u"Decryption failed\n{0}".format(traceback.format_exc())
try:
tb.cleanup()
name, ccn = i.split(',')
# Generate Barnes & Noble EPUB user key from name and credit card number.
userkeys.append(generate_key(name, ccn))
except:
pass
return 1
except Exception, e:
print u"Decryption failed\m{0}".format(traceback.format_exc())
try:
tb.cleanup()
except:
pass
return 1
return 0
if __name__ == '__main__':
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
sys.exit(cli_main())
return userkeys

View File

@@ -0,0 +1,61 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import with_statement
__license__ = 'GPL v3'
# Standard Python modules.
import os, sys, re, hashlib
from calibre_plugins.dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION
def WineGetKeys(scriptpath, extension, wineprefix=""):
import subprocess
from subprocess import Popen, PIPE, STDOUT
import subasyncio
from subasyncio import Process
if extension == u".k4i":
import json
basepath, script = os.path.split(scriptpath)
print u"{0} v{1}: Running {2} under Wine".format(PLUGIN_NAME, PLUGIN_VERSION, script)
outdirpath = os.path.join(basepath, u"winekeysdir")
if not os.path.exists(outdirpath):
os.makedirs(outdirpath)
wineprefix = os.path.abspath(os.path.expanduser(os.path.expandvars(wineprefix)))
if wineprefix != "" and os.path.exists(wineprefix):
cmdline = u"WINEPREFIX=\"{2}\" wine python.exe \"{0}\" \"{1}\"".format(scriptpath,outdirpath,wineprefix)
else:
cmdline = u"wine python.exe \"{0}\" \"{1}\"".format(scriptpath,outdirpath)
print u"{0} v{1}: Command line: “{2}".format(PLUGIN_NAME, PLUGIN_VERSION, 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 u"{0} v{1}: Wine subprocess call error: {2}".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0])
return []
winekeys = []
# get any files with extension in the output dir
files = [f for f in os.listdir(outdirpath) if f.endswith(extension)]
for filename in files:
try:
fpath = os.path.join(outdirpath, filename)
with open(fpath, 'rb') as keyfile:
if extension == u".k4i":
new_key_value = json.loads(keyfile.read())
else:
new_key_value = keyfile.read()
winekeys.append(new_key_value)
except:
print u"{0} v{1}: Error loading file {2}".format(PLUGIN_NAME, PLUGIN_VERSION, filename)
traceback.print_exc()
os.remove(fpath)
print u"{0} v{1}: Found and decrypted {2} {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(winekeys), u"key file" if len(winekeys) == 1 else u"key files")
return winekeys

Some files were not shown because too many files have changed in this diff Show More