mirror of
https://github.com/noDRM/DeDRM_tools.git
synced 2026-03-20 13:08:55 +00:00
Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
908ebc5c58 | ||
|
|
4d7556e919 | ||
|
|
6feeb352fc | ||
|
|
bc3676c1bc | ||
|
|
a4ebec359b | ||
|
|
10cffca6b4 | ||
|
|
24a8c80617 | ||
|
|
b2338b71c0 | ||
|
|
16733c3198 | ||
|
|
3a931dfc90 | ||
|
|
eaa7a1afed | ||
|
|
dc5261870f | ||
|
|
a2ba5005c9 | ||
|
|
24922999dc | ||
|
|
e2170b4260 | ||
|
|
054ddc894b | ||
|
|
8cd4be6fb0 | ||
|
|
d67e05cf04 | ||
|
|
a5197a6abb | ||
|
|
cfc13db6c5 | ||
|
|
8aa2157d55 | ||
|
|
81b08dcf05 | ||
|
|
ca42e028a7 | ||
|
|
10963f6011 | ||
|
|
72968d2124 | ||
|
|
3e95168972 | ||
|
|
ecf1d76d90 | ||
|
|
e59f0f346d | ||
|
|
b6046d3f4b | ||
|
|
a863a4856a | ||
|
|
a13d08c3bc | ||
|
|
9434751a72 | ||
|
|
fc156852a4 | ||
|
|
0c67fd43a2 | ||
|
|
00a5c4e1d1 | ||
|
|
4ea0d81144 | ||
|
|
b1cccf4b25 | ||
|
|
fe6074949b | ||
|
|
2db7ee8894 | ||
|
|
93d8758462 | ||
|
|
f97bc078db | ||
|
|
2e96db6cdc | ||
|
|
0d530c0c46 | ||
|
|
488924d443 | ||
|
|
07485be2c0 | ||
|
|
e5e269fbae | ||
|
|
d54dc38c2d | ||
|
|
3c322f3695 | ||
|
|
c112c28f58 | ||
|
|
b8606cd182 | ||
|
|
f2190a6755 | ||
|
|
33d8a63f61 | ||
|
|
91b22c18c4 | ||
|
|
3317dc7330 | ||
|
|
aa866938f5 | ||
|
|
aa822de138 | ||
|
|
f5e66d42a1 | ||
|
|
c16d767b00 | ||
|
|
9a8d5f74a6 | ||
|
|
6be1323817 | ||
|
|
9b77255212 | ||
|
|
46426a9eae | ||
|
|
45ad3cedec | ||
|
|
d140b7e2dc | ||
|
|
e729ae8904 | ||
|
|
0837482686 | ||
|
|
4c9aacd01e | ||
|
|
6b2672ff7c | ||
|
|
39c9d57b15 | ||
|
|
9c347ca42f | ||
|
|
032fcfa422 | ||
|
|
35aaf20c8d | ||
|
|
b146e4b864 | ||
|
|
27d8f08b54 | ||
|
|
6db762bc40 | ||
|
|
c7c34274e9 | ||
|
|
cf922b6ba1 | ||
|
|
9d9c879413 | ||
|
|
c4fc10395b |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -37,7 +37,6 @@ nosetests.xml
|
||||
coverage.xml
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
|
||||
@@ -1,51 +1,70 @@
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
|
||||
\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fnil\fcharset0 Verdana;\f2\fnil\fcharset128 HiraKakuProN-W3;
|
||||
}
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf470
|
||||
{\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\partightenfactor0
|
||||
|
||||
\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\partightenfactor0
|
||||
\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.\
|
||||
\
|
||||
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:\
|
||||
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720\partightenfactor0
|
||||
|
||||
\b \cf0 First Use for Mac OS X 10.9 and later\
|
||||
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720\partightenfactor0
|
||||
|
||||
\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\partightenfactor0
|
||||
|
||||
\b \cf0 \
|
||||
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720\qj\partightenfactor0
|
||||
|
||||
\b0 \cf0 \
|
||||
\
|
||||
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\
|
||||
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, 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:\
|
||||
\
|
||||
|
||||
\b eInk Kindle (not Kindle Fire)
|
||||
\b0 :
|
||||
\b \
|
||||
|
||||
\b0 16 digit Serial Number, found in your Amazon account web pages.\
|
||||
|
||||
\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.\
|
||||
\
|
||||
Once these preferences have been set, you can drag and drop ebooks (or folders of ebooks) onto the DeDRM droplet to remove the DRM.\
|
||||
\
|
||||
This program requires Mac OS X 10.4 or above. It will not work on Mac OS X 10.3 or earlier.\
|
||||
This program uses notifications, so really needs Mac OS X 10.8 or above. It will not work on Mac OS X 10.4 or earlier. It might work on Mac OS X 10.5-10.7, but the latest Kindle for Mac does not support those System versions.\
|
||||
\
|
||||
\
|
||||
\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\partightenfactor0
|
||||
|
||||
\b \cf0 Installation
|
||||
\b0 \
|
||||
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.\
|
||||
\
|
||||
Mac OS X 10.5 and above: You do
|
||||
\i not
|
||||
\i0 need to install Python.\
|
||||
\
|
||||
Drag the DeDRM application from from the DeDRM_Application_Macintosh folder (the location of this ReadMe) to your Applications folder, or anywhere else you find convenient.\
|
||||
\
|
||||
\
|
||||
|
||||
\b Use
|
||||
\b \
|
||||
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 +72,28 @@ 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\partightenfactor0
|
||||
|
||||
\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\partightenfactor0
|
||||
{\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
|
||||
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720\partightenfactor0
|
||||
|
||||
\b \cf0 Credits\
|
||||
\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\partightenfactor0
|
||||
|
||||
\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.\
|
||||
}
|
||||
Binary file not shown.
@@ -24,37 +24,43 @@
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>droplet</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>DeDRM AppleScript 6.0.9. Written 2010–2013 by Apprentice Alf and others.</string>
|
||||
<string>DeDRM AppleScript 6.5.0 Written 2010–2016 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.9</string>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>DeDRM</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>6.0.9</string>
|
||||
<string>6.5.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>dplt</string>
|
||||
<key>LSRequiresCarbon</key>
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2010–2016 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>
|
||||
|
||||
@@ -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 plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering the necessary data to generate a new key.</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, it’s your name as set in your Barnes & Noble account, My Account page, directly under PERSONAL INFORMATION. Sometimes it is the the name used in the default shipping address, and sometimes it’s 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 that’s 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 that’s 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 don’t 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 plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing ‘.b64’ key files. Key files might come from being exported from this or older plugins, or may have been generated using the original i♥cabbages script.</p>
|
||||
<p>At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing ‘.b64’ key files. Key files might come from being exported from this or older plugins, or may have been generated using the original i♥cabbages script, or you may have made it by following the instructions above.</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>
|
||||
|
||||
@@ -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 plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering a new Kindle serial number.</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 didn’t want to enter a serial number.</p>
|
||||
|
||||
@@ -17,7 +17,7 @@ p {margin-top: 0}
|
||||
|
||||
<body>
|
||||
|
||||
<h1>DeDRM Plugin <span class="version">(v6.0.9)</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>
|
||||
|
||||
|
||||
@@ -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 plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog with two main controls.
|
||||
<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 don’t 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 plugin’s customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted key in the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.</p>
|
||||
|
||||
<h3>Renaming Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will prompt you to enter a new name for the highlighted key in the list. Enter the new name for the key and click the OK button to use the new name, or Cancel to revert to the old name.</p>
|
||||
|
||||
<h3>Exporting Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a computer’s hard-drive. Use this button to export the highlighted key to a file (with a ‘.k4a' file name extension). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.</p>
|
||||
|
||||
<h3>Importing Existing Keyfiles:</h3>
|
||||
|
||||
<p>At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import any ‘.k4a’ file you obtained by using the androidkindlekey.py script manually, or by exporting from another copy of calibre.</p>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Binary file not shown.
@@ -9,8 +9,6 @@ __docformat__ = 'restructuredtext en'
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
#
|
||||
# Requires Calibre version 0.7.55 or higher.
|
||||
#
|
||||
# All credit given to i♥cabbages and The Dark Reverser for the original standalone scripts.
|
||||
# We had the much easier job of converting them to a calibre plugin.
|
||||
#
|
||||
@@ -36,13 +34,34 @@ __docformat__ = 'restructuredtext en'
|
||||
# 6.0.7 - Error handling for incomplete PDF metadata
|
||||
# 6.0.8 - Fixes a Wine key issue and topaz support
|
||||
# 6.0.9 - Ported to work with newer versions of Calibre (moved to Qt5). Still supports older Qt4 versions.
|
||||
# 6.1.0 - Fixed multiple books import problem and PDF import with no key problem
|
||||
# 6.2.0 - Support for getting B&N key from nook Study log. Fix for UTF-8 filenames in Adobe ePubs.
|
||||
# Fix for not copying needed files. Fix for getting default Adobe key for PDFs
|
||||
# 6.2.1 - Fix for non-ascii Windows user names
|
||||
# 6.2.2 - Added URL method for B&N/nook books
|
||||
# 6.3.0 - Added in Kindle for Android serial number solution
|
||||
# 6.3.1 - Version number bump for clarity
|
||||
# 6.3.2 - Fixed Kindle for Android help file
|
||||
# 6.3.3 - Bug fix for Kindle for PC support
|
||||
# 6.3.4 - Fixes for Kindle for Android, Linux, and Kobo 3.17
|
||||
# 6.3.5 - Fixes for Linux, and Kobo 3.19 and more logging
|
||||
# 6.3.6 - Fixes for ADE ePub and PDF introduced in 6.3.5
|
||||
# 6.4.0 - Updated for new Kindle for PC encryption
|
||||
# 6.4.1 - Fix for some new tags in Topaz ebooks.
|
||||
# 6.4.2 - Fix for more new tags in Topaz ebooks and very small Topaz ebooks
|
||||
# 6.4.3 - Fix for error that only appears when not in debug mode
|
||||
# Also includes fix for Macs with bonded ethernet ports
|
||||
# 6.5.0 - Big update to Macintosh app
|
||||
# Fix for some more 'new' tags in Topaz ebooks.
|
||||
# Fix an error in wineutils.py
|
||||
|
||||
|
||||
"""
|
||||
Decrypt DRMed ebooks.
|
||||
"""
|
||||
|
||||
PLUGIN_NAME = u"DeDRM"
|
||||
PLUGIN_VERSION_TUPLE = (6, 0, 9)
|
||||
PLUGIN_VERSION_TUPLE = (6, 5, 0)
|
||||
PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE])
|
||||
# Include an html helpfile in the plugin's zipfile with the following name.
|
||||
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
|
||||
@@ -74,8 +93,12 @@ class SafeUnbuffered:
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
try:
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
except:
|
||||
# We can do nothing if a write fails
|
||||
pass
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
@@ -83,37 +106,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'])
|
||||
minimum_calibre_version = (1, 0, 0) # Compiled python libraries cannot be imported in earlier versions.
|
||||
file_types = set(['epub','pdf','pdb','prc','mobi','pobi','azw','azw1','azw3','azw4','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)
|
||||
@@ -126,9 +139,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
|
||||
@@ -166,7 +208,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()
|
||||
|
||||
@@ -177,8 +224,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
|
||||
@@ -196,13 +304,19 @@ 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()
|
||||
try:
|
||||
of.close()
|
||||
except:
|
||||
print u"{0} v{1}: Exception closing temporary file after {2:.1f} seconds. Ignored.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
|
||||
|
||||
if result == 0:
|
||||
# Decryption was successful.
|
||||
# Return the modified PersistentTemporary file to calibre.
|
||||
print u"{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime)
|
||||
return of.name
|
||||
|
||||
print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime)
|
||||
@@ -226,6 +340,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""
|
||||
|
||||
@@ -244,6 +359,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()
|
||||
@@ -255,8 +372,11 @@ 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()
|
||||
print u"{0} v{1}: Decrypted with new default key after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
|
||||
# Return the modified PersistentTemporary file to calibre.
|
||||
return of.name
|
||||
|
||||
@@ -266,12 +386,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
|
||||
@@ -289,6 +409,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()
|
||||
@@ -298,67 +420,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 = 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):
|
||||
@@ -375,10 +503,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
|
||||
@@ -397,6 +530,8 @@ class DeDRM(FileTypePlugin):
|
||||
scriptpath = os.path.join(self.alfdir,u"kindlekey.py")
|
||||
defaultkeys = WineGetKeys(scriptpath, u".k4i",dedrmprefs['kindlewineprefix'])
|
||||
except:
|
||||
print u"{0} v{1}: Exception when getting default Kindle Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
|
||||
traceback.print_exc()
|
||||
pass
|
||||
|
||||
newkeys = {}
|
||||
@@ -407,7 +542,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")
|
||||
@@ -419,8 +554,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)
|
||||
@@ -449,12 +583,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):
|
||||
@@ -467,7 +602,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':
|
||||
@@ -484,7 +619,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):
|
||||
|
||||
@@ -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)
|
||||
@@ -1,157 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#fileencoding: utf-8
|
||||
|
||||
import os
|
||||
import sys
|
||||
import zlib
|
||||
import tarfile
|
||||
from hashlib import md5
|
||||
from cStringIO import StringIO
|
||||
from binascii import a2b_hex, b2a_hex
|
||||
|
||||
STORAGE = 'AmazonSecureStorage.xml'
|
||||
|
||||
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_serials(path=None):
|
||||
''' get serials from android's shared preference xml '''
|
||||
if path is None:
|
||||
if not os.path.isfile(STORAGE):
|
||||
if os.path.isfile("backup.ab"):
|
||||
get_storage()
|
||||
else:
|
||||
return []
|
||||
path = STORAGE
|
||||
|
||||
if not os.path.isfile(path):
|
||||
return []
|
||||
|
||||
storage = parse_preference(path)
|
||||
salt = storage.get('AmazonSaltKey')
|
||||
if salt and len(salt) == 16:
|
||||
sys.stdout.write('Using AndroidObfuscationV2\n')
|
||||
obfuscation = AndroidObfuscationV2(a2b_hex(salt))
|
||||
else:
|
||||
sys.stdout.write('Using AndroidObfuscation\n')
|
||||
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 [dsnid]
|
||||
|
||||
serials = []
|
||||
for token in tokens:
|
||||
if token:
|
||||
serials.append('%s%s' % (dsnid, token))
|
||||
serials.append(dsnid)
|
||||
for token in tokens:
|
||||
if token:
|
||||
serials.append(token)
|
||||
return serials
|
||||
|
||||
def get_storage(path='backup.ab'):
|
||||
'''get AmazonSecureStorage.xml from android backup.ab
|
||||
backup.ab can be get using adb command:
|
||||
shell> adb backup com.amazon.kindle
|
||||
'''
|
||||
output = None
|
||||
read = open(path, 'rb')
|
||||
head = read.read(24)
|
||||
if head == 'ANDROID BACKUP\n1\n1\nnone\n':
|
||||
output = StringIO(zlib.decompress(read.read()))
|
||||
read.close()
|
||||
|
||||
if not output:
|
||||
return False
|
||||
|
||||
tar = tarfile.open(fileobj=output)
|
||||
for member in tar.getmembers():
|
||||
if member.name.strip().endswith(STORAGE):
|
||||
write = open(STORAGE, 'w')
|
||||
write.write(tar.extractfile(member).read())
|
||||
write.close()
|
||||
break
|
||||
|
||||
return True
|
||||
|
||||
__all__ = [ 'get_storage', 'get_serials', 'parse_preference',
|
||||
'AndroidObfuscation', 'AndroidObfuscationV2', 'STORAGE']
|
||||
|
||||
if __name__ == '__main__':
|
||||
print get_serials()
|
||||
@@ -1,6 +0,0 @@
|
||||
1.1 get AmazonSecureStorage.xml from /data/data/com.amazon.kindle/shared_prefs/AmazonSecureStorage.xml
|
||||
|
||||
1.2 on android 4.0+, run `adb backup com.amazon.kindle` from PC will get backup.ab
|
||||
now android.py can convert backup.ab to AmazonSecureStorage.xml
|
||||
|
||||
2. run `k4mobidedrm.py -a AmazonSecureStorage.xml <infile> <outdir>'
|
||||
@@ -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())
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -36,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):
|
||||
@@ -53,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']
|
||||
@@ -83,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")
|
||||
@@ -104,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)
|
||||
@@ -115,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:
|
||||
@@ -162,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'])
|
||||
@@ -188,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))
|
||||
@@ -219,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)
|
||||
|
||||
@@ -377,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):
|
||||
@@ -395,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:
|
||||
@@ -429,6 +445,10 @@ class ManageKeysDialog(QDialog):
|
||||
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])
|
||||
|
||||
@@ -515,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)
|
||||
@@ -536,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)
|
||||
@@ -550,6 +566,19 @@ class AddBandNKeyDialog(QDialog):
|
||||
data_group_box_layout.addWidget(ccn_disclaimer_label)
|
||||
layout.addSpacing(10)
|
||||
|
||||
key_group = QHBoxLayout()
|
||||
data_group_box_layout.addLayout(key_group)
|
||||
key_group.addWidget(QLabel(u"Retrieved key:", self))
|
||||
self.key_display = QLabel(u"", self)
|
||||
self.key_display.setToolTip(_(u"Click the Retrieve Key button to fetch your B&N encryption key from the B&N servers"))
|
||||
key_group.addWidget(self.key_display)
|
||||
self.retrieve_button = QtGui.QPushButton(self)
|
||||
self.retrieve_button.setToolTip(_(u"Click to retrieve your B&N encryption key from the B&N servers"))
|
||||
self.retrieve_button.setText(u"Retrieve Key")
|
||||
self.retrieve_button.clicked.connect(self.retrieve_key)
|
||||
key_group.addWidget(self.retrieve_button)
|
||||
layout.addSpacing(10)
|
||||
|
||||
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
self.button_box.accepted.connect(self.accept)
|
||||
self.button_box.rejected.connect(self.reject)
|
||||
@@ -563,8 +592,7 @@ class AddBandNKeyDialog(QDialog):
|
||||
|
||||
@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)
|
||||
return unicode(self.key_display.text()).strip()
|
||||
|
||||
@property
|
||||
def user_name(self):
|
||||
@@ -572,19 +600,28 @@ class AddBandNKeyDialog(QDialog):
|
||||
|
||||
@property
|
||||
def cc_number(self):
|
||||
return unicode(self.cc_ledit.text()).strip().replace(' ', '').replace('-','')
|
||||
return unicode(self.cc_ledit.text()).strip()
|
||||
|
||||
def retrieve_key(self):
|
||||
from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key
|
||||
fetched_key = fetch_bandn_key(self.user_name,self.cc_number)
|
||||
if fetched_key == "":
|
||||
errmsg = u"Could not retrieve key. Check username, password and intenet connectivity and try again."
|
||||
error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
else:
|
||||
self.key_display.setText(fetched_key)
|
||||
|
||||
def accept(self):
|
||||
if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace():
|
||||
errmsg = u"All fields are required!"
|
||||
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
if 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)
|
||||
if len(self.key_value) == 0:
|
||||
self.retrieve_key()
|
||||
if len(self.key_value) == 0:
|
||||
return
|
||||
QDialog.accept(self)
|
||||
|
||||
class AddEReaderDialog(QDialog):
|
||||
@@ -606,9 +643,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)
|
||||
@@ -707,9 +741,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)
|
||||
@@ -780,15 +812,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)
|
||||
@@ -834,9 +865,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)
|
||||
@@ -863,6 +891,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)
|
||||
@@ -882,9 +993,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)
|
||||
|
||||
@@ -164,6 +164,7 @@ class PageParser(object):
|
||||
self.id = os.path.basename(filename).replace('.dat','')
|
||||
self.dict = dict
|
||||
self.debug = debug
|
||||
self.first_unknown = True
|
||||
self.flat_xml = flat_xml
|
||||
self.tagpath = []
|
||||
self.doc = []
|
||||
@@ -262,6 +263,9 @@ class PageParser(object):
|
||||
'img.w' : (1, 'scalar_number', 0, 0),
|
||||
'img.src' : (1, 'scalar_number', 0, 0),
|
||||
'img.color_src' : (1, 'scalar_number', 0, 0),
|
||||
'img.gridSize' : (1, 'scalar_number', 0, 0),
|
||||
'img.gridBottomCenter' : (1, 'scalar_number', 0, 0),
|
||||
'img.gridTopCenter' : (1, 'scalar_number', 0, 0),
|
||||
'img.gridBeginCenter' : (1, 'scalar_number', 0, 0),
|
||||
'img.gridEndCenter' : (1, 'scalar_number', 0, 0),
|
||||
'img.image_type' : (1, 'scalar_number', 0, 0),
|
||||
@@ -313,11 +317,16 @@ class PageParser(object):
|
||||
'span.gridBeginCenter' : (1, 'scalar_number', 0, 0),
|
||||
'span.gridEndCenter' : (1, 'scalar_number', 0, 0),
|
||||
|
||||
'extratokens' : (1, 'snippets', 1, 0),
|
||||
'extratokens.class' : (1, 'scalar_text', 0, 0),
|
||||
'extratokens.type' : (1, 'scalar_text', 0, 0),
|
||||
'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0),
|
||||
'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0),
|
||||
'extratokens' : (1, 'snippets', 1, 0),
|
||||
'extratokens.class' : (1, 'scalar_text', 0, 0),
|
||||
'extratokens.type' : (1, 'scalar_text', 0, 0),
|
||||
'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0),
|
||||
'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0),
|
||||
'extratokens.gridSize' : (1, 'scalar_number', 0, 0),
|
||||
'extratokens.gridBottomCenter' : (1, 'scalar_number', 0, 0),
|
||||
'extratokens.gridTopCenter' : (1, 'scalar_number', 0, 0),
|
||||
'extratokens.gridBeginCenter' : (1, 'scalar_number', 0, 0),
|
||||
'extratokens.gridEndCenter' : (1, 'scalar_number', 0, 0),
|
||||
|
||||
'glyph.h' : (1, 'number', 0, 0),
|
||||
'glyph.w' : (1, 'number', 0, 0),
|
||||
@@ -382,6 +391,18 @@ class PageParser(object):
|
||||
'startID' : (0, 'number', 1, 1),
|
||||
'startID.page' : (1, 'number', 0, 0),
|
||||
'startID.id' : (1, 'number', 0, 0),
|
||||
|
||||
'median_d' : (1, 'number', 0, 0),
|
||||
'median_h' : (1, 'number', 0, 0),
|
||||
'median_firsty' : (1, 'number', 0, 0),
|
||||
'median_lasty' : (1, 'number', 0, 0),
|
||||
|
||||
'num_footers_maybe' : (1, 'number', 0, 0),
|
||||
'num_footers_yes' : (1, 'number', 0, 0),
|
||||
'num_headers_maybe' : (1, 'number', 0, 0),
|
||||
'num_headers_yes' : (1, 'number', 0, 0),
|
||||
|
||||
'tracking' : (1, 'number', 0, 0),
|
||||
|
||||
}
|
||||
|
||||
@@ -506,8 +527,9 @@ class PageParser(object):
|
||||
# or an out of sync condition
|
||||
else:
|
||||
result = []
|
||||
if (self.debug):
|
||||
if (self.debug or self.first_unknown):
|
||||
print 'Unknown Token:', token
|
||||
self.first_unknown = False
|
||||
self.tag_pop()
|
||||
return result
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf1348\cocoasubrtf170
|
||||
{\fonttbl}
|
||||
{\colortbl;\red255\green255\blue255;}
|
||||
}
|
||||
@@ -385,7 +385,7 @@ def generateBook(bookDir, raw, fixedimage):
|
||||
# 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)
|
||||
pname = 'page%04d.dat' % (pnum - 1)
|
||||
fname = os.path.join(pageDir,pname)
|
||||
flat_xml = convert2xml.fromData(dict, fname)
|
||||
|
||||
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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()
|
||||
|
||||
@@ -3,18 +3,19 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ineptepub.pyw, version 6.1
|
||||
# ineptepub.pyw, version 6.5
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2013 by some_updates, DiapDealer and Apprentice Alf
|
||||
# Modified 2015–2016 by Apprentice Harper
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# Windows users: Before running this program, you must first install Python 2.7
|
||||
# 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
|
||||
# install the version for Python 2.7). Save this script file as
|
||||
# ineptepub.pyw and double-click on it to run it.
|
||||
#
|
||||
# Mac OS X users: Save this script file as ineptepub.pyw. You can run this
|
||||
@@ -37,13 +38,17 @@ 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
|
||||
# 6.3 - Add additional check on DER file sanity
|
||||
# 6.4 - Remove erroneous check on DER file sanity
|
||||
# 6.5 - Completely remove erroneous check on DER file sanity
|
||||
|
||||
"""
|
||||
Decrypt Adobe Digital Editions encrypted ePub books.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "6.1"
|
||||
__version__ = "6.5"
|
||||
|
||||
import sys
|
||||
import os
|
||||
@@ -366,7 +371,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)
|
||||
|
||||
@@ -3,18 +3,19 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ineptpdf.pyw, version 7.11
|
||||
# ineptpdf.pyw, version 8.0.4
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
# Modified 2015-2016 by Apprentice Harper
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# Windows users: Before running this program, you must first install Python 2.7
|
||||
# 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
|
||||
# install the version for Python 2.7). Save this script file as
|
||||
# ineptpdf.pyw and double-click on it to run it.
|
||||
#
|
||||
# Mac OS X users: Save this script file as ineptpdf.pyw. You can run this
|
||||
@@ -53,13 +54,17 @@ from __future__ import with_statement
|
||||
# 7.14 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 8.0 - Work if TkInter is missing
|
||||
# 8.0.1 - Broken Metadata fix.
|
||||
# 8.0.2 - Add additional check on DER file sanity
|
||||
# 8.0.3 - Remove erroneous check on DER file sanity
|
||||
# 8.0.4 - Completely remove erroneous check on DER file sanity
|
||||
|
||||
|
||||
"""
|
||||
Decrypts Adobe ADEPT-encrypted PDF files.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "8.0.1"
|
||||
__version__ = "8.0.4"
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
@@ -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,12 +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 android
|
||||
from calibre_plugins.dedrm import androidkindlekey
|
||||
else:
|
||||
import mobidedrm
|
||||
import topazextract
|
||||
import kgenpids
|
||||
import android
|
||||
import androidkindlekey
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
@@ -187,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.")
|
||||
@@ -207,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:
|
||||
@@ -223,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:
|
||||
@@ -239,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()
|
||||
@@ -254,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())
|
||||
@@ -275,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>] [ -a <AmazonSecureStorage.xml> ] <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
|
||||
@@ -298,6 +304,7 @@ def cli_main():
|
||||
infile = args[0]
|
||||
outdir = args[1]
|
||||
kDatabaseFiles = []
|
||||
androidFiles = []
|
||||
serials = []
|
||||
pids = []
|
||||
|
||||
@@ -316,14 +323,13 @@ def cli_main():
|
||||
serials = a.split(',')
|
||||
if o == '-a':
|
||||
if a == None:
|
||||
continue
|
||||
serials.extend(android.get_serials(a))
|
||||
serials.extend(android.get_serials())
|
||||
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__':
|
||||
|
||||
@@ -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,21 +197,26 @@ def getK4Pids(rec209, token, kindleDatabase):
|
||||
|
||||
try:
|
||||
# Get the Mazama Random number
|
||||
MazamaRandomNumber = (kindleDatabase[1])['MazamaRandomNumber'].decode('hex').encode('ascii')
|
||||
|
||||
# Get the kindle account token
|
||||
kindleAccountToken = (kindleDatabase[1])['kindle.account.tokens'].decode('hex').encode('ascii')
|
||||
MazamaRandomNumber = (kindleDatabase[1])['MazamaRandomNumber'].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])
|
||||
return pids
|
||||
|
||||
try:
|
||||
# Get the kindle account token, if present
|
||||
kindleAccountToken = (kindleDatabase[1])['kindle.account.tokens'].decode('hex')
|
||||
|
||||
except KeyError:
|
||||
kindleAccountToken=""
|
||||
pass
|
||||
|
||||
# Get the ID string used
|
||||
encodedIDString = encodeHash(IDString,charMap1)
|
||||
|
||||
|
||||
@@ -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-2016 by some_updates, Apprentice Alf and Apprentice Harper
|
||||
|
||||
# Revision history:
|
||||
# 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc.
|
||||
@@ -20,6 +18,10 @@ from __future__ import with_statement
|
||||
# 1.7 - Work if TkInter is missing
|
||||
# 1.8 - Fixes for Kindle for Mac, and non-ascii in Windows user names
|
||||
# 1.9 - Fixes for Unicode in Windows user names
|
||||
# 2.0 - Added comments and extra fix for non-ascii Windows user names
|
||||
# 2.1 - Fixed Kindle for PC encryption changes March 2016
|
||||
# 2.2 - Fixes for Macs with bonded ethernet ports
|
||||
# Also removed old .kinfo file support (pre-2011)
|
||||
|
||||
|
||||
"""
|
||||
@@ -27,7 +29,7 @@ Retrieve Kindle for PC/Mac user key.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '1.9'
|
||||
__version__ = '2.2'
|
||||
|
||||
import sys, os, re
|
||||
from struct import pack, unpack, unpack_from
|
||||
@@ -885,6 +887,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()
|
||||
@@ -926,7 +929,7 @@ if iswindows:
|
||||
# or the python interface to the 32 vs 64 bit registry is broken
|
||||
path = ""
|
||||
if 'LOCALAPPDATA' in os.environ.keys():
|
||||
# Python 2.x does not return unicode env. Use Python 3.x
|
||||
# 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')
|
||||
@@ -993,179 +996,120 @@ if iswindows:
|
||||
# determine type of kindle info provided and return a
|
||||
# database of keynames and values
|
||||
def getDBfromFile(kInfoFile):
|
||||
names = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber', 'max_date', 'SIGVERIF']
|
||||
names = [\
|
||||
'kindle.account.tokens',\
|
||||
'kindle.cookie.item',\
|
||||
'eulaVersionAccepted',\
|
||||
'login_date',\
|
||||
'kindle.token.item',\
|
||||
'login',\
|
||||
'kindle.key.item',\
|
||||
'kindle.name.info',\
|
||||
'kindle.device.info',\
|
||||
'MazamaRandomNumber',\
|
||||
'max_date',\
|
||||
'SIGVERIF',\
|
||||
'build_version',\
|
||||
]
|
||||
|
||||
DB = {}
|
||||
with open(kInfoFile, 'rb') as infoReader:
|
||||
hdr = infoReader.read(1)
|
||||
data = infoReader.read()
|
||||
# assume newest .kinf2011 style .kinf file
|
||||
# the .kinf file uses "/" to separate it into records
|
||||
# so remove the trailing "/" to make it easy to use split
|
||||
data = data[:-1]
|
||||
items = data.split('/')
|
||||
|
||||
if data.find('{') != -1 :
|
||||
# older style kindle-info file
|
||||
items = data.split('{')
|
||||
for item in items:
|
||||
if item != '':
|
||||
keyhash, rawdata = item.split(':')
|
||||
keyname = "unknown"
|
||||
for name in names:
|
||||
if encodeHash(name,charMap2) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
keyname = keyhash
|
||||
encryptedValue = decode(rawdata,charMap2)
|
||||
DB[keyname] = CryptUnprotectData(encryptedValue, "", 0)
|
||||
elif hdr == '/':
|
||||
# else rainier-2-1-1 .kinf file
|
||||
# the .kinf file uses "/" to separate it into records
|
||||
# so remove the trailing "/" to make it easy to use split
|
||||
data = data[:-1]
|
||||
items = data.split('/')
|
||||
# starts with an encoded and encrypted header blob
|
||||
headerblob = items.pop(0)
|
||||
encryptedValue = decode(headerblob, testMap1)
|
||||
cleartext = UnprotectHeaderData(encryptedValue)
|
||||
#print "header cleartext:",cleartext
|
||||
# now extract the pieces that form the added entropy
|
||||
pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
|
||||
for m in re.finditer(pattern, cleartext):
|
||||
added_entropy = m.group(2) + m.group(4)
|
||||
|
||||
# loop through the item records until all are processed
|
||||
while len(items) > 0:
|
||||
|
||||
# get the first item record
|
||||
# loop through the item records until all are processed
|
||||
while len(items) > 0:
|
||||
|
||||
# get the first item record
|
||||
item = items.pop(0)
|
||||
|
||||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
|
||||
# the sha1 of raw keyhash string is used to create entropy along
|
||||
# with the added entropy provided above from the headerblob
|
||||
entropy = SHA1(keyhash) + added_entropy
|
||||
|
||||
# the remainder of the first record when decoded with charMap5
|
||||
# has the ':' split char followed by the string representation
|
||||
# of the number of records that follow
|
||||
# and make up the contents
|
||||
srcnt = decode(item[34:],charMap5)
|
||||
rcnt = int(srcnt)
|
||||
|
||||
# read and store in rcnt records of data
|
||||
# that make up the contents value
|
||||
edlst = []
|
||||
for i in xrange(rcnt):
|
||||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
# key names now use the new testMap8 encoding
|
||||
keyname = "unknown"
|
||||
for name in names:
|
||||
if encodeHash(name,testMap8) == keyhash:
|
||||
keyname = name
|
||||
#print "keyname found from hash:",keyname
|
||||
break
|
||||
if keyname == "unknown":
|
||||
keyname = keyhash
|
||||
#print "keyname not found, hash is:",keyname
|
||||
|
||||
# the raw keyhash string is used to create entropy for the actual
|
||||
# CryptProtectData Blob that represents that keys contents
|
||||
entropy = SHA1(keyhash)
|
||||
# the testMap8 encoded contents data has had a length
|
||||
# of chars (always odd) cut off of the front and moved
|
||||
# to the end to prevent decoding using testMap8 from
|
||||
# working properly, and thereby preventing the ensuing
|
||||
# CryptUnprotectData call from succeeding.
|
||||
|
||||
# the remainder of the first record when decoded with charMap5
|
||||
# has the ':' split char followed by the string representation
|
||||
# of the number of records that follow
|
||||
# and make up the contents
|
||||
srcnt = decode(item[34:],charMap5)
|
||||
rcnt = int(srcnt)
|
||||
# The offset into the testMap8 encoded contents seems to be:
|
||||
# len(contents)-largest prime number <= int(len(content)/3)
|
||||
# (in other words split "about" 2/3rds of the way through)
|
||||
|
||||
# read and store in rcnt records of data
|
||||
# that make up the contents value
|
||||
edlst = []
|
||||
for i in xrange(rcnt):
|
||||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
keyname = "unknown"
|
||||
for name in names:
|
||||
if encodeHash(name,charMap5) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
keyname = keyhash
|
||||
# the charMap5 encoded contents data has had a length
|
||||
# of chars (always odd) cut off of the front and moved
|
||||
# to the end to prevent decoding using charMap5 from
|
||||
# working properly, and thereby preventing the ensuing
|
||||
# CryptUnprotectData call from succeeding.
|
||||
|
||||
# The offset into the charMap5 encoded contents seems to be:
|
||||
# len(contents)-largest prime number <= int(len(content)/3)
|
||||
# (in other words split "about" 2/3rds of the way through)
|
||||
|
||||
# move first offsets chars to end to align for decode by charMap5
|
||||
encdata = "".join(edlst)
|
||||
contlen = len(encdata)
|
||||
noffset = contlen - primes(int(contlen/3))[-1]
|
||||
|
||||
# now properly split and recombine
|
||||
# by moving noffset chars from the start of the
|
||||
# string to the end of the string
|
||||
pfx = encdata[0:noffset]
|
||||
encdata = encdata[noffset:]
|
||||
encdata = encdata + pfx
|
||||
|
||||
# decode using Map5 to get the CryptProtect Data
|
||||
encryptedValue = decode(encdata,charMap5)
|
||||
DB[keyname] = CryptUnprotectData(encryptedValue, entropy, 1)
|
||||
else:
|
||||
# else newest .kinf2011 style .kinf file
|
||||
# the .kinf file uses "/" to separate it into records
|
||||
# so remove the trailing "/" to make it easy to use split
|
||||
# need to put back the first char read because it it part
|
||||
# of the added entropy blob
|
||||
data = hdr + data[:-1]
|
||||
items = data.split('/')
|
||||
|
||||
# starts with and encoded and encrypted header blob
|
||||
headerblob = items.pop(0)
|
||||
encryptedValue = decode(headerblob, testMap1)
|
||||
cleartext = UnprotectHeaderData(encryptedValue)
|
||||
# now extract the pieces that form the added entropy
|
||||
pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
|
||||
for m in re.finditer(pattern, cleartext):
|
||||
added_entropy = m.group(2) + m.group(4)
|
||||
# move first offsets chars to end to align for decode by testMap8
|
||||
# by moving noffset chars from the start of the
|
||||
# string to the end of the string
|
||||
encdata = "".join(edlst)
|
||||
#print "encrypted data:",encdata
|
||||
contlen = len(encdata)
|
||||
noffset = contlen - primes(int(contlen/3))[-1]
|
||||
pfx = encdata[0:noffset]
|
||||
encdata = encdata[noffset:]
|
||||
encdata = encdata + pfx
|
||||
#print "rearranged data:",encdata
|
||||
|
||||
|
||||
# loop through the item records until all are processed
|
||||
while len(items) > 0:
|
||||
|
||||
# get the first item record
|
||||
item = items.pop(0)
|
||||
|
||||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
|
||||
# the sha1 of raw keyhash string is used to create entropy along
|
||||
# with the added entropy provided above from the headerblob
|
||||
entropy = SHA1(keyhash) + added_entropy
|
||||
|
||||
# the remainder of the first record when decoded with charMap5
|
||||
# has the ':' split char followed by the string representation
|
||||
# of the number of records that follow
|
||||
# and make up the contents
|
||||
srcnt = decode(item[34:],charMap5)
|
||||
rcnt = int(srcnt)
|
||||
|
||||
# read and store in rcnt records of data
|
||||
# that make up the contents value
|
||||
edlst = []
|
||||
for i in xrange(rcnt):
|
||||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
# key names now use the new testMap8 encoding
|
||||
keyname = "unknown"
|
||||
for name in names:
|
||||
if encodeHash(name,testMap8) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
|
||||
# the testMap8 encoded contents data has had a length
|
||||
# of chars (always odd) cut off of the front and moved
|
||||
# to the end to prevent decoding using testMap8 from
|
||||
# working properly, and thereby preventing the ensuing
|
||||
# CryptUnprotectData call from succeeding.
|
||||
|
||||
# The offset into the testMap8 encoded contents seems to be:
|
||||
# len(contents)-largest prime number <= int(len(content)/3)
|
||||
# (in other words split "about" 2/3rds of the way through)
|
||||
|
||||
# move first offsets chars to end to align for decode by testMap8
|
||||
# by moving noffset chars from the start of the
|
||||
# string to the end of the string
|
||||
encdata = "".join(edlst)
|
||||
contlen = len(encdata)
|
||||
noffset = contlen - primes(int(contlen/3))[-1]
|
||||
pfx = encdata[0:noffset]
|
||||
encdata = encdata[noffset:]
|
||||
encdata = encdata + pfx
|
||||
|
||||
# decode using new testMap8 to get the original CryptProtect Data
|
||||
encryptedValue = decode(encdata,testMap8)
|
||||
cleartext = CryptUnprotectData(encryptedValue, entropy, 1)
|
||||
# decode using new testMap8 to get the original CryptProtect Data
|
||||
encryptedValue = decode(encdata,testMap8)
|
||||
#print "decoded data:",encryptedValue.encode('hex')
|
||||
cleartext = CryptUnprotectData(encryptedValue, entropy, 1)
|
||||
if len(cleartext)>0:
|
||||
#print "cleartext data:",cleartext,":end data"
|
||||
DB[keyname] = cleartext
|
||||
#print keyname, cleartext
|
||||
|
||||
if '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 len(DB)>6:
|
||||
# store values used in decryption
|
||||
DB['IDString'] = GetIDString()
|
||||
DB['UserName'] = GetUserName()
|
||||
print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(GetIDString(), GetUserName().encode('hex'))
|
||||
else:
|
||||
print u"Couldn't decrypt file."
|
||||
DB = {}
|
||||
return DB
|
||||
elif isosx:
|
||||
@@ -1297,11 +1241,9 @@ elif isosx:
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
out1, out2 = p.communicate()
|
||||
#print out1
|
||||
reslst = out1.split('\n')
|
||||
cnt = len(reslst)
|
||||
bsdname = None
|
||||
sernum = None
|
||||
foundIt = False
|
||||
for j in xrange(cnt):
|
||||
resline = reslst[j]
|
||||
pp = resline.find('\"Serial Number\" = \"')
|
||||
@@ -1310,31 +1252,24 @@ elif isosx:
|
||||
sernums.append(sernum.strip())
|
||||
return sernums
|
||||
|
||||
def GetUserHomeAppSupKindleDirParitionName():
|
||||
home = os.getenv('HOME')
|
||||
dpath = home + '/Library'
|
||||
def GetDiskPartitionNames():
|
||||
names = []
|
||||
cmdline = '/sbin/mount'
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
out1, out2 = p.communicate()
|
||||
reslst = out1.split('\n')
|
||||
cnt = len(reslst)
|
||||
disk = ''
|
||||
foundIt = False
|
||||
for j in xrange(cnt):
|
||||
resline = reslst[j]
|
||||
if resline.startswith('/dev'):
|
||||
(devpart, mpath) = resline.split(' on ')
|
||||
dpart = devpart[5:]
|
||||
pp = mpath.find('(')
|
||||
if pp >= 0:
|
||||
mpath = mpath[:pp-1]
|
||||
if dpath.startswith(mpath):
|
||||
disk = dpart
|
||||
return disk
|
||||
names.append(dpart)
|
||||
return names
|
||||
|
||||
# uses a sub process to get the UUID of the specified disk partition using ioreg
|
||||
def GetDiskPartitionUUIDs(diskpart):
|
||||
# uses a sub process to get the UUID of all disk partitions
|
||||
def GetDiskPartitionUUIDs():
|
||||
uuids = []
|
||||
uuidnum = os.getenv('MYUUIDNUMBER')
|
||||
if uuidnum != None:
|
||||
@@ -1343,46 +1278,16 @@ elif isosx:
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
out1, out2 = p.communicate()
|
||||
#print out1
|
||||
reslst = out1.split('\n')
|
||||
cnt = len(reslst)
|
||||
bsdname = None
|
||||
uuidnum = None
|
||||
foundIt = False
|
||||
nest = 0
|
||||
uuidnest = -1
|
||||
partnest = -2
|
||||
for j in xrange(cnt):
|
||||
resline = reslst[j]
|
||||
if resline.find('{') >= 0:
|
||||
nest += 1
|
||||
if resline.find('}') >= 0:
|
||||
nest -= 1
|
||||
pp = resline.find('\"UUID\" = \"')
|
||||
if pp >= 0:
|
||||
uuidnum = resline[pp+10:-1]
|
||||
uuidnum = uuidnum.strip()
|
||||
uuidnest = nest
|
||||
if partnest == uuidnest and uuidnest > 0:
|
||||
foundIt = True
|
||||
break
|
||||
bb = resline.find('\"BSD Name\" = \"')
|
||||
if bb >= 0:
|
||||
bsdname = resline[bb+14:-1]
|
||||
bsdname = bsdname.strip()
|
||||
if (bsdname == diskpart):
|
||||
partnest = nest
|
||||
else :
|
||||
partnest = -2
|
||||
if partnest == uuidnest and partnest > 0:
|
||||
foundIt = True
|
||||
break
|
||||
if nest == 0:
|
||||
partnest = -2
|
||||
uuidnest = -1
|
||||
uuidnum = None
|
||||
bsdname = None
|
||||
if foundIt:
|
||||
uuids.append(uuidnum)
|
||||
uuids.append(uuidnum)
|
||||
return uuids
|
||||
|
||||
def GetMACAddressesMunged():
|
||||
@@ -1390,28 +1295,26 @@ elif isosx:
|
||||
macnum = os.getenv('MYMACNUM')
|
||||
if macnum != None:
|
||||
macnums.append(macnum)
|
||||
cmdline = '/sbin/ifconfig en0'
|
||||
cmdline = 'networksetup -listallhardwareports' # en0'
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
out1, out2 = p.communicate()
|
||||
reslst = out1.split('\n')
|
||||
cnt = len(reslst)
|
||||
macnum = None
|
||||
foundIt = False
|
||||
for j in xrange(cnt):
|
||||
resline = reslst[j]
|
||||
pp = resline.find('ether ')
|
||||
pp = resline.find('Ethernet Address: ')
|
||||
if pp >= 0:
|
||||
macnum = resline[pp+6:-1]
|
||||
#print resline
|
||||
macnum = resline[pp+18:]
|
||||
macnum = macnum.strip()
|
||||
# print 'original mac', macnum
|
||||
# now munge it up the way Kindle app does
|
||||
# by xoring it with 0xa5 and swapping elements 3 and 4
|
||||
maclst = macnum.split(':')
|
||||
n = len(maclst)
|
||||
if n != 6:
|
||||
fountIt = False
|
||||
break
|
||||
continue
|
||||
#print 'original mac', macnum
|
||||
# now munge it up the way Kindle app does
|
||||
# by xoring it with 0xa5 and swapping elements 3 and 4
|
||||
for i in range(6):
|
||||
maclst[i] = int('0x' + maclst[i], 0)
|
||||
mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
|
||||
@@ -1422,16 +1325,15 @@ elif isosx:
|
||||
mlst[1] = maclst[1] ^ 0xa5
|
||||
mlst[0] = maclst[0] ^ 0xa5
|
||||
macnum = '%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x' % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5])
|
||||
foundIt = True
|
||||
break
|
||||
if foundIt:
|
||||
macnums.append(macnum)
|
||||
#print 'munged mac', macnum
|
||||
macnums.append(macnum)
|
||||
return macnums
|
||||
|
||||
|
||||
# uses unix env to get username instead of using sysctlbyname
|
||||
def GetUserName():
|
||||
username = os.getenv('USER')
|
||||
#print "Username:",username
|
||||
return username
|
||||
|
||||
def GetIDStrings():
|
||||
@@ -1439,58 +1341,13 @@ elif isosx:
|
||||
strings = []
|
||||
strings.extend(GetMACAddressesMunged())
|
||||
strings.extend(GetVolumesSerialNumbers())
|
||||
diskpart = GetUserHomeAppSupKindleDirParitionName()
|
||||
strings.extend(GetDiskPartitionUUIDs(diskpart))
|
||||
strings.extend(GetDiskPartitionNames())
|
||||
strings.extend(GetDiskPartitionUUIDs())
|
||||
strings.append('9999999999')
|
||||
#print strings
|
||||
#print "ID Strings:\n",strings
|
||||
return strings
|
||||
|
||||
|
||||
# implements an Pseudo Mac Version of Windows built-in Crypto routine
|
||||
# used by Kindle for Mac versions < 1.6.0
|
||||
class CryptUnprotectData(object):
|
||||
def __init__(self, IDString):
|
||||
sp = IDString + '!@#' + GetUserName()
|
||||
passwdData = encode(SHA256(sp),charMap1)
|
||||
salt = '16743'
|
||||
self.crp = LibCrypto()
|
||||
iter = 0x3e8
|
||||
keylen = 0x80
|
||||
key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen)
|
||||
self.key = key_iv[0:32]
|
||||
self.iv = key_iv[32:48]
|
||||
self.crp.set_decrypt_key(self.key, self.iv)
|
||||
|
||||
def decrypt(self, encryptedData):
|
||||
cleartext = self.crp.decrypt(encryptedData)
|
||||
cleartext = decode(cleartext,charMap1)
|
||||
return cleartext
|
||||
|
||||
|
||||
# implements an Pseudo Mac Version of Windows built-in Crypto routine
|
||||
# used for Kindle for Mac Versions >= 1.6.0
|
||||
class CryptUnprotectDataV2(object):
|
||||
def __init__(self, IDString):
|
||||
sp = GetUserName() + ':&%:' + IDString
|
||||
passwdData = encode(SHA256(sp),charMap5)
|
||||
# salt generation as per the code
|
||||
salt = 0x0512981d * 2 * 1 * 1
|
||||
salt = str(salt) + GetUserName()
|
||||
salt = encode(salt,charMap5)
|
||||
self.crp = LibCrypto()
|
||||
iter = 0x800
|
||||
keylen = 0x400
|
||||
key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen)
|
||||
self.key = key_iv[0:32]
|
||||
self.iv = key_iv[32:48]
|
||||
self.crp.set_decrypt_key(self.key, self.iv)
|
||||
|
||||
def decrypt(self, encryptedData):
|
||||
cleartext = self.crp.decrypt(encryptedData)
|
||||
cleartext = decode(cleartext, charMap5)
|
||||
return cleartext
|
||||
|
||||
|
||||
# unprotect the new header blob in .kinf2011
|
||||
# used in Kindle for Mac Version >= 1.9.0
|
||||
def UnprotectHeaderData(encryptedData):
|
||||
@@ -1508,8 +1365,7 @@ elif isosx:
|
||||
|
||||
|
||||
# implements an Pseudo Mac Version of Windows built-in Crypto routine
|
||||
# used for Kindle for Mac Versions >= 1.9.0
|
||||
class CryptUnprotectDataV3(object):
|
||||
class CryptUnprotectData(object):
|
||||
def __init__(self, entropy, IDString):
|
||||
sp = GetUserName() + '+@#$%+' + IDString
|
||||
passwdData = encode(SHA256(sp),charMap2)
|
||||
@@ -1577,206 +1433,118 @@ elif isosx:
|
||||
# determine type of kindle info provided and return a
|
||||
# database of keynames and values
|
||||
def getDBfromFile(kInfoFile):
|
||||
names = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber', 'max_date', 'SIGVERIF']
|
||||
names = [\
|
||||
'kindle.account.tokens',\
|
||||
'kindle.cookie.item',\
|
||||
'eulaVersionAccepted',\
|
||||
'login_date',\
|
||||
'kindle.token.item',\
|
||||
'login',\
|
||||
'kindle.key.item',\
|
||||
'kindle.name.info',\
|
||||
'kindle.device.info',\
|
||||
'MazamaRandomNumber',\
|
||||
'max_date',\
|
||||
'SIGVERIF',\
|
||||
'build_version',\
|
||||
]
|
||||
with open(kInfoFile, 'rb') as infoReader:
|
||||
filehdr = infoReader.read(1)
|
||||
filedata = infoReader.read()
|
||||
|
||||
data = filedata[:-1]
|
||||
items = data.split('/')
|
||||
IDStrings = GetIDStrings()
|
||||
for IDString in IDStrings:
|
||||
DB = {}
|
||||
#print "trying IDString:",IDString
|
||||
try:
|
||||
hdr = filehdr
|
||||
data = filedata
|
||||
if data.find('[') != -1 :
|
||||
# older style kindle-info file
|
||||
cud = CryptUnprotectData(IDString)
|
||||
items = data.split('[')
|
||||
for item in items:
|
||||
if item != '':
|
||||
keyhash, rawdata = item.split(':')
|
||||
keyname = 'unknown'
|
||||
for name in names:
|
||||
if encodeHash(name,charMap2) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == 'unknown':
|
||||
keyname = keyhash
|
||||
encryptedValue = decode(rawdata,charMap2)
|
||||
cleartext = cud.decrypt(encryptedValue)
|
||||
if len(cleartext) > 0:
|
||||
DB[keyname] = cleartext
|
||||
if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB:
|
||||
break
|
||||
elif hdr == '/':
|
||||
# else newer style .kinf file used by K4Mac >= 1.6.0
|
||||
# the .kinf file uses '/' to separate it into records
|
||||
# so remove the trailing '/' to make it easy to use split
|
||||
data = data[:-1]
|
||||
items = data.split('/')
|
||||
cud = CryptUnprotectDataV2(IDString)
|
||||
DB = {}
|
||||
items = data.split('/')
|
||||
|
||||
# the headerblob is the encrypted information needed to build the entropy string
|
||||
headerblob = items.pop(0)
|
||||
encryptedValue = decode(headerblob, charMap1)
|
||||
cleartext = UnprotectHeaderData(encryptedValue)
|
||||
|
||||
# loop through the item records until all are processed
|
||||
while len(items) > 0:
|
||||
# now extract the pieces in the same way
|
||||
# this version is different from K4PC it scales the build number by multipying by 735
|
||||
pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
|
||||
for m in re.finditer(pattern, cleartext):
|
||||
entropy = str(int(m.group(2)) * 0x2df) + m.group(4)
|
||||
|
||||
# get the first item record
|
||||
cud = CryptUnprotectData(entropy,IDString)
|
||||
|
||||
# loop through the item records until all are processed
|
||||
while len(items) > 0:
|
||||
|
||||
# get the first item record
|
||||
item = items.pop(0)
|
||||
|
||||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
keyname = 'unknown'
|
||||
|
||||
# unlike K4PC the keyhash is not used in generating entropy
|
||||
# entropy = SHA1(keyhash) + added_entropy
|
||||
# entropy = added_entropy
|
||||
|
||||
# the remainder of the first record when decoded with charMap5
|
||||
# has the ':' split char followed by the string representation
|
||||
# of the number of records that follow
|
||||
# and make up the contents
|
||||
srcnt = decode(item[34:],charMap5)
|
||||
rcnt = int(srcnt)
|
||||
|
||||
# read and store in rcnt records of data
|
||||
# that make up the contents value
|
||||
edlst = []
|
||||
for i in xrange(rcnt):
|
||||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
keyname = 'unknown'
|
||||
keyname = 'unknown'
|
||||
for name in names:
|
||||
if encodeHash(name,testMap8) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == 'unknown':
|
||||
keyname = keyhash
|
||||
|
||||
# the raw keyhash string is also used to create entropy for the actual
|
||||
# CryptProtectData Blob that represents that keys contents
|
||||
# 'entropy' not used for K4Mac only K4PC
|
||||
# entropy = SHA1(keyhash)
|
||||
# the testMap8 encoded contents data has had a length
|
||||
# of chars (always odd) cut off of the front and moved
|
||||
# to the end to prevent decoding using testMap8 from
|
||||
# working properly, and thereby preventing the ensuing
|
||||
# CryptUnprotectData call from succeeding.
|
||||
|
||||
# the remainder of the first record when decoded with charMap5
|
||||
# has the ':' split char followed by the string representation
|
||||
# of the number of records that follow
|
||||
# and make up the contents
|
||||
srcnt = decode(item[34:],charMap5)
|
||||
rcnt = int(srcnt)
|
||||
# The offset into the testMap8 encoded contents seems to be:
|
||||
# len(contents) - largest prime number less than or equal to int(len(content)/3)
|
||||
# (in other words split 'about' 2/3rds of the way through)
|
||||
|
||||
# read and store in rcnt records of data
|
||||
# that make up the contents value
|
||||
edlst = []
|
||||
for i in xrange(rcnt):
|
||||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
# move first offsets chars to end to align for decode by testMap8
|
||||
encdata = ''.join(edlst)
|
||||
contlen = len(encdata)
|
||||
|
||||
keyname = 'unknown'
|
||||
for name in names:
|
||||
if encodeHash(name,charMap5) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == 'unknown':
|
||||
keyname = keyhash
|
||||
# now properly split and recombine
|
||||
# by moving noffset chars from the start of the
|
||||
# string to the end of the string
|
||||
noffset = contlen - primes(int(contlen/3))[-1]
|
||||
pfx = encdata[0:noffset]
|
||||
encdata = encdata[noffset:]
|
||||
encdata = encdata + pfx
|
||||
|
||||
# the charMap5 encoded contents data has had a length
|
||||
# of chars (always odd) cut off of the front and moved
|
||||
# to the end to prevent decoding using charMap5 from
|
||||
# working properly, and thereby preventing the ensuing
|
||||
# CryptUnprotectData call from succeeding.
|
||||
# decode using testMap8 to get the CryptProtect Data
|
||||
encryptedValue = decode(encdata,testMap8)
|
||||
cleartext = cud.decrypt(encryptedValue)
|
||||
# print keyname
|
||||
# print cleartext
|
||||
if len(cleartext) > 0:
|
||||
DB[keyname] = cleartext
|
||||
|
||||
# The offset into the charMap5 encoded contents seems to be:
|
||||
# len(contents) - largest prime number less than or equal to int(len(content)/3)
|
||||
# (in other words split 'about' 2/3rds of the way through)
|
||||
|
||||
# move first offsets chars to end to align for decode by charMap5
|
||||
encdata = ''.join(edlst)
|
||||
contlen = len(encdata)
|
||||
|
||||
# now properly split and recombine
|
||||
# by moving noffset chars from the start of the
|
||||
# string to the end of the string
|
||||
noffset = contlen - primes(int(contlen/3))[-1]
|
||||
pfx = encdata[0:noffset]
|
||||
encdata = encdata[noffset:]
|
||||
encdata = encdata + pfx
|
||||
|
||||
# decode using charMap5 to get the CryptProtect Data
|
||||
encryptedValue = decode(encdata,charMap5)
|
||||
cleartext = cud.decrypt(encryptedValue)
|
||||
if len(cleartext) > 0:
|
||||
DB[keyname] = cleartext
|
||||
|
||||
if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB:
|
||||
break
|
||||
else:
|
||||
# the latest .kinf2011 version for K4M 1.9.1
|
||||
# put back the hdr char, it is needed
|
||||
data = hdr + data
|
||||
data = data[:-1]
|
||||
items = data.split('/')
|
||||
|
||||
# the headerblob is the encrypted information needed to build the entropy string
|
||||
headerblob = items.pop(0)
|
||||
encryptedValue = decode(headerblob, charMap1)
|
||||
cleartext = UnprotectHeaderData(encryptedValue)
|
||||
|
||||
# now extract the pieces in the same way
|
||||
# this version is different from K4PC it scales the build number by multipying by 735
|
||||
pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
|
||||
for m in re.finditer(pattern, cleartext):
|
||||
entropy = str(int(m.group(2)) * 0x2df) + m.group(4)
|
||||
|
||||
cud = CryptUnprotectDataV3(entropy,IDString)
|
||||
|
||||
# loop through the item records until all are processed
|
||||
while len(items) > 0:
|
||||
|
||||
# get the first item record
|
||||
item = items.pop(0)
|
||||
|
||||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
keyname = 'unknown'
|
||||
|
||||
# unlike K4PC the keyhash is not used in generating entropy
|
||||
# entropy = SHA1(keyhash) + added_entropy
|
||||
# entropy = added_entropy
|
||||
|
||||
# the remainder of the first record when decoded with charMap5
|
||||
# has the ':' split char followed by the string representation
|
||||
# of the number of records that follow
|
||||
# and make up the contents
|
||||
srcnt = decode(item[34:],charMap5)
|
||||
rcnt = int(srcnt)
|
||||
|
||||
# read and store in rcnt records of data
|
||||
# that make up the contents value
|
||||
edlst = []
|
||||
for i in xrange(rcnt):
|
||||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
keyname = 'unknown'
|
||||
for name in names:
|
||||
if encodeHash(name,testMap8) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == 'unknown':
|
||||
keyname = keyhash
|
||||
|
||||
# the testMap8 encoded contents data has had a length
|
||||
# of chars (always odd) cut off of the front and moved
|
||||
# to the end to prevent decoding using testMap8 from
|
||||
# working properly, and thereby preventing the ensuing
|
||||
# CryptUnprotectData call from succeeding.
|
||||
|
||||
# The offset into the testMap8 encoded contents seems to be:
|
||||
# len(contents) - largest prime number less than or equal to int(len(content)/3)
|
||||
# (in other words split 'about' 2/3rds of the way through)
|
||||
|
||||
# move first offsets chars to end to align for decode by testMap8
|
||||
encdata = ''.join(edlst)
|
||||
contlen = len(encdata)
|
||||
|
||||
# now properly split and recombine
|
||||
# by moving noffset chars from the start of the
|
||||
# string to the end of the string
|
||||
noffset = contlen - primes(int(contlen/3))[-1]
|
||||
pfx = encdata[0:noffset]
|
||||
encdata = encdata[noffset:]
|
||||
encdata = encdata + pfx
|
||||
|
||||
# decode using testMap8 to get the CryptProtect Data
|
||||
encryptedValue = decode(encdata,testMap8)
|
||||
cleartext = cud.decrypt(encryptedValue)
|
||||
# print keyname
|
||||
# print cleartext
|
||||
if len(cleartext) > 0:
|
||||
DB[keyname] = cleartext
|
||||
|
||||
if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB:
|
||||
break
|
||||
if len(DB)>6:
|
||||
break
|
||||
except:
|
||||
pass
|
||||
if 'kindle.account.tokens' in DB:
|
||||
if len(DB)>6:
|
||||
# store values used in decryption
|
||||
print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString, GetUserName())
|
||||
DB['IDString'] = IDString
|
||||
@@ -1840,7 +1608,7 @@ def cli_main():
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
argv=unicode_argv()
|
||||
progname = os.path.basename(argv[0])
|
||||
print u"{0} v{1}\nCopyright © 2010-2013 some_updates and Apprentice Alf".format(progname,__version__)
|
||||
print u"{0} v{1}\nCopyright © 2010-2016 by some_updates, Apprentice Alf and Apprentice Harper".format(progname,__version__)
|
||||
|
||||
try:
|
||||
opts, args = getopt.getopt(argv[1:], "hk:")
|
||||
@@ -1870,7 +1638,7 @@ def cli_main():
|
||||
# save to the same directory as the script
|
||||
outpath = os.path.dirname(argv[0])
|
||||
|
||||
# make sure the outpath is the
|
||||
# make sure the outpath is canonical
|
||||
outpath = os.path.realpath(os.path.normpath(outpath))
|
||||
|
||||
if not getkey(outpath, files):
|
||||
|
||||
@@ -448,6 +448,8 @@ class MobiBook:
|
||||
goodpids.append(pid[0:-2])
|
||||
elif len(pid)==8:
|
||||
goodpids.append(pid)
|
||||
else:
|
||||
print u"Warning: PID {0} has wrong number of digits".format(pid)
|
||||
|
||||
if self.crypto_type == 1:
|
||||
t1_keyvec = 'QDCVEPMU675RUBSZ'
|
||||
@@ -530,7 +532,7 @@ def cli_main():
|
||||
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])
|
||||
print u"MobiDeDRM v{0} Error: {1:s}".format(__version__,e.args[0])
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
@@ -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'] == []:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -26,11 +26,12 @@ def WineGetKeys(scriptpath, extension, wineprefix=""):
|
||||
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)
|
||||
print u"{0} v{1}: Command line: '{2}'".format(PLUGIN_NAME, PLUGIN_VERSION, cmdline)
|
||||
|
||||
try:
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
@@ -38,8 +39,20 @@ def WineGetKeys(scriptpath, extension, wineprefix=""):
|
||||
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 []
|
||||
if wineprefix != "" and os.path.exists(wineprefix):
|
||||
cmdline = u"WINEPREFIX=\"{2}\" wine C:\\Python27\\python.exe \"{0}\" \"{1}\"".format(scriptpath,outdirpath,wineprefix)
|
||||
else:
|
||||
cmdline = u"wine C:\\Python27\\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])
|
||||
|
||||
# try finding winekeys anyway, even if above code errored
|
||||
winekeys = []
|
||||
# get any files with extension in the output dir
|
||||
files = [f for f in os.listdir(outdirpath) if f.endswith(extension)]
|
||||
|
||||
@@ -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-2016 some_updates, Apprentice Alf and Apprentice Harper
|
||||
|
||||
# Revision history:
|
||||
# 6.0.0 - Release along with unified plugin
|
||||
@@ -11,8 +11,23 @@
|
||||
# 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
|
||||
# 6.3.5 - Version bump to match plugin
|
||||
# 6.3.6 - Version bump to match plugin
|
||||
# 6.4.0 - Fix for Kindle for PC encryption change
|
||||
# 6.4.1 - Fix for new tags in Topaz ebooks
|
||||
# 6.4.2 - Fix for new tags in Topaz ebooks, and very small Topaz ebooks
|
||||
# 6.4.3 - Version bump to match plugin & Mac app
|
||||
# 6.5.0 - Fix for some new tags in Topaz ebooks
|
||||
|
||||
__version__ = '6.0.8'
|
||||
__version__ = '6.5.0'
|
||||
|
||||
import sys
|
||||
import os, os.path
|
||||
@@ -104,6 +119,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 +143,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 +179,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 +348,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 +410,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 +593,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
|
||||
@@ -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 plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering the necessary data to generate a new key.</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, it’s your name as set in your Barnes & Noble account, My Account page, directly under PERSONAL INFORMATION. Sometimes it is the the name used in the default shipping address, and sometimes it’s 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 that’s 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 that’s 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 don’t 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 plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing ‘.b64’ key files. Key files might come from being exported from this or older plugins, or may have been generated using the original i♥cabbages script.</p>
|
||||
<p>At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing ‘.b64’ key files. Key files might come from being exported from this or older plugins, or may have been generated using the original i♥cabbages script, or you may have made it by following the instructions above.</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>
|
||||
|
||||
@@ -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 plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering a new Kindle serial number.</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 didn’t want to enter a serial number.</p>
|
||||
|
||||
@@ -17,7 +17,7 @@ p {margin-top: 0}
|
||||
|
||||
<body>
|
||||
|
||||
<h1>DeDRM Plugin <span class="version">(v6.0.9)</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>
|
||||
|
||||
|
||||
@@ -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 plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog with two main controls.
|
||||
<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 don’t 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 plugin’s customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted key in the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.</p>
|
||||
|
||||
<h3>Renaming Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will prompt you to enter a new name for the highlighted key in the list. Enter the new name for the key and click the OK button to use the new name, or Cancel to revert to the old name.</p>
|
||||
|
||||
<h3>Exporting Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a computer’s hard-drive. Use this button to export the highlighted key to a file (with a ‘.k4a' file name extension). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.</p>
|
||||
|
||||
<h3>Importing Existing Keyfiles:</h3>
|
||||
|
||||
<p>At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import any ‘.k4a’ file you obtained by using the androidkindlekey.py script manually, or by exporting from another copy of calibre.</p>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -9,8 +9,6 @@ __docformat__ = 'restructuredtext en'
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
#
|
||||
# Requires Calibre version 0.7.55 or higher.
|
||||
#
|
||||
# All credit given to i♥cabbages and The Dark Reverser for the original standalone scripts.
|
||||
# We had the much easier job of converting them to a calibre plugin.
|
||||
#
|
||||
@@ -36,13 +34,34 @@ __docformat__ = 'restructuredtext en'
|
||||
# 6.0.7 - Error handling for incomplete PDF metadata
|
||||
# 6.0.8 - Fixes a Wine key issue and topaz support
|
||||
# 6.0.9 - Ported to work with newer versions of Calibre (moved to Qt5). Still supports older Qt4 versions.
|
||||
# 6.1.0 - Fixed multiple books import problem and PDF import with no key problem
|
||||
# 6.2.0 - Support for getting B&N key from nook Study log. Fix for UTF-8 filenames in Adobe ePubs.
|
||||
# Fix for not copying needed files. Fix for getting default Adobe key for PDFs
|
||||
# 6.2.1 - Fix for non-ascii Windows user names
|
||||
# 6.2.2 - Added URL method for B&N/nook books
|
||||
# 6.3.0 - Added in Kindle for Android serial number solution
|
||||
# 6.3.1 - Version number bump for clarity
|
||||
# 6.3.2 - Fixed Kindle for Android help file
|
||||
# 6.3.3 - Bug fix for Kindle for PC support
|
||||
# 6.3.4 - Fixes for Kindle for Android, Linux, and Kobo 3.17
|
||||
# 6.3.5 - Fixes for Linux, and Kobo 3.19 and more logging
|
||||
# 6.3.6 - Fixes for ADE ePub and PDF introduced in 6.3.5
|
||||
# 6.4.0 - Updated for new Kindle for PC encryption
|
||||
# 6.4.1 - Fix for some new tags in Topaz ebooks.
|
||||
# 6.4.2 - Fix for more new tags in Topaz ebooks and very small Topaz ebooks
|
||||
# 6.4.3 - Fix for error that only appears when not in debug mode
|
||||
# Also includes fix for Macs with bonded ethernet ports
|
||||
# 6.5.0 - Big update to Macintosh app
|
||||
# Fix for some more 'new' tags in Topaz ebooks.
|
||||
# Fix an error in wineutils.py
|
||||
|
||||
|
||||
"""
|
||||
Decrypt DRMed ebooks.
|
||||
"""
|
||||
|
||||
PLUGIN_NAME = u"DeDRM"
|
||||
PLUGIN_VERSION_TUPLE = (6, 0, 9)
|
||||
PLUGIN_VERSION_TUPLE = (6, 5, 0)
|
||||
PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE])
|
||||
# Include an html helpfile in the plugin's zipfile with the following name.
|
||||
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
|
||||
@@ -74,8 +93,12 @@ class SafeUnbuffered:
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
try:
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
except:
|
||||
# We can do nothing if a write fails
|
||||
pass
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
@@ -83,37 +106,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'])
|
||||
minimum_calibre_version = (1, 0, 0) # Compiled python libraries cannot be imported in earlier versions.
|
||||
file_types = set(['epub','pdf','pdb','prc','mobi','pobi','azw','azw1','azw3','azw4','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)
|
||||
@@ -126,9 +139,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
|
||||
@@ -166,7 +208,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()
|
||||
|
||||
@@ -177,8 +224,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
|
||||
@@ -196,13 +304,19 @@ 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()
|
||||
try:
|
||||
of.close()
|
||||
except:
|
||||
print u"{0} v{1}: Exception closing temporary file after {2:.1f} seconds. Ignored.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
|
||||
|
||||
if result == 0:
|
||||
# Decryption was successful.
|
||||
# Return the modified PersistentTemporary file to calibre.
|
||||
print u"{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime)
|
||||
return of.name
|
||||
|
||||
print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime)
|
||||
@@ -226,6 +340,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""
|
||||
|
||||
@@ -244,6 +359,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()
|
||||
@@ -255,8 +372,11 @@ 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()
|
||||
print u"{0} v{1}: Decrypted with new default key after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
|
||||
# Return the modified PersistentTemporary file to calibre.
|
||||
return of.name
|
||||
|
||||
@@ -266,12 +386,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
|
||||
@@ -289,6 +409,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()
|
||||
@@ -298,67 +420,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 = 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):
|
||||
@@ -375,10 +503,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
|
||||
@@ -397,6 +530,8 @@ class DeDRM(FileTypePlugin):
|
||||
scriptpath = os.path.join(self.alfdir,u"kindlekey.py")
|
||||
defaultkeys = WineGetKeys(scriptpath, u".k4i",dedrmprefs['kindlewineprefix'])
|
||||
except:
|
||||
print u"{0} v{1}: Exception when getting default Kindle Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
|
||||
traceback.print_exc()
|
||||
pass
|
||||
|
||||
newkeys = {}
|
||||
@@ -407,7 +542,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")
|
||||
@@ -419,8 +554,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)
|
||||
@@ -449,12 +583,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):
|
||||
@@ -467,7 +602,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':
|
||||
@@ -484,7 +619,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):
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#fileencoding: utf-8
|
||||
|
||||
import os
|
||||
import sys
|
||||
import zlib
|
||||
import tarfile
|
||||
from hashlib import md5
|
||||
from cStringIO import StringIO
|
||||
from binascii import a2b_hex, b2a_hex
|
||||
|
||||
STORAGE = 'AmazonSecureStorage.xml'
|
||||
|
||||
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_serials(path=None):
|
||||
''' get serials from android's shared preference xml '''
|
||||
if path is None:
|
||||
if not os.path.isfile(STORAGE):
|
||||
if os.path.isfile("backup.ab"):
|
||||
get_storage()
|
||||
else:
|
||||
return []
|
||||
path = STORAGE
|
||||
|
||||
if not os.path.isfile(path):
|
||||
return []
|
||||
|
||||
storage = parse_preference(path)
|
||||
salt = storage.get('AmazonSaltKey')
|
||||
if salt and len(salt) == 16:
|
||||
sys.stdout.write('Using AndroidObfuscationV2\n')
|
||||
obfuscation = AndroidObfuscationV2(a2b_hex(salt))
|
||||
else:
|
||||
sys.stdout.write('Using AndroidObfuscation\n')
|
||||
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 [dsnid]
|
||||
|
||||
serials = []
|
||||
for token in tokens:
|
||||
if token:
|
||||
serials.append('%s%s' % (dsnid, token))
|
||||
serials.append(dsnid)
|
||||
for token in tokens:
|
||||
if token:
|
||||
serials.append(token)
|
||||
return serials
|
||||
|
||||
def get_storage(path='backup.ab'):
|
||||
'''get AmazonSecureStorage.xml from android backup.ab
|
||||
backup.ab can be get using adb command:
|
||||
shell> adb backup com.amazon.kindle
|
||||
'''
|
||||
output = None
|
||||
read = open(path, 'rb')
|
||||
head = read.read(24)
|
||||
if head == 'ANDROID BACKUP\n1\n1\nnone\n':
|
||||
output = StringIO(zlib.decompress(read.read()))
|
||||
read.close()
|
||||
|
||||
if not output:
|
||||
return False
|
||||
|
||||
tar = tarfile.open(fileobj=output)
|
||||
for member in tar.getmembers():
|
||||
if member.name.strip().endswith(STORAGE):
|
||||
write = open(STORAGE, 'w')
|
||||
write.write(tar.extractfile(member).read())
|
||||
write.close()
|
||||
break
|
||||
|
||||
return True
|
||||
|
||||
__all__ = [ 'get_storage', 'get_serials', 'parse_preference',
|
||||
'AndroidObfuscation', 'AndroidObfuscationV2', 'STORAGE']
|
||||
|
||||
if __name__ == '__main__':
|
||||
print get_serials()
|
||||
@@ -1,6 +0,0 @@
|
||||
1.1 get AmazonSecureStorage.xml from /data/data/com.amazon.kindle/shared_prefs/AmazonSecureStorage.xml
|
||||
|
||||
1.2 on android 4.0+, run `adb backup com.amazon.kindle` from PC will get backup.ab
|
||||
now android.py can convert backup.ab to AmazonSecureStorage.xml
|
||||
|
||||
2. run `k4mobidedrm.py -a AmazonSecureStorage.xml <infile> <outdir>'
|
||||
@@ -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())
|
||||
@@ -36,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):
|
||||
@@ -53,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']
|
||||
@@ -83,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")
|
||||
@@ -104,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)
|
||||
@@ -115,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:
|
||||
@@ -162,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'])
|
||||
@@ -188,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))
|
||||
@@ -219,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)
|
||||
|
||||
@@ -377,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):
|
||||
@@ -395,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:
|
||||
@@ -429,6 +445,10 @@ class ManageKeysDialog(QDialog):
|
||||
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])
|
||||
|
||||
@@ -515,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)
|
||||
@@ -536,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)
|
||||
@@ -550,6 +566,19 @@ class AddBandNKeyDialog(QDialog):
|
||||
data_group_box_layout.addWidget(ccn_disclaimer_label)
|
||||
layout.addSpacing(10)
|
||||
|
||||
key_group = QHBoxLayout()
|
||||
data_group_box_layout.addLayout(key_group)
|
||||
key_group.addWidget(QLabel(u"Retrieved key:", self))
|
||||
self.key_display = QLabel(u"", self)
|
||||
self.key_display.setToolTip(_(u"Click the Retrieve Key button to fetch your B&N encryption key from the B&N servers"))
|
||||
key_group.addWidget(self.key_display)
|
||||
self.retrieve_button = QtGui.QPushButton(self)
|
||||
self.retrieve_button.setToolTip(_(u"Click to retrieve your B&N encryption key from the B&N servers"))
|
||||
self.retrieve_button.setText(u"Retrieve Key")
|
||||
self.retrieve_button.clicked.connect(self.retrieve_key)
|
||||
key_group.addWidget(self.retrieve_button)
|
||||
layout.addSpacing(10)
|
||||
|
||||
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
self.button_box.accepted.connect(self.accept)
|
||||
self.button_box.rejected.connect(self.reject)
|
||||
@@ -563,8 +592,7 @@ class AddBandNKeyDialog(QDialog):
|
||||
|
||||
@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)
|
||||
return unicode(self.key_display.text()).strip()
|
||||
|
||||
@property
|
||||
def user_name(self):
|
||||
@@ -572,19 +600,28 @@ class AddBandNKeyDialog(QDialog):
|
||||
|
||||
@property
|
||||
def cc_number(self):
|
||||
return unicode(self.cc_ledit.text()).strip().replace(' ', '').replace('-','')
|
||||
return unicode(self.cc_ledit.text()).strip()
|
||||
|
||||
def retrieve_key(self):
|
||||
from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key
|
||||
fetched_key = fetch_bandn_key(self.user_name,self.cc_number)
|
||||
if fetched_key == "":
|
||||
errmsg = u"Could not retrieve key. Check username, password and intenet connectivity and try again."
|
||||
error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
else:
|
||||
self.key_display.setText(fetched_key)
|
||||
|
||||
def accept(self):
|
||||
if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace():
|
||||
errmsg = u"All fields are required!"
|
||||
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
if 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)
|
||||
if len(self.key_value) == 0:
|
||||
self.retrieve_key()
|
||||
if len(self.key_value) == 0:
|
||||
return
|
||||
QDialog.accept(self)
|
||||
|
||||
class AddEReaderDialog(QDialog):
|
||||
@@ -606,9 +643,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)
|
||||
@@ -707,9 +741,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)
|
||||
@@ -780,15 +812,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)
|
||||
@@ -834,9 +865,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)
|
||||
@@ -863,6 +891,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)
|
||||
@@ -882,9 +993,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)
|
||||
|
||||
@@ -164,6 +164,7 @@ class PageParser(object):
|
||||
self.id = os.path.basename(filename).replace('.dat','')
|
||||
self.dict = dict
|
||||
self.debug = debug
|
||||
self.first_unknown = True
|
||||
self.flat_xml = flat_xml
|
||||
self.tagpath = []
|
||||
self.doc = []
|
||||
@@ -262,6 +263,9 @@ class PageParser(object):
|
||||
'img.w' : (1, 'scalar_number', 0, 0),
|
||||
'img.src' : (1, 'scalar_number', 0, 0),
|
||||
'img.color_src' : (1, 'scalar_number', 0, 0),
|
||||
'img.gridSize' : (1, 'scalar_number', 0, 0),
|
||||
'img.gridBottomCenter' : (1, 'scalar_number', 0, 0),
|
||||
'img.gridTopCenter' : (1, 'scalar_number', 0, 0),
|
||||
'img.gridBeginCenter' : (1, 'scalar_number', 0, 0),
|
||||
'img.gridEndCenter' : (1, 'scalar_number', 0, 0),
|
||||
'img.image_type' : (1, 'scalar_number', 0, 0),
|
||||
@@ -313,11 +317,16 @@ class PageParser(object):
|
||||
'span.gridBeginCenter' : (1, 'scalar_number', 0, 0),
|
||||
'span.gridEndCenter' : (1, 'scalar_number', 0, 0),
|
||||
|
||||
'extratokens' : (1, 'snippets', 1, 0),
|
||||
'extratokens.class' : (1, 'scalar_text', 0, 0),
|
||||
'extratokens.type' : (1, 'scalar_text', 0, 0),
|
||||
'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0),
|
||||
'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0),
|
||||
'extratokens' : (1, 'snippets', 1, 0),
|
||||
'extratokens.class' : (1, 'scalar_text', 0, 0),
|
||||
'extratokens.type' : (1, 'scalar_text', 0, 0),
|
||||
'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0),
|
||||
'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0),
|
||||
'extratokens.gridSize' : (1, 'scalar_number', 0, 0),
|
||||
'extratokens.gridBottomCenter' : (1, 'scalar_number', 0, 0),
|
||||
'extratokens.gridTopCenter' : (1, 'scalar_number', 0, 0),
|
||||
'extratokens.gridBeginCenter' : (1, 'scalar_number', 0, 0),
|
||||
'extratokens.gridEndCenter' : (1, 'scalar_number', 0, 0),
|
||||
|
||||
'glyph.h' : (1, 'number', 0, 0),
|
||||
'glyph.w' : (1, 'number', 0, 0),
|
||||
@@ -382,6 +391,18 @@ class PageParser(object):
|
||||
'startID' : (0, 'number', 1, 1),
|
||||
'startID.page' : (1, 'number', 0, 0),
|
||||
'startID.id' : (1, 'number', 0, 0),
|
||||
|
||||
'median_d' : (1, 'number', 0, 0),
|
||||
'median_h' : (1, 'number', 0, 0),
|
||||
'median_firsty' : (1, 'number', 0, 0),
|
||||
'median_lasty' : (1, 'number', 0, 0),
|
||||
|
||||
'num_footers_maybe' : (1, 'number', 0, 0),
|
||||
'num_footers_yes' : (1, 'number', 0, 0),
|
||||
'num_headers_maybe' : (1, 'number', 0, 0),
|
||||
'num_headers_yes' : (1, 'number', 0, 0),
|
||||
|
||||
'tracking' : (1, 'number', 0, 0),
|
||||
|
||||
}
|
||||
|
||||
@@ -506,8 +527,9 @@ class PageParser(object):
|
||||
# or an out of sync condition
|
||||
else:
|
||||
result = []
|
||||
if (self.debug):
|
||||
if (self.debug or self.first_unknown):
|
||||
print 'Unknown Token:', token
|
||||
self.first_unknown = False
|
||||
self.tag_pop()
|
||||
return result
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -385,7 +385,7 @@ def generateBook(bookDir, raw, fixedimage):
|
||||
# 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)
|
||||
pname = 'page%04d.dat' % (pnum - 1)
|
||||
fname = os.path.join(pageDir,pname)
|
||||
flat_xml = convert2xml.fromData(dict, fname)
|
||||
|
||||
|
||||
336
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignoblekey.py
Normal file
336
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignoblekey.py
Normal 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())
|
||||
@@ -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())
|
||||
@@ -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()
|
||||
|
||||
@@ -3,18 +3,19 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ineptepub.pyw, version 6.1
|
||||
# ineptepub.pyw, version 6.5
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2013 by some_updates, DiapDealer and Apprentice Alf
|
||||
# Modified 2015–2016 by Apprentice Harper
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# Windows users: Before running this program, you must first install Python 2.7
|
||||
# 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
|
||||
# install the version for Python 2.7). Save this script file as
|
||||
# ineptepub.pyw and double-click on it to run it.
|
||||
#
|
||||
# Mac OS X users: Save this script file as ineptepub.pyw. You can run this
|
||||
@@ -37,13 +38,17 @@ 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
|
||||
# 6.3 - Add additional check on DER file sanity
|
||||
# 6.4 - Remove erroneous check on DER file sanity
|
||||
# 6.5 - Completely remove erroneous check on DER file sanity
|
||||
|
||||
"""
|
||||
Decrypt Adobe Digital Editions encrypted ePub books.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "6.1"
|
||||
__version__ = "6.5"
|
||||
|
||||
import sys
|
||||
import os
|
||||
@@ -366,7 +371,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)
|
||||
|
||||
@@ -3,18 +3,19 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ineptpdf.pyw, version 7.11
|
||||
# ineptpdf.pyw, version 8.0.4
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
# Modified 2015-2016 by Apprentice Harper
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# Windows users: Before running this program, you must first install Python 2.7
|
||||
# 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
|
||||
# install the version for Python 2.7). Save this script file as
|
||||
# ineptpdf.pyw and double-click on it to run it.
|
||||
#
|
||||
# Mac OS X users: Save this script file as ineptpdf.pyw. You can run this
|
||||
@@ -53,13 +54,17 @@ from __future__ import with_statement
|
||||
# 7.14 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 8.0 - Work if TkInter is missing
|
||||
# 8.0.1 - Broken Metadata fix.
|
||||
# 8.0.2 - Add additional check on DER file sanity
|
||||
# 8.0.3 - Remove erroneous check on DER file sanity
|
||||
# 8.0.4 - Completely remove erroneous check on DER file sanity
|
||||
|
||||
|
||||
"""
|
||||
Decrypts Adobe ADEPT-encrypted PDF files.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "8.0.1"
|
||||
__version__ = "8.0.4"
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
@@ -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,12 +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 android
|
||||
from calibre_plugins.dedrm import androidkindlekey
|
||||
else:
|
||||
import mobidedrm
|
||||
import topazextract
|
||||
import kgenpids
|
||||
import android
|
||||
import androidkindlekey
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
@@ -187,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.")
|
||||
@@ -207,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:
|
||||
@@ -223,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:
|
||||
@@ -239,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()
|
||||
@@ -254,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())
|
||||
@@ -275,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>] [ -a <AmazonSecureStorage.xml> ] <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
|
||||
@@ -298,6 +304,7 @@ def cli_main():
|
||||
infile = args[0]
|
||||
outdir = args[1]
|
||||
kDatabaseFiles = []
|
||||
androidFiles = []
|
||||
serials = []
|
||||
pids = []
|
||||
|
||||
@@ -316,14 +323,13 @@ def cli_main():
|
||||
serials = a.split(',')
|
||||
if o == '-a':
|
||||
if a == None:
|
||||
continue
|
||||
serials.extend(android.get_serials(a))
|
||||
serials.extend(android.get_serials())
|
||||
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__':
|
||||
|
||||
@@ -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,21 +197,26 @@ def getK4Pids(rec209, token, kindleDatabase):
|
||||
|
||||
try:
|
||||
# Get the Mazama Random number
|
||||
MazamaRandomNumber = (kindleDatabase[1])['MazamaRandomNumber'].decode('hex').encode('ascii')
|
||||
|
||||
# Get the kindle account token
|
||||
kindleAccountToken = (kindleDatabase[1])['kindle.account.tokens'].decode('hex').encode('ascii')
|
||||
MazamaRandomNumber = (kindleDatabase[1])['MazamaRandomNumber'].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])
|
||||
return pids
|
||||
|
||||
try:
|
||||
# Get the kindle account token, if present
|
||||
kindleAccountToken = (kindleDatabase[1])['kindle.account.tokens'].decode('hex')
|
||||
|
||||
except KeyError:
|
||||
kindleAccountToken=""
|
||||
pass
|
||||
|
||||
# Get the ID string used
|
||||
encodedIDString = encodeHash(IDString,charMap1)
|
||||
|
||||
|
||||
@@ -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-2016 by some_updates, Apprentice Alf and Apprentice Harper
|
||||
|
||||
# Revision history:
|
||||
# 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc.
|
||||
@@ -20,6 +18,10 @@ from __future__ import with_statement
|
||||
# 1.7 - Work if TkInter is missing
|
||||
# 1.8 - Fixes for Kindle for Mac, and non-ascii in Windows user names
|
||||
# 1.9 - Fixes for Unicode in Windows user names
|
||||
# 2.0 - Added comments and extra fix for non-ascii Windows user names
|
||||
# 2.1 - Fixed Kindle for PC encryption changes March 2016
|
||||
# 2.2 - Fixes for Macs with bonded ethernet ports
|
||||
# Also removed old .kinfo file support (pre-2011)
|
||||
|
||||
|
||||
"""
|
||||
@@ -27,7 +29,7 @@ Retrieve Kindle for PC/Mac user key.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '1.9'
|
||||
__version__ = '2.2'
|
||||
|
||||
import sys, os, re
|
||||
from struct import pack, unpack, unpack_from
|
||||
@@ -885,6 +887,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()
|
||||
@@ -926,7 +929,7 @@ if iswindows:
|
||||
# or the python interface to the 32 vs 64 bit registry is broken
|
||||
path = ""
|
||||
if 'LOCALAPPDATA' in os.environ.keys():
|
||||
# Python 2.x does not return unicode env. Use Python 3.x
|
||||
# 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')
|
||||
@@ -993,179 +996,120 @@ if iswindows:
|
||||
# determine type of kindle info provided and return a
|
||||
# database of keynames and values
|
||||
def getDBfromFile(kInfoFile):
|
||||
names = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber', 'max_date', 'SIGVERIF']
|
||||
names = [\
|
||||
'kindle.account.tokens',\
|
||||
'kindle.cookie.item',\
|
||||
'eulaVersionAccepted',\
|
||||
'login_date',\
|
||||
'kindle.token.item',\
|
||||
'login',\
|
||||
'kindle.key.item',\
|
||||
'kindle.name.info',\
|
||||
'kindle.device.info',\
|
||||
'MazamaRandomNumber',\
|
||||
'max_date',\
|
||||
'SIGVERIF',\
|
||||
'build_version',\
|
||||
]
|
||||
|
||||
DB = {}
|
||||
with open(kInfoFile, 'rb') as infoReader:
|
||||
hdr = infoReader.read(1)
|
||||
data = infoReader.read()
|
||||
# assume newest .kinf2011 style .kinf file
|
||||
# the .kinf file uses "/" to separate it into records
|
||||
# so remove the trailing "/" to make it easy to use split
|
||||
data = data[:-1]
|
||||
items = data.split('/')
|
||||
|
||||
if data.find('{') != -1 :
|
||||
# older style kindle-info file
|
||||
items = data.split('{')
|
||||
for item in items:
|
||||
if item != '':
|
||||
keyhash, rawdata = item.split(':')
|
||||
keyname = "unknown"
|
||||
for name in names:
|
||||
if encodeHash(name,charMap2) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
keyname = keyhash
|
||||
encryptedValue = decode(rawdata,charMap2)
|
||||
DB[keyname] = CryptUnprotectData(encryptedValue, "", 0)
|
||||
elif hdr == '/':
|
||||
# else rainier-2-1-1 .kinf file
|
||||
# the .kinf file uses "/" to separate it into records
|
||||
# so remove the trailing "/" to make it easy to use split
|
||||
data = data[:-1]
|
||||
items = data.split('/')
|
||||
# starts with an encoded and encrypted header blob
|
||||
headerblob = items.pop(0)
|
||||
encryptedValue = decode(headerblob, testMap1)
|
||||
cleartext = UnprotectHeaderData(encryptedValue)
|
||||
#print "header cleartext:",cleartext
|
||||
# now extract the pieces that form the added entropy
|
||||
pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
|
||||
for m in re.finditer(pattern, cleartext):
|
||||
added_entropy = m.group(2) + m.group(4)
|
||||
|
||||
# loop through the item records until all are processed
|
||||
while len(items) > 0:
|
||||
|
||||
# get the first item record
|
||||
# loop through the item records until all are processed
|
||||
while len(items) > 0:
|
||||
|
||||
# get the first item record
|
||||
item = items.pop(0)
|
||||
|
||||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
|
||||
# the sha1 of raw keyhash string is used to create entropy along
|
||||
# with the added entropy provided above from the headerblob
|
||||
entropy = SHA1(keyhash) + added_entropy
|
||||
|
||||
# the remainder of the first record when decoded with charMap5
|
||||
# has the ':' split char followed by the string representation
|
||||
# of the number of records that follow
|
||||
# and make up the contents
|
||||
srcnt = decode(item[34:],charMap5)
|
||||
rcnt = int(srcnt)
|
||||
|
||||
# read and store in rcnt records of data
|
||||
# that make up the contents value
|
||||
edlst = []
|
||||
for i in xrange(rcnt):
|
||||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
# key names now use the new testMap8 encoding
|
||||
keyname = "unknown"
|
||||
for name in names:
|
||||
if encodeHash(name,testMap8) == keyhash:
|
||||
keyname = name
|
||||
#print "keyname found from hash:",keyname
|
||||
break
|
||||
if keyname == "unknown":
|
||||
keyname = keyhash
|
||||
#print "keyname not found, hash is:",keyname
|
||||
|
||||
# the raw keyhash string is used to create entropy for the actual
|
||||
# CryptProtectData Blob that represents that keys contents
|
||||
entropy = SHA1(keyhash)
|
||||
# the testMap8 encoded contents data has had a length
|
||||
# of chars (always odd) cut off of the front and moved
|
||||
# to the end to prevent decoding using testMap8 from
|
||||
# working properly, and thereby preventing the ensuing
|
||||
# CryptUnprotectData call from succeeding.
|
||||
|
||||
# the remainder of the first record when decoded with charMap5
|
||||
# has the ':' split char followed by the string representation
|
||||
# of the number of records that follow
|
||||
# and make up the contents
|
||||
srcnt = decode(item[34:],charMap5)
|
||||
rcnt = int(srcnt)
|
||||
# The offset into the testMap8 encoded contents seems to be:
|
||||
# len(contents)-largest prime number <= int(len(content)/3)
|
||||
# (in other words split "about" 2/3rds of the way through)
|
||||
|
||||
# read and store in rcnt records of data
|
||||
# that make up the contents value
|
||||
edlst = []
|
||||
for i in xrange(rcnt):
|
||||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
keyname = "unknown"
|
||||
for name in names:
|
||||
if encodeHash(name,charMap5) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
keyname = keyhash
|
||||
# the charMap5 encoded contents data has had a length
|
||||
# of chars (always odd) cut off of the front and moved
|
||||
# to the end to prevent decoding using charMap5 from
|
||||
# working properly, and thereby preventing the ensuing
|
||||
# CryptUnprotectData call from succeeding.
|
||||
|
||||
# The offset into the charMap5 encoded contents seems to be:
|
||||
# len(contents)-largest prime number <= int(len(content)/3)
|
||||
# (in other words split "about" 2/3rds of the way through)
|
||||
|
||||
# move first offsets chars to end to align for decode by charMap5
|
||||
encdata = "".join(edlst)
|
||||
contlen = len(encdata)
|
||||
noffset = contlen - primes(int(contlen/3))[-1]
|
||||
|
||||
# now properly split and recombine
|
||||
# by moving noffset chars from the start of the
|
||||
# string to the end of the string
|
||||
pfx = encdata[0:noffset]
|
||||
encdata = encdata[noffset:]
|
||||
encdata = encdata + pfx
|
||||
|
||||
# decode using Map5 to get the CryptProtect Data
|
||||
encryptedValue = decode(encdata,charMap5)
|
||||
DB[keyname] = CryptUnprotectData(encryptedValue, entropy, 1)
|
||||
else:
|
||||
# else newest .kinf2011 style .kinf file
|
||||
# the .kinf file uses "/" to separate it into records
|
||||
# so remove the trailing "/" to make it easy to use split
|
||||
# need to put back the first char read because it it part
|
||||
# of the added entropy blob
|
||||
data = hdr + data[:-1]
|
||||
items = data.split('/')
|
||||
|
||||
# starts with and encoded and encrypted header blob
|
||||
headerblob = items.pop(0)
|
||||
encryptedValue = decode(headerblob, testMap1)
|
||||
cleartext = UnprotectHeaderData(encryptedValue)
|
||||
# now extract the pieces that form the added entropy
|
||||
pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
|
||||
for m in re.finditer(pattern, cleartext):
|
||||
added_entropy = m.group(2) + m.group(4)
|
||||
# move first offsets chars to end to align for decode by testMap8
|
||||
# by moving noffset chars from the start of the
|
||||
# string to the end of the string
|
||||
encdata = "".join(edlst)
|
||||
#print "encrypted data:",encdata
|
||||
contlen = len(encdata)
|
||||
noffset = contlen - primes(int(contlen/3))[-1]
|
||||
pfx = encdata[0:noffset]
|
||||
encdata = encdata[noffset:]
|
||||
encdata = encdata + pfx
|
||||
#print "rearranged data:",encdata
|
||||
|
||||
|
||||
# loop through the item records until all are processed
|
||||
while len(items) > 0:
|
||||
|
||||
# get the first item record
|
||||
item = items.pop(0)
|
||||
|
||||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
|
||||
# the sha1 of raw keyhash string is used to create entropy along
|
||||
# with the added entropy provided above from the headerblob
|
||||
entropy = SHA1(keyhash) + added_entropy
|
||||
|
||||
# the remainder of the first record when decoded with charMap5
|
||||
# has the ':' split char followed by the string representation
|
||||
# of the number of records that follow
|
||||
# and make up the contents
|
||||
srcnt = decode(item[34:],charMap5)
|
||||
rcnt = int(srcnt)
|
||||
|
||||
# read and store in rcnt records of data
|
||||
# that make up the contents value
|
||||
edlst = []
|
||||
for i in xrange(rcnt):
|
||||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
# key names now use the new testMap8 encoding
|
||||
keyname = "unknown"
|
||||
for name in names:
|
||||
if encodeHash(name,testMap8) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
|
||||
# the testMap8 encoded contents data has had a length
|
||||
# of chars (always odd) cut off of the front and moved
|
||||
# to the end to prevent decoding using testMap8 from
|
||||
# working properly, and thereby preventing the ensuing
|
||||
# CryptUnprotectData call from succeeding.
|
||||
|
||||
# The offset into the testMap8 encoded contents seems to be:
|
||||
# len(contents)-largest prime number <= int(len(content)/3)
|
||||
# (in other words split "about" 2/3rds of the way through)
|
||||
|
||||
# move first offsets chars to end to align for decode by testMap8
|
||||
# by moving noffset chars from the start of the
|
||||
# string to the end of the string
|
||||
encdata = "".join(edlst)
|
||||
contlen = len(encdata)
|
||||
noffset = contlen - primes(int(contlen/3))[-1]
|
||||
pfx = encdata[0:noffset]
|
||||
encdata = encdata[noffset:]
|
||||
encdata = encdata + pfx
|
||||
|
||||
# decode using new testMap8 to get the original CryptProtect Data
|
||||
encryptedValue = decode(encdata,testMap8)
|
||||
cleartext = CryptUnprotectData(encryptedValue, entropy, 1)
|
||||
# decode using new testMap8 to get the original CryptProtect Data
|
||||
encryptedValue = decode(encdata,testMap8)
|
||||
#print "decoded data:",encryptedValue.encode('hex')
|
||||
cleartext = CryptUnprotectData(encryptedValue, entropy, 1)
|
||||
if len(cleartext)>0:
|
||||
#print "cleartext data:",cleartext,":end data"
|
||||
DB[keyname] = cleartext
|
||||
#print keyname, cleartext
|
||||
|
||||
if '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 len(DB)>6:
|
||||
# store values used in decryption
|
||||
DB['IDString'] = GetIDString()
|
||||
DB['UserName'] = GetUserName()
|
||||
print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(GetIDString(), GetUserName().encode('hex'))
|
||||
else:
|
||||
print u"Couldn't decrypt file."
|
||||
DB = {}
|
||||
return DB
|
||||
elif isosx:
|
||||
@@ -1297,11 +1241,9 @@ elif isosx:
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
out1, out2 = p.communicate()
|
||||
#print out1
|
||||
reslst = out1.split('\n')
|
||||
cnt = len(reslst)
|
||||
bsdname = None
|
||||
sernum = None
|
||||
foundIt = False
|
||||
for j in xrange(cnt):
|
||||
resline = reslst[j]
|
||||
pp = resline.find('\"Serial Number\" = \"')
|
||||
@@ -1310,31 +1252,24 @@ elif isosx:
|
||||
sernums.append(sernum.strip())
|
||||
return sernums
|
||||
|
||||
def GetUserHomeAppSupKindleDirParitionName():
|
||||
home = os.getenv('HOME')
|
||||
dpath = home + '/Library'
|
||||
def GetDiskPartitionNames():
|
||||
names = []
|
||||
cmdline = '/sbin/mount'
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
out1, out2 = p.communicate()
|
||||
reslst = out1.split('\n')
|
||||
cnt = len(reslst)
|
||||
disk = ''
|
||||
foundIt = False
|
||||
for j in xrange(cnt):
|
||||
resline = reslst[j]
|
||||
if resline.startswith('/dev'):
|
||||
(devpart, mpath) = resline.split(' on ')
|
||||
dpart = devpart[5:]
|
||||
pp = mpath.find('(')
|
||||
if pp >= 0:
|
||||
mpath = mpath[:pp-1]
|
||||
if dpath.startswith(mpath):
|
||||
disk = dpart
|
||||
return disk
|
||||
names.append(dpart)
|
||||
return names
|
||||
|
||||
# uses a sub process to get the UUID of the specified disk partition using ioreg
|
||||
def GetDiskPartitionUUIDs(diskpart):
|
||||
# uses a sub process to get the UUID of all disk partitions
|
||||
def GetDiskPartitionUUIDs():
|
||||
uuids = []
|
||||
uuidnum = os.getenv('MYUUIDNUMBER')
|
||||
if uuidnum != None:
|
||||
@@ -1343,46 +1278,16 @@ elif isosx:
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
out1, out2 = p.communicate()
|
||||
#print out1
|
||||
reslst = out1.split('\n')
|
||||
cnt = len(reslst)
|
||||
bsdname = None
|
||||
uuidnum = None
|
||||
foundIt = False
|
||||
nest = 0
|
||||
uuidnest = -1
|
||||
partnest = -2
|
||||
for j in xrange(cnt):
|
||||
resline = reslst[j]
|
||||
if resline.find('{') >= 0:
|
||||
nest += 1
|
||||
if resline.find('}') >= 0:
|
||||
nest -= 1
|
||||
pp = resline.find('\"UUID\" = \"')
|
||||
if pp >= 0:
|
||||
uuidnum = resline[pp+10:-1]
|
||||
uuidnum = uuidnum.strip()
|
||||
uuidnest = nest
|
||||
if partnest == uuidnest and uuidnest > 0:
|
||||
foundIt = True
|
||||
break
|
||||
bb = resline.find('\"BSD Name\" = \"')
|
||||
if bb >= 0:
|
||||
bsdname = resline[bb+14:-1]
|
||||
bsdname = bsdname.strip()
|
||||
if (bsdname == diskpart):
|
||||
partnest = nest
|
||||
else :
|
||||
partnest = -2
|
||||
if partnest == uuidnest and partnest > 0:
|
||||
foundIt = True
|
||||
break
|
||||
if nest == 0:
|
||||
partnest = -2
|
||||
uuidnest = -1
|
||||
uuidnum = None
|
||||
bsdname = None
|
||||
if foundIt:
|
||||
uuids.append(uuidnum)
|
||||
uuids.append(uuidnum)
|
||||
return uuids
|
||||
|
||||
def GetMACAddressesMunged():
|
||||
@@ -1390,28 +1295,26 @@ elif isosx:
|
||||
macnum = os.getenv('MYMACNUM')
|
||||
if macnum != None:
|
||||
macnums.append(macnum)
|
||||
cmdline = '/sbin/ifconfig en0'
|
||||
cmdline = 'networksetup -listallhardwareports' # en0'
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
out1, out2 = p.communicate()
|
||||
reslst = out1.split('\n')
|
||||
cnt = len(reslst)
|
||||
macnum = None
|
||||
foundIt = False
|
||||
for j in xrange(cnt):
|
||||
resline = reslst[j]
|
||||
pp = resline.find('ether ')
|
||||
pp = resline.find('Ethernet Address: ')
|
||||
if pp >= 0:
|
||||
macnum = resline[pp+6:-1]
|
||||
#print resline
|
||||
macnum = resline[pp+18:]
|
||||
macnum = macnum.strip()
|
||||
# print 'original mac', macnum
|
||||
# now munge it up the way Kindle app does
|
||||
# by xoring it with 0xa5 and swapping elements 3 and 4
|
||||
maclst = macnum.split(':')
|
||||
n = len(maclst)
|
||||
if n != 6:
|
||||
fountIt = False
|
||||
break
|
||||
continue
|
||||
#print 'original mac', macnum
|
||||
# now munge it up the way Kindle app does
|
||||
# by xoring it with 0xa5 and swapping elements 3 and 4
|
||||
for i in range(6):
|
||||
maclst[i] = int('0x' + maclst[i], 0)
|
||||
mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
|
||||
@@ -1422,16 +1325,15 @@ elif isosx:
|
||||
mlst[1] = maclst[1] ^ 0xa5
|
||||
mlst[0] = maclst[0] ^ 0xa5
|
||||
macnum = '%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x' % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5])
|
||||
foundIt = True
|
||||
break
|
||||
if foundIt:
|
||||
macnums.append(macnum)
|
||||
#print 'munged mac', macnum
|
||||
macnums.append(macnum)
|
||||
return macnums
|
||||
|
||||
|
||||
# uses unix env to get username instead of using sysctlbyname
|
||||
def GetUserName():
|
||||
username = os.getenv('USER')
|
||||
#print "Username:",username
|
||||
return username
|
||||
|
||||
def GetIDStrings():
|
||||
@@ -1439,58 +1341,13 @@ elif isosx:
|
||||
strings = []
|
||||
strings.extend(GetMACAddressesMunged())
|
||||
strings.extend(GetVolumesSerialNumbers())
|
||||
diskpart = GetUserHomeAppSupKindleDirParitionName()
|
||||
strings.extend(GetDiskPartitionUUIDs(diskpart))
|
||||
strings.extend(GetDiskPartitionNames())
|
||||
strings.extend(GetDiskPartitionUUIDs())
|
||||
strings.append('9999999999')
|
||||
#print strings
|
||||
#print "ID Strings:\n",strings
|
||||
return strings
|
||||
|
||||
|
||||
# implements an Pseudo Mac Version of Windows built-in Crypto routine
|
||||
# used by Kindle for Mac versions < 1.6.0
|
||||
class CryptUnprotectData(object):
|
||||
def __init__(self, IDString):
|
||||
sp = IDString + '!@#' + GetUserName()
|
||||
passwdData = encode(SHA256(sp),charMap1)
|
||||
salt = '16743'
|
||||
self.crp = LibCrypto()
|
||||
iter = 0x3e8
|
||||
keylen = 0x80
|
||||
key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen)
|
||||
self.key = key_iv[0:32]
|
||||
self.iv = key_iv[32:48]
|
||||
self.crp.set_decrypt_key(self.key, self.iv)
|
||||
|
||||
def decrypt(self, encryptedData):
|
||||
cleartext = self.crp.decrypt(encryptedData)
|
||||
cleartext = decode(cleartext,charMap1)
|
||||
return cleartext
|
||||
|
||||
|
||||
# implements an Pseudo Mac Version of Windows built-in Crypto routine
|
||||
# used for Kindle for Mac Versions >= 1.6.0
|
||||
class CryptUnprotectDataV2(object):
|
||||
def __init__(self, IDString):
|
||||
sp = GetUserName() + ':&%:' + IDString
|
||||
passwdData = encode(SHA256(sp),charMap5)
|
||||
# salt generation as per the code
|
||||
salt = 0x0512981d * 2 * 1 * 1
|
||||
salt = str(salt) + GetUserName()
|
||||
salt = encode(salt,charMap5)
|
||||
self.crp = LibCrypto()
|
||||
iter = 0x800
|
||||
keylen = 0x400
|
||||
key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen)
|
||||
self.key = key_iv[0:32]
|
||||
self.iv = key_iv[32:48]
|
||||
self.crp.set_decrypt_key(self.key, self.iv)
|
||||
|
||||
def decrypt(self, encryptedData):
|
||||
cleartext = self.crp.decrypt(encryptedData)
|
||||
cleartext = decode(cleartext, charMap5)
|
||||
return cleartext
|
||||
|
||||
|
||||
# unprotect the new header blob in .kinf2011
|
||||
# used in Kindle for Mac Version >= 1.9.0
|
||||
def UnprotectHeaderData(encryptedData):
|
||||
@@ -1508,8 +1365,7 @@ elif isosx:
|
||||
|
||||
|
||||
# implements an Pseudo Mac Version of Windows built-in Crypto routine
|
||||
# used for Kindle for Mac Versions >= 1.9.0
|
||||
class CryptUnprotectDataV3(object):
|
||||
class CryptUnprotectData(object):
|
||||
def __init__(self, entropy, IDString):
|
||||
sp = GetUserName() + '+@#$%+' + IDString
|
||||
passwdData = encode(SHA256(sp),charMap2)
|
||||
@@ -1577,206 +1433,118 @@ elif isosx:
|
||||
# determine type of kindle info provided and return a
|
||||
# database of keynames and values
|
||||
def getDBfromFile(kInfoFile):
|
||||
names = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber', 'max_date', 'SIGVERIF']
|
||||
names = [\
|
||||
'kindle.account.tokens',\
|
||||
'kindle.cookie.item',\
|
||||
'eulaVersionAccepted',\
|
||||
'login_date',\
|
||||
'kindle.token.item',\
|
||||
'login',\
|
||||
'kindle.key.item',\
|
||||
'kindle.name.info',\
|
||||
'kindle.device.info',\
|
||||
'MazamaRandomNumber',\
|
||||
'max_date',\
|
||||
'SIGVERIF',\
|
||||
'build_version',\
|
||||
]
|
||||
with open(kInfoFile, 'rb') as infoReader:
|
||||
filehdr = infoReader.read(1)
|
||||
filedata = infoReader.read()
|
||||
|
||||
data = filedata[:-1]
|
||||
items = data.split('/')
|
||||
IDStrings = GetIDStrings()
|
||||
for IDString in IDStrings:
|
||||
DB = {}
|
||||
#print "trying IDString:",IDString
|
||||
try:
|
||||
hdr = filehdr
|
||||
data = filedata
|
||||
if data.find('[') != -1 :
|
||||
# older style kindle-info file
|
||||
cud = CryptUnprotectData(IDString)
|
||||
items = data.split('[')
|
||||
for item in items:
|
||||
if item != '':
|
||||
keyhash, rawdata = item.split(':')
|
||||
keyname = 'unknown'
|
||||
for name in names:
|
||||
if encodeHash(name,charMap2) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == 'unknown':
|
||||
keyname = keyhash
|
||||
encryptedValue = decode(rawdata,charMap2)
|
||||
cleartext = cud.decrypt(encryptedValue)
|
||||
if len(cleartext) > 0:
|
||||
DB[keyname] = cleartext
|
||||
if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB:
|
||||
break
|
||||
elif hdr == '/':
|
||||
# else newer style .kinf file used by K4Mac >= 1.6.0
|
||||
# the .kinf file uses '/' to separate it into records
|
||||
# so remove the trailing '/' to make it easy to use split
|
||||
data = data[:-1]
|
||||
items = data.split('/')
|
||||
cud = CryptUnprotectDataV2(IDString)
|
||||
DB = {}
|
||||
items = data.split('/')
|
||||
|
||||
# the headerblob is the encrypted information needed to build the entropy string
|
||||
headerblob = items.pop(0)
|
||||
encryptedValue = decode(headerblob, charMap1)
|
||||
cleartext = UnprotectHeaderData(encryptedValue)
|
||||
|
||||
# loop through the item records until all are processed
|
||||
while len(items) > 0:
|
||||
# now extract the pieces in the same way
|
||||
# this version is different from K4PC it scales the build number by multipying by 735
|
||||
pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
|
||||
for m in re.finditer(pattern, cleartext):
|
||||
entropy = str(int(m.group(2)) * 0x2df) + m.group(4)
|
||||
|
||||
# get the first item record
|
||||
cud = CryptUnprotectData(entropy,IDString)
|
||||
|
||||
# loop through the item records until all are processed
|
||||
while len(items) > 0:
|
||||
|
||||
# get the first item record
|
||||
item = items.pop(0)
|
||||
|
||||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
keyname = 'unknown'
|
||||
|
||||
# unlike K4PC the keyhash is not used in generating entropy
|
||||
# entropy = SHA1(keyhash) + added_entropy
|
||||
# entropy = added_entropy
|
||||
|
||||
# the remainder of the first record when decoded with charMap5
|
||||
# has the ':' split char followed by the string representation
|
||||
# of the number of records that follow
|
||||
# and make up the contents
|
||||
srcnt = decode(item[34:],charMap5)
|
||||
rcnt = int(srcnt)
|
||||
|
||||
# read and store in rcnt records of data
|
||||
# that make up the contents value
|
||||
edlst = []
|
||||
for i in xrange(rcnt):
|
||||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
keyname = 'unknown'
|
||||
keyname = 'unknown'
|
||||
for name in names:
|
||||
if encodeHash(name,testMap8) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == 'unknown':
|
||||
keyname = keyhash
|
||||
|
||||
# the raw keyhash string is also used to create entropy for the actual
|
||||
# CryptProtectData Blob that represents that keys contents
|
||||
# 'entropy' not used for K4Mac only K4PC
|
||||
# entropy = SHA1(keyhash)
|
||||
# the testMap8 encoded contents data has had a length
|
||||
# of chars (always odd) cut off of the front and moved
|
||||
# to the end to prevent decoding using testMap8 from
|
||||
# working properly, and thereby preventing the ensuing
|
||||
# CryptUnprotectData call from succeeding.
|
||||
|
||||
# the remainder of the first record when decoded with charMap5
|
||||
# has the ':' split char followed by the string representation
|
||||
# of the number of records that follow
|
||||
# and make up the contents
|
||||
srcnt = decode(item[34:],charMap5)
|
||||
rcnt = int(srcnt)
|
||||
# The offset into the testMap8 encoded contents seems to be:
|
||||
# len(contents) - largest prime number less than or equal to int(len(content)/3)
|
||||
# (in other words split 'about' 2/3rds of the way through)
|
||||
|
||||
# read and store in rcnt records of data
|
||||
# that make up the contents value
|
||||
edlst = []
|
||||
for i in xrange(rcnt):
|
||||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
# move first offsets chars to end to align for decode by testMap8
|
||||
encdata = ''.join(edlst)
|
||||
contlen = len(encdata)
|
||||
|
||||
keyname = 'unknown'
|
||||
for name in names:
|
||||
if encodeHash(name,charMap5) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == 'unknown':
|
||||
keyname = keyhash
|
||||
# now properly split and recombine
|
||||
# by moving noffset chars from the start of the
|
||||
# string to the end of the string
|
||||
noffset = contlen - primes(int(contlen/3))[-1]
|
||||
pfx = encdata[0:noffset]
|
||||
encdata = encdata[noffset:]
|
||||
encdata = encdata + pfx
|
||||
|
||||
# the charMap5 encoded contents data has had a length
|
||||
# of chars (always odd) cut off of the front and moved
|
||||
# to the end to prevent decoding using charMap5 from
|
||||
# working properly, and thereby preventing the ensuing
|
||||
# CryptUnprotectData call from succeeding.
|
||||
# decode using testMap8 to get the CryptProtect Data
|
||||
encryptedValue = decode(encdata,testMap8)
|
||||
cleartext = cud.decrypt(encryptedValue)
|
||||
# print keyname
|
||||
# print cleartext
|
||||
if len(cleartext) > 0:
|
||||
DB[keyname] = cleartext
|
||||
|
||||
# The offset into the charMap5 encoded contents seems to be:
|
||||
# len(contents) - largest prime number less than or equal to int(len(content)/3)
|
||||
# (in other words split 'about' 2/3rds of the way through)
|
||||
|
||||
# move first offsets chars to end to align for decode by charMap5
|
||||
encdata = ''.join(edlst)
|
||||
contlen = len(encdata)
|
||||
|
||||
# now properly split and recombine
|
||||
# by moving noffset chars from the start of the
|
||||
# string to the end of the string
|
||||
noffset = contlen - primes(int(contlen/3))[-1]
|
||||
pfx = encdata[0:noffset]
|
||||
encdata = encdata[noffset:]
|
||||
encdata = encdata + pfx
|
||||
|
||||
# decode using charMap5 to get the CryptProtect Data
|
||||
encryptedValue = decode(encdata,charMap5)
|
||||
cleartext = cud.decrypt(encryptedValue)
|
||||
if len(cleartext) > 0:
|
||||
DB[keyname] = cleartext
|
||||
|
||||
if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB:
|
||||
break
|
||||
else:
|
||||
# the latest .kinf2011 version for K4M 1.9.1
|
||||
# put back the hdr char, it is needed
|
||||
data = hdr + data
|
||||
data = data[:-1]
|
||||
items = data.split('/')
|
||||
|
||||
# the headerblob is the encrypted information needed to build the entropy string
|
||||
headerblob = items.pop(0)
|
||||
encryptedValue = decode(headerblob, charMap1)
|
||||
cleartext = UnprotectHeaderData(encryptedValue)
|
||||
|
||||
# now extract the pieces in the same way
|
||||
# this version is different from K4PC it scales the build number by multipying by 735
|
||||
pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
|
||||
for m in re.finditer(pattern, cleartext):
|
||||
entropy = str(int(m.group(2)) * 0x2df) + m.group(4)
|
||||
|
||||
cud = CryptUnprotectDataV3(entropy,IDString)
|
||||
|
||||
# loop through the item records until all are processed
|
||||
while len(items) > 0:
|
||||
|
||||
# get the first item record
|
||||
item = items.pop(0)
|
||||
|
||||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
keyname = 'unknown'
|
||||
|
||||
# unlike K4PC the keyhash is not used in generating entropy
|
||||
# entropy = SHA1(keyhash) + added_entropy
|
||||
# entropy = added_entropy
|
||||
|
||||
# the remainder of the first record when decoded with charMap5
|
||||
# has the ':' split char followed by the string representation
|
||||
# of the number of records that follow
|
||||
# and make up the contents
|
||||
srcnt = decode(item[34:],charMap5)
|
||||
rcnt = int(srcnt)
|
||||
|
||||
# read and store in rcnt records of data
|
||||
# that make up the contents value
|
||||
edlst = []
|
||||
for i in xrange(rcnt):
|
||||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
keyname = 'unknown'
|
||||
for name in names:
|
||||
if encodeHash(name,testMap8) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == 'unknown':
|
||||
keyname = keyhash
|
||||
|
||||
# the testMap8 encoded contents data has had a length
|
||||
# of chars (always odd) cut off of the front and moved
|
||||
# to the end to prevent decoding using testMap8 from
|
||||
# working properly, and thereby preventing the ensuing
|
||||
# CryptUnprotectData call from succeeding.
|
||||
|
||||
# The offset into the testMap8 encoded contents seems to be:
|
||||
# len(contents) - largest prime number less than or equal to int(len(content)/3)
|
||||
# (in other words split 'about' 2/3rds of the way through)
|
||||
|
||||
# move first offsets chars to end to align for decode by testMap8
|
||||
encdata = ''.join(edlst)
|
||||
contlen = len(encdata)
|
||||
|
||||
# now properly split and recombine
|
||||
# by moving noffset chars from the start of the
|
||||
# string to the end of the string
|
||||
noffset = contlen - primes(int(contlen/3))[-1]
|
||||
pfx = encdata[0:noffset]
|
||||
encdata = encdata[noffset:]
|
||||
encdata = encdata + pfx
|
||||
|
||||
# decode using testMap8 to get the CryptProtect Data
|
||||
encryptedValue = decode(encdata,testMap8)
|
||||
cleartext = cud.decrypt(encryptedValue)
|
||||
# print keyname
|
||||
# print cleartext
|
||||
if len(cleartext) > 0:
|
||||
DB[keyname] = cleartext
|
||||
|
||||
if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB:
|
||||
break
|
||||
if len(DB)>6:
|
||||
break
|
||||
except:
|
||||
pass
|
||||
if 'kindle.account.tokens' in DB:
|
||||
if len(DB)>6:
|
||||
# store values used in decryption
|
||||
print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString, GetUserName())
|
||||
DB['IDString'] = IDString
|
||||
@@ -1840,7 +1608,7 @@ def cli_main():
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
argv=unicode_argv()
|
||||
progname = os.path.basename(argv[0])
|
||||
print u"{0} v{1}\nCopyright © 2010-2013 some_updates and Apprentice Alf".format(progname,__version__)
|
||||
print u"{0} v{1}\nCopyright © 2010-2016 by some_updates, Apprentice Alf and Apprentice Harper".format(progname,__version__)
|
||||
|
||||
try:
|
||||
opts, args = getopt.getopt(argv[1:], "hk:")
|
||||
@@ -1870,7 +1638,7 @@ def cli_main():
|
||||
# save to the same directory as the script
|
||||
outpath = os.path.dirname(argv[0])
|
||||
|
||||
# make sure the outpath is the
|
||||
# make sure the outpath is canonical
|
||||
outpath = os.path.realpath(os.path.normpath(outpath))
|
||||
|
||||
if not getkey(outpath, files):
|
||||
|
||||
@@ -448,6 +448,8 @@ class MobiBook:
|
||||
goodpids.append(pid[0:-2])
|
||||
elif len(pid)==8:
|
||||
goodpids.append(pid)
|
||||
else:
|
||||
print u"Warning: PID {0} has wrong number of digits".format(pid)
|
||||
|
||||
if self.crypto_type == 1:
|
||||
t1_keyvec = 'QDCVEPMU675RUBSZ'
|
||||
@@ -530,7 +532,7 @@ def cli_main():
|
||||
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])
|
||||
print u"MobiDeDRM v{0} Error: {1:s}".format(__version__,e.args[0])
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
@@ -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'] == []:
|
||||
|
||||
@@ -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)
|
||||
|
||||
148
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/subasyncio.py
Normal file
148
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/subasyncio.py
Normal 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()
|
||||
@@ -26,11 +26,12 @@ def WineGetKeys(scriptpath, extension, wineprefix=""):
|
||||
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)
|
||||
print u"{0} v{1}: Command line: '{2}'".format(PLUGIN_NAME, PLUGIN_VERSION, cmdline)
|
||||
|
||||
try:
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
@@ -38,8 +39,20 @@ def WineGetKeys(scriptpath, extension, wineprefix=""):
|
||||
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 []
|
||||
if wineprefix != "" and os.path.exists(wineprefix):
|
||||
cmdline = u"WINEPREFIX=\"{2}\" wine C:\\Python27\\python.exe \"{0}\" \"{1}\"".format(scriptpath,outdirpath,wineprefix)
|
||||
else:
|
||||
cmdline = u"wine C:\\Python27\\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])
|
||||
|
||||
# try finding winekeys anyway, even if above code errored
|
||||
winekeys = []
|
||||
# get any files with extension in the output dir
|
||||
files = [f for f in os.listdir(outdirpath) if f.endswith(extension)]
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
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 (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 (2.0.1) 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, 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
|
||||
eReader Social DRM: Name:Last 8 digits of CC number
|
||||
MobiPocket: 10 digit PID
|
||||
eReader Social DRM: Name:Last 8 digits of CC number
|
||||
|
||||
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.3.4a/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.
|
||||
|
||||
@@ -30,37 +29,37 @@ Installation
|
||||
|
||||
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 ActiveState’s Active Python, free Community Edition for Windows (x86) 32 bits. This is a free, full version of the Python. It comes with some important additional modules that are not included in the bare-bones version from www.python.org unless you choose to install everything.
|
||||
I strongly recommend fully installing ActiveState’s 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, but NOT 3.x) 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 Foord’s blog.
|
||||
|
||||
1. Download PyCrypto 2.1 for 32bit Windows and Python 2.7 from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
|
||||
|
||||
2. When it has finished downloading, unzip it. This will produce a file “pycrypto-2.1.0.win32-py2.7.exe”.
|
||||
|
||||
3. Double-click “pycrypto-2.1.0.win32-py2.7.exe” to run it. Accept the default options.
|
||||
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 Foord’s 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.
|
||||
|
||||
Binary file not shown.
@@ -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 plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering the necessary data to generate a new key.</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, it’s your name as set in your Barnes & Noble account, My Account page, directly under PERSONAL INFORMATION. Sometimes it is the the name used in the default shipping address, and sometimes it’s 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 that’s 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 that’s 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 don’t 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 plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing ‘.b64’ key files. Key files might come from being exported from this or older plugins, or may have been generated using the original i♥cabbages script.</p>
|
||||
<p>At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing ‘.b64’ key files. Key files might come from being exported from this or older plugins, or may have been generated using the original i♥cabbages script, or you may have made it by following the instructions above.</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>
|
||||
|
||||
@@ -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 plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering a new Kindle serial number.</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 didn’t want to enter a serial number.</p>
|
||||
|
||||
@@ -17,7 +17,7 @@ p {margin-top: 0}
|
||||
|
||||
<body>
|
||||
|
||||
<h1>DeDRM Plugin <span class="version">(v6.0.9)</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>
|
||||
|
||||
|
||||
@@ -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 plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog with two main controls.
|
||||
<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 don’t 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 plugin’s customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted key in the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.</p>
|
||||
|
||||
<h3>Renaming Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will prompt you to enter a new name for the highlighted key in the list. Enter the new name for the key and click the OK button to use the new name, or Cancel to revert to the old name.</p>
|
||||
|
||||
<h3>Exporting Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a computer’s hard-drive. Use this button to export the highlighted key to a file (with a ‘.k4a' file name extension). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.</p>
|
||||
|
||||
<h3>Importing Existing Keyfiles:</h3>
|
||||
|
||||
<p>At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import any ‘.k4a’ file you obtained by using the androidkindlekey.py script manually, or by exporting from another copy of calibre.</p>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -9,8 +9,6 @@ __docformat__ = 'restructuredtext en'
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
#
|
||||
# Requires Calibre version 0.7.55 or higher.
|
||||
#
|
||||
# All credit given to i♥cabbages and The Dark Reverser for the original standalone scripts.
|
||||
# We had the much easier job of converting them to a calibre plugin.
|
||||
#
|
||||
@@ -36,13 +34,34 @@ __docformat__ = 'restructuredtext en'
|
||||
# 6.0.7 - Error handling for incomplete PDF metadata
|
||||
# 6.0.8 - Fixes a Wine key issue and topaz support
|
||||
# 6.0.9 - Ported to work with newer versions of Calibre (moved to Qt5). Still supports older Qt4 versions.
|
||||
# 6.1.0 - Fixed multiple books import problem and PDF import with no key problem
|
||||
# 6.2.0 - Support for getting B&N key from nook Study log. Fix for UTF-8 filenames in Adobe ePubs.
|
||||
# Fix for not copying needed files. Fix for getting default Adobe key for PDFs
|
||||
# 6.2.1 - Fix for non-ascii Windows user names
|
||||
# 6.2.2 - Added URL method for B&N/nook books
|
||||
# 6.3.0 - Added in Kindle for Android serial number solution
|
||||
# 6.3.1 - Version number bump for clarity
|
||||
# 6.3.2 - Fixed Kindle for Android help file
|
||||
# 6.3.3 - Bug fix for Kindle for PC support
|
||||
# 6.3.4 - Fixes for Kindle for Android, Linux, and Kobo 3.17
|
||||
# 6.3.5 - Fixes for Linux, and Kobo 3.19 and more logging
|
||||
# 6.3.6 - Fixes for ADE ePub and PDF introduced in 6.3.5
|
||||
# 6.4.0 - Updated for new Kindle for PC encryption
|
||||
# 6.4.1 - Fix for some new tags in Topaz ebooks.
|
||||
# 6.4.2 - Fix for more new tags in Topaz ebooks and very small Topaz ebooks
|
||||
# 6.4.3 - Fix for error that only appears when not in debug mode
|
||||
# Also includes fix for Macs with bonded ethernet ports
|
||||
# 6.5.0 - Big update to Macintosh app
|
||||
# Fix for some more 'new' tags in Topaz ebooks.
|
||||
# Fix an error in wineutils.py
|
||||
|
||||
|
||||
"""
|
||||
Decrypt DRMed ebooks.
|
||||
"""
|
||||
|
||||
PLUGIN_NAME = u"DeDRM"
|
||||
PLUGIN_VERSION_TUPLE = (6, 0, 9)
|
||||
PLUGIN_VERSION_TUPLE = (6, 5, 0)
|
||||
PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE])
|
||||
# Include an html helpfile in the plugin's zipfile with the following name.
|
||||
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
|
||||
@@ -74,8 +93,12 @@ class SafeUnbuffered:
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
try:
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
except:
|
||||
# We can do nothing if a write fails
|
||||
pass
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
@@ -83,37 +106,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'])
|
||||
minimum_calibre_version = (1, 0, 0) # Compiled python libraries cannot be imported in earlier versions.
|
||||
file_types = set(['epub','pdf','pdb','prc','mobi','pobi','azw','azw1','azw3','azw4','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)
|
||||
@@ -126,9 +139,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
|
||||
@@ -166,7 +208,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()
|
||||
|
||||
@@ -177,8 +224,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
|
||||
@@ -196,13 +304,19 @@ 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()
|
||||
try:
|
||||
of.close()
|
||||
except:
|
||||
print u"{0} v{1}: Exception closing temporary file after {2:.1f} seconds. Ignored.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
|
||||
|
||||
if result == 0:
|
||||
# Decryption was successful.
|
||||
# Return the modified PersistentTemporary file to calibre.
|
||||
print u"{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime)
|
||||
return of.name
|
||||
|
||||
print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime)
|
||||
@@ -226,6 +340,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""
|
||||
|
||||
@@ -244,6 +359,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()
|
||||
@@ -255,8 +372,11 @@ 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()
|
||||
print u"{0} v{1}: Decrypted with new default key after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
|
||||
# Return the modified PersistentTemporary file to calibre.
|
||||
return of.name
|
||||
|
||||
@@ -266,12 +386,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
|
||||
@@ -289,6 +409,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()
|
||||
@@ -298,67 +420,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 = 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):
|
||||
@@ -375,10 +503,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
|
||||
@@ -397,6 +530,8 @@ class DeDRM(FileTypePlugin):
|
||||
scriptpath = os.path.join(self.alfdir,u"kindlekey.py")
|
||||
defaultkeys = WineGetKeys(scriptpath, u".k4i",dedrmprefs['kindlewineprefix'])
|
||||
except:
|
||||
print u"{0} v{1}: Exception when getting default Kindle Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
|
||||
traceback.print_exc()
|
||||
pass
|
||||
|
||||
newkeys = {}
|
||||
@@ -407,7 +542,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")
|
||||
@@ -419,8 +554,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)
|
||||
@@ -449,12 +583,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):
|
||||
@@ -467,7 +602,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':
|
||||
@@ -484,7 +619,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):
|
||||
|
||||
75
DeDRM_calibre_plugin/DeDRM_plugin/activitybar.py
Normal file
75
DeDRM_calibre_plugin/DeDRM_plugin/activitybar.py
Normal 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)
|
||||
@@ -1,157 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#fileencoding: utf-8
|
||||
|
||||
import os
|
||||
import sys
|
||||
import zlib
|
||||
import tarfile
|
||||
from hashlib import md5
|
||||
from cStringIO import StringIO
|
||||
from binascii import a2b_hex, b2a_hex
|
||||
|
||||
STORAGE = 'AmazonSecureStorage.xml'
|
||||
|
||||
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_serials(path=None):
|
||||
''' get serials from android's shared preference xml '''
|
||||
if path is None:
|
||||
if not os.path.isfile(STORAGE):
|
||||
if os.path.isfile("backup.ab"):
|
||||
get_storage()
|
||||
else:
|
||||
return []
|
||||
path = STORAGE
|
||||
|
||||
if not os.path.isfile(path):
|
||||
return []
|
||||
|
||||
storage = parse_preference(path)
|
||||
salt = storage.get('AmazonSaltKey')
|
||||
if salt and len(salt) == 16:
|
||||
sys.stdout.write('Using AndroidObfuscationV2\n')
|
||||
obfuscation = AndroidObfuscationV2(a2b_hex(salt))
|
||||
else:
|
||||
sys.stdout.write('Using AndroidObfuscation\n')
|
||||
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 [dsnid]
|
||||
|
||||
serials = []
|
||||
for token in tokens:
|
||||
if token:
|
||||
serials.append('%s%s' % (dsnid, token))
|
||||
serials.append(dsnid)
|
||||
for token in tokens:
|
||||
if token:
|
||||
serials.append(token)
|
||||
return serials
|
||||
|
||||
def get_storage(path='backup.ab'):
|
||||
'''get AmazonSecureStorage.xml from android backup.ab
|
||||
backup.ab can be get using adb command:
|
||||
shell> adb backup com.amazon.kindle
|
||||
'''
|
||||
output = None
|
||||
read = open(path, 'rb')
|
||||
head = read.read(24)
|
||||
if head == 'ANDROID BACKUP\n1\n1\nnone\n':
|
||||
output = StringIO(zlib.decompress(read.read()))
|
||||
read.close()
|
||||
|
||||
if not output:
|
||||
return False
|
||||
|
||||
tar = tarfile.open(fileobj=output)
|
||||
for member in tar.getmembers():
|
||||
if member.name.strip().endswith(STORAGE):
|
||||
write = open(STORAGE, 'w')
|
||||
write.write(tar.extractfile(member).read())
|
||||
write.close()
|
||||
break
|
||||
|
||||
return True
|
||||
|
||||
__all__ = [ 'get_storage', 'get_serials', 'parse_preference',
|
||||
'AndroidObfuscation', 'AndroidObfuscationV2', 'STORAGE']
|
||||
|
||||
if __name__ == '__main__':
|
||||
print get_serials()
|
||||
@@ -1,6 +0,0 @@
|
||||
1.1 get AmazonSecureStorage.xml from /data/data/com.amazon.kindle/shared_prefs/AmazonSecureStorage.xml
|
||||
|
||||
1.2 on android 4.0+, run `adb backup com.amazon.kindle` from PC will get backup.ab
|
||||
now android.py can convert backup.ab to AmazonSecureStorage.xml
|
||||
|
||||
2. run `k4mobidedrm.py -a AmazonSecureStorage.xml <infile> <outdir>'
|
||||
468
DeDRM_calibre_plugin/DeDRM_plugin/androidkindlekey.py
Normal file
468
DeDRM_calibre_plugin/DeDRM_plugin/androidkindlekey.py
Normal 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())
|
||||
88
DeDRM_calibre_plugin/DeDRM_plugin/argv_utils.py
Normal file
88
DeDRM_calibre_plugin/DeDRM_plugin/argv_utils.py
Normal 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
|
||||
|
||||
|
||||
211
DeDRM_calibre_plugin/DeDRM_plugin/askfolder_ed.py
Normal file
211
DeDRM_calibre_plugin/DeDRM_plugin/askfolder_ed.py
Normal 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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -36,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):
|
||||
@@ -53,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']
|
||||
@@ -83,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")
|
||||
@@ -104,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)
|
||||
@@ -115,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:
|
||||
@@ -162,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'])
|
||||
@@ -188,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))
|
||||
@@ -219,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)
|
||||
|
||||
@@ -377,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):
|
||||
@@ -395,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:
|
||||
@@ -429,6 +445,10 @@ class ManageKeysDialog(QDialog):
|
||||
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])
|
||||
|
||||
@@ -515,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)
|
||||
@@ -536,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)
|
||||
@@ -550,6 +566,19 @@ class AddBandNKeyDialog(QDialog):
|
||||
data_group_box_layout.addWidget(ccn_disclaimer_label)
|
||||
layout.addSpacing(10)
|
||||
|
||||
key_group = QHBoxLayout()
|
||||
data_group_box_layout.addLayout(key_group)
|
||||
key_group.addWidget(QLabel(u"Retrieved key:", self))
|
||||
self.key_display = QLabel(u"", self)
|
||||
self.key_display.setToolTip(_(u"Click the Retrieve Key button to fetch your B&N encryption key from the B&N servers"))
|
||||
key_group.addWidget(self.key_display)
|
||||
self.retrieve_button = QtGui.QPushButton(self)
|
||||
self.retrieve_button.setToolTip(_(u"Click to retrieve your B&N encryption key from the B&N servers"))
|
||||
self.retrieve_button.setText(u"Retrieve Key")
|
||||
self.retrieve_button.clicked.connect(self.retrieve_key)
|
||||
key_group.addWidget(self.retrieve_button)
|
||||
layout.addSpacing(10)
|
||||
|
||||
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
self.button_box.accepted.connect(self.accept)
|
||||
self.button_box.rejected.connect(self.reject)
|
||||
@@ -563,8 +592,7 @@ class AddBandNKeyDialog(QDialog):
|
||||
|
||||
@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)
|
||||
return unicode(self.key_display.text()).strip()
|
||||
|
||||
@property
|
||||
def user_name(self):
|
||||
@@ -572,19 +600,28 @@ class AddBandNKeyDialog(QDialog):
|
||||
|
||||
@property
|
||||
def cc_number(self):
|
||||
return unicode(self.cc_ledit.text()).strip().replace(' ', '').replace('-','')
|
||||
return unicode(self.cc_ledit.text()).strip()
|
||||
|
||||
def retrieve_key(self):
|
||||
from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key
|
||||
fetched_key = fetch_bandn_key(self.user_name,self.cc_number)
|
||||
if fetched_key == "":
|
||||
errmsg = u"Could not retrieve key. Check username, password and intenet connectivity and try again."
|
||||
error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
else:
|
||||
self.key_display.setText(fetched_key)
|
||||
|
||||
def accept(self):
|
||||
if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace():
|
||||
errmsg = u"All fields are required!"
|
||||
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
if 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)
|
||||
if len(self.key_value) == 0:
|
||||
self.retrieve_key()
|
||||
if len(self.key_value) == 0:
|
||||
return
|
||||
QDialog.accept(self)
|
||||
|
||||
class AddEReaderDialog(QDialog):
|
||||
@@ -606,9 +643,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)
|
||||
@@ -707,9 +741,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)
|
||||
@@ -780,15 +812,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)
|
||||
@@ -834,9 +865,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)
|
||||
@@ -863,6 +891,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)
|
||||
@@ -882,9 +993,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)
|
||||
|
||||
@@ -164,6 +164,7 @@ class PageParser(object):
|
||||
self.id = os.path.basename(filename).replace('.dat','')
|
||||
self.dict = dict
|
||||
self.debug = debug
|
||||
self.first_unknown = True
|
||||
self.flat_xml = flat_xml
|
||||
self.tagpath = []
|
||||
self.doc = []
|
||||
@@ -262,6 +263,9 @@ class PageParser(object):
|
||||
'img.w' : (1, 'scalar_number', 0, 0),
|
||||
'img.src' : (1, 'scalar_number', 0, 0),
|
||||
'img.color_src' : (1, 'scalar_number', 0, 0),
|
||||
'img.gridSize' : (1, 'scalar_number', 0, 0),
|
||||
'img.gridBottomCenter' : (1, 'scalar_number', 0, 0),
|
||||
'img.gridTopCenter' : (1, 'scalar_number', 0, 0),
|
||||
'img.gridBeginCenter' : (1, 'scalar_number', 0, 0),
|
||||
'img.gridEndCenter' : (1, 'scalar_number', 0, 0),
|
||||
'img.image_type' : (1, 'scalar_number', 0, 0),
|
||||
@@ -313,11 +317,16 @@ class PageParser(object):
|
||||
'span.gridBeginCenter' : (1, 'scalar_number', 0, 0),
|
||||
'span.gridEndCenter' : (1, 'scalar_number', 0, 0),
|
||||
|
||||
'extratokens' : (1, 'snippets', 1, 0),
|
||||
'extratokens.class' : (1, 'scalar_text', 0, 0),
|
||||
'extratokens.type' : (1, 'scalar_text', 0, 0),
|
||||
'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0),
|
||||
'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0),
|
||||
'extratokens' : (1, 'snippets', 1, 0),
|
||||
'extratokens.class' : (1, 'scalar_text', 0, 0),
|
||||
'extratokens.type' : (1, 'scalar_text', 0, 0),
|
||||
'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0),
|
||||
'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0),
|
||||
'extratokens.gridSize' : (1, 'scalar_number', 0, 0),
|
||||
'extratokens.gridBottomCenter' : (1, 'scalar_number', 0, 0),
|
||||
'extratokens.gridTopCenter' : (1, 'scalar_number', 0, 0),
|
||||
'extratokens.gridBeginCenter' : (1, 'scalar_number', 0, 0),
|
||||
'extratokens.gridEndCenter' : (1, 'scalar_number', 0, 0),
|
||||
|
||||
'glyph.h' : (1, 'number', 0, 0),
|
||||
'glyph.w' : (1, 'number', 0, 0),
|
||||
@@ -382,6 +391,18 @@ class PageParser(object):
|
||||
'startID' : (0, 'number', 1, 1),
|
||||
'startID.page' : (1, 'number', 0, 0),
|
||||
'startID.id' : (1, 'number', 0, 0),
|
||||
|
||||
'median_d' : (1, 'number', 0, 0),
|
||||
'median_h' : (1, 'number', 0, 0),
|
||||
'median_firsty' : (1, 'number', 0, 0),
|
||||
'median_lasty' : (1, 'number', 0, 0),
|
||||
|
||||
'num_footers_maybe' : (1, 'number', 0, 0),
|
||||
'num_footers_yes' : (1, 'number', 0, 0),
|
||||
'num_headers_maybe' : (1, 'number', 0, 0),
|
||||
'num_headers_yes' : (1, 'number', 0, 0),
|
||||
|
||||
'tracking' : (1, 'number', 0, 0),
|
||||
|
||||
}
|
||||
|
||||
@@ -506,8 +527,9 @@ class PageParser(object):
|
||||
# or an out of sync condition
|
||||
else:
|
||||
result = []
|
||||
if (self.debug):
|
||||
if (self.debug or self.first_unknown):
|
||||
print 'Unknown Token:', token
|
||||
self.first_unknown = False
|
||||
self.tag_pop()
|
||||
return result
|
||||
|
||||
|
||||
@@ -385,7 +385,7 @@ def generateBook(bookDir, raw, fixedimage):
|
||||
# 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)
|
||||
pname = 'page%04d.dat' % (pnum - 1)
|
||||
fname = os.path.join(pageDir,pname)
|
||||
flat_xml = convert2xml.fromData(dict, fname)
|
||||
|
||||
|
||||
336
DeDRM_calibre_plugin/DeDRM_plugin/ignoblekey.py
Normal file
336
DeDRM_calibre_plugin/DeDRM_plugin/ignoblekey.py
Normal 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())
|
||||
258
DeDRM_calibre_plugin/DeDRM_plugin/ignoblekeyfetch.py
Normal file
258
DeDRM_calibre_plugin/DeDRM_plugin/ignoblekeyfetch.py
Normal 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())
|
||||
@@ -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()
|
||||
|
||||
@@ -3,18 +3,19 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ineptepub.pyw, version 6.1
|
||||
# ineptepub.pyw, version 6.5
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2013 by some_updates, DiapDealer and Apprentice Alf
|
||||
# Modified 2015–2016 by Apprentice Harper
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# Windows users: Before running this program, you must first install Python 2.7
|
||||
# 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
|
||||
# install the version for Python 2.7). Save this script file as
|
||||
# ineptepub.pyw and double-click on it to run it.
|
||||
#
|
||||
# Mac OS X users: Save this script file as ineptepub.pyw. You can run this
|
||||
@@ -37,13 +38,17 @@ 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
|
||||
# 6.3 - Add additional check on DER file sanity
|
||||
# 6.4 - Remove erroneous check on DER file sanity
|
||||
# 6.5 - Completely remove erroneous check on DER file sanity
|
||||
|
||||
"""
|
||||
Decrypt Adobe Digital Editions encrypted ePub books.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "6.1"
|
||||
__version__ = "6.5"
|
||||
|
||||
import sys
|
||||
import os
|
||||
@@ -366,7 +371,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)
|
||||
|
||||
@@ -3,18 +3,19 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ineptpdf.pyw, version 7.11
|
||||
# ineptpdf.pyw, version 8.0.4
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
# Modified 2015-2016 by Apprentice Harper
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# Windows users: Before running this program, you must first install Python 2.7
|
||||
# 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
|
||||
# install the version for Python 2.7). Save this script file as
|
||||
# ineptpdf.pyw and double-click on it to run it.
|
||||
#
|
||||
# Mac OS X users: Save this script file as ineptpdf.pyw. You can run this
|
||||
@@ -53,13 +54,17 @@ from __future__ import with_statement
|
||||
# 7.14 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 8.0 - Work if TkInter is missing
|
||||
# 8.0.1 - Broken Metadata fix.
|
||||
# 8.0.2 - Add additional check on DER file sanity
|
||||
# 8.0.3 - Remove erroneous check on DER file sanity
|
||||
# 8.0.4 - Completely remove erroneous check on DER file sanity
|
||||
|
||||
|
||||
"""
|
||||
Decrypts Adobe ADEPT-encrypted PDF files.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "8.0.1"
|
||||
__version__ = "8.0.4"
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
@@ -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,12 +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 android
|
||||
from calibre_plugins.dedrm import androidkindlekey
|
||||
else:
|
||||
import mobidedrm
|
||||
import topazextract
|
||||
import kgenpids
|
||||
import android
|
||||
import androidkindlekey
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
@@ -187,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.")
|
||||
@@ -207,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:
|
||||
@@ -223,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:
|
||||
@@ -239,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()
|
||||
@@ -254,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())
|
||||
@@ -275,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>] [ -a <AmazonSecureStorage.xml> ] <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
|
||||
@@ -298,6 +304,7 @@ def cli_main():
|
||||
infile = args[0]
|
||||
outdir = args[1]
|
||||
kDatabaseFiles = []
|
||||
androidFiles = []
|
||||
serials = []
|
||||
pids = []
|
||||
|
||||
@@ -316,14 +323,13 @@ def cli_main():
|
||||
serials = a.split(',')
|
||||
if o == '-a':
|
||||
if a == None:
|
||||
continue
|
||||
serials.extend(android.get_serials(a))
|
||||
serials.extend(android.get_serials())
|
||||
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__':
|
||||
|
||||
@@ -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,21 +197,26 @@ def getK4Pids(rec209, token, kindleDatabase):
|
||||
|
||||
try:
|
||||
# Get the Mazama Random number
|
||||
MazamaRandomNumber = (kindleDatabase[1])['MazamaRandomNumber'].decode('hex').encode('ascii')
|
||||
|
||||
# Get the kindle account token
|
||||
kindleAccountToken = (kindleDatabase[1])['kindle.account.tokens'].decode('hex').encode('ascii')
|
||||
MazamaRandomNumber = (kindleDatabase[1])['MazamaRandomNumber'].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])
|
||||
return pids
|
||||
|
||||
try:
|
||||
# Get the kindle account token, if present
|
||||
kindleAccountToken = (kindleDatabase[1])['kindle.account.tokens'].decode('hex')
|
||||
|
||||
except KeyError:
|
||||
kindleAccountToken=""
|
||||
pass
|
||||
|
||||
# Get the ID string used
|
||||
encodedIDString = encodeHash(IDString,charMap1)
|
||||
|
||||
|
||||
@@ -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-2016 by some_updates, Apprentice Alf and Apprentice Harper
|
||||
|
||||
# Revision history:
|
||||
# 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc.
|
||||
@@ -20,6 +18,10 @@ from __future__ import with_statement
|
||||
# 1.7 - Work if TkInter is missing
|
||||
# 1.8 - Fixes for Kindle for Mac, and non-ascii in Windows user names
|
||||
# 1.9 - Fixes for Unicode in Windows user names
|
||||
# 2.0 - Added comments and extra fix for non-ascii Windows user names
|
||||
# 2.1 - Fixed Kindle for PC encryption changes March 2016
|
||||
# 2.2 - Fixes for Macs with bonded ethernet ports
|
||||
# Also removed old .kinfo file support (pre-2011)
|
||||
|
||||
|
||||
"""
|
||||
@@ -27,7 +29,7 @@ Retrieve Kindle for PC/Mac user key.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '1.9'
|
||||
__version__ = '2.2'
|
||||
|
||||
import sys, os, re
|
||||
from struct import pack, unpack, unpack_from
|
||||
@@ -885,6 +887,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()
|
||||
@@ -926,7 +929,7 @@ if iswindows:
|
||||
# or the python interface to the 32 vs 64 bit registry is broken
|
||||
path = ""
|
||||
if 'LOCALAPPDATA' in os.environ.keys():
|
||||
# Python 2.x does not return unicode env. Use Python 3.x
|
||||
# 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')
|
||||
@@ -993,179 +996,120 @@ if iswindows:
|
||||
# determine type of kindle info provided and return a
|
||||
# database of keynames and values
|
||||
def getDBfromFile(kInfoFile):
|
||||
names = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber', 'max_date', 'SIGVERIF']
|
||||
names = [\
|
||||
'kindle.account.tokens',\
|
||||
'kindle.cookie.item',\
|
||||
'eulaVersionAccepted',\
|
||||
'login_date',\
|
||||
'kindle.token.item',\
|
||||
'login',\
|
||||
'kindle.key.item',\
|
||||
'kindle.name.info',\
|
||||
'kindle.device.info',\
|
||||
'MazamaRandomNumber',\
|
||||
'max_date',\
|
||||
'SIGVERIF',\
|
||||
'build_version',\
|
||||
]
|
||||
|
||||
DB = {}
|
||||
with open(kInfoFile, 'rb') as infoReader:
|
||||
hdr = infoReader.read(1)
|
||||
data = infoReader.read()
|
||||
# assume newest .kinf2011 style .kinf file
|
||||
# the .kinf file uses "/" to separate it into records
|
||||
# so remove the trailing "/" to make it easy to use split
|
||||
data = data[:-1]
|
||||
items = data.split('/')
|
||||
|
||||
if data.find('{') != -1 :
|
||||
# older style kindle-info file
|
||||
items = data.split('{')
|
||||
for item in items:
|
||||
if item != '':
|
||||
keyhash, rawdata = item.split(':')
|
||||
keyname = "unknown"
|
||||
for name in names:
|
||||
if encodeHash(name,charMap2) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
keyname = keyhash
|
||||
encryptedValue = decode(rawdata,charMap2)
|
||||
DB[keyname] = CryptUnprotectData(encryptedValue, "", 0)
|
||||
elif hdr == '/':
|
||||
# else rainier-2-1-1 .kinf file
|
||||
# the .kinf file uses "/" to separate it into records
|
||||
# so remove the trailing "/" to make it easy to use split
|
||||
data = data[:-1]
|
||||
items = data.split('/')
|
||||
# starts with an encoded and encrypted header blob
|
||||
headerblob = items.pop(0)
|
||||
encryptedValue = decode(headerblob, testMap1)
|
||||
cleartext = UnprotectHeaderData(encryptedValue)
|
||||
#print "header cleartext:",cleartext
|
||||
# now extract the pieces that form the added entropy
|
||||
pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
|
||||
for m in re.finditer(pattern, cleartext):
|
||||
added_entropy = m.group(2) + m.group(4)
|
||||
|
||||
# loop through the item records until all are processed
|
||||
while len(items) > 0:
|
||||
|
||||
# get the first item record
|
||||
# loop through the item records until all are processed
|
||||
while len(items) > 0:
|
||||
|
||||
# get the first item record
|
||||
item = items.pop(0)
|
||||
|
||||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
|
||||
# the sha1 of raw keyhash string is used to create entropy along
|
||||
# with the added entropy provided above from the headerblob
|
||||
entropy = SHA1(keyhash) + added_entropy
|
||||
|
||||
# the remainder of the first record when decoded with charMap5
|
||||
# has the ':' split char followed by the string representation
|
||||
# of the number of records that follow
|
||||
# and make up the contents
|
||||
srcnt = decode(item[34:],charMap5)
|
||||
rcnt = int(srcnt)
|
||||
|
||||
# read and store in rcnt records of data
|
||||
# that make up the contents value
|
||||
edlst = []
|
||||
for i in xrange(rcnt):
|
||||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
# key names now use the new testMap8 encoding
|
||||
keyname = "unknown"
|
||||
for name in names:
|
||||
if encodeHash(name,testMap8) == keyhash:
|
||||
keyname = name
|
||||
#print "keyname found from hash:",keyname
|
||||
break
|
||||
if keyname == "unknown":
|
||||
keyname = keyhash
|
||||
#print "keyname not found, hash is:",keyname
|
||||
|
||||
# the raw keyhash string is used to create entropy for the actual
|
||||
# CryptProtectData Blob that represents that keys contents
|
||||
entropy = SHA1(keyhash)
|
||||
# the testMap8 encoded contents data has had a length
|
||||
# of chars (always odd) cut off of the front and moved
|
||||
# to the end to prevent decoding using testMap8 from
|
||||
# working properly, and thereby preventing the ensuing
|
||||
# CryptUnprotectData call from succeeding.
|
||||
|
||||
# the remainder of the first record when decoded with charMap5
|
||||
# has the ':' split char followed by the string representation
|
||||
# of the number of records that follow
|
||||
# and make up the contents
|
||||
srcnt = decode(item[34:],charMap5)
|
||||
rcnt = int(srcnt)
|
||||
# The offset into the testMap8 encoded contents seems to be:
|
||||
# len(contents)-largest prime number <= int(len(content)/3)
|
||||
# (in other words split "about" 2/3rds of the way through)
|
||||
|
||||
# read and store in rcnt records of data
|
||||
# that make up the contents value
|
||||
edlst = []
|
||||
for i in xrange(rcnt):
|
||||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
keyname = "unknown"
|
||||
for name in names:
|
||||
if encodeHash(name,charMap5) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
keyname = keyhash
|
||||
# the charMap5 encoded contents data has had a length
|
||||
# of chars (always odd) cut off of the front and moved
|
||||
# to the end to prevent decoding using charMap5 from
|
||||
# working properly, and thereby preventing the ensuing
|
||||
# CryptUnprotectData call from succeeding.
|
||||
|
||||
# The offset into the charMap5 encoded contents seems to be:
|
||||
# len(contents)-largest prime number <= int(len(content)/3)
|
||||
# (in other words split "about" 2/3rds of the way through)
|
||||
|
||||
# move first offsets chars to end to align for decode by charMap5
|
||||
encdata = "".join(edlst)
|
||||
contlen = len(encdata)
|
||||
noffset = contlen - primes(int(contlen/3))[-1]
|
||||
|
||||
# now properly split and recombine
|
||||
# by moving noffset chars from the start of the
|
||||
# string to the end of the string
|
||||
pfx = encdata[0:noffset]
|
||||
encdata = encdata[noffset:]
|
||||
encdata = encdata + pfx
|
||||
|
||||
# decode using Map5 to get the CryptProtect Data
|
||||
encryptedValue = decode(encdata,charMap5)
|
||||
DB[keyname] = CryptUnprotectData(encryptedValue, entropy, 1)
|
||||
else:
|
||||
# else newest .kinf2011 style .kinf file
|
||||
# the .kinf file uses "/" to separate it into records
|
||||
# so remove the trailing "/" to make it easy to use split
|
||||
# need to put back the first char read because it it part
|
||||
# of the added entropy blob
|
||||
data = hdr + data[:-1]
|
||||
items = data.split('/')
|
||||
|
||||
# starts with and encoded and encrypted header blob
|
||||
headerblob = items.pop(0)
|
||||
encryptedValue = decode(headerblob, testMap1)
|
||||
cleartext = UnprotectHeaderData(encryptedValue)
|
||||
# now extract the pieces that form the added entropy
|
||||
pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
|
||||
for m in re.finditer(pattern, cleartext):
|
||||
added_entropy = m.group(2) + m.group(4)
|
||||
# move first offsets chars to end to align for decode by testMap8
|
||||
# by moving noffset chars from the start of the
|
||||
# string to the end of the string
|
||||
encdata = "".join(edlst)
|
||||
#print "encrypted data:",encdata
|
||||
contlen = len(encdata)
|
||||
noffset = contlen - primes(int(contlen/3))[-1]
|
||||
pfx = encdata[0:noffset]
|
||||
encdata = encdata[noffset:]
|
||||
encdata = encdata + pfx
|
||||
#print "rearranged data:",encdata
|
||||
|
||||
|
||||
# loop through the item records until all are processed
|
||||
while len(items) > 0:
|
||||
|
||||
# get the first item record
|
||||
item = items.pop(0)
|
||||
|
||||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
|
||||
# the sha1 of raw keyhash string is used to create entropy along
|
||||
# with the added entropy provided above from the headerblob
|
||||
entropy = SHA1(keyhash) + added_entropy
|
||||
|
||||
# the remainder of the first record when decoded with charMap5
|
||||
# has the ':' split char followed by the string representation
|
||||
# of the number of records that follow
|
||||
# and make up the contents
|
||||
srcnt = decode(item[34:],charMap5)
|
||||
rcnt = int(srcnt)
|
||||
|
||||
# read and store in rcnt records of data
|
||||
# that make up the contents value
|
||||
edlst = []
|
||||
for i in xrange(rcnt):
|
||||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
# key names now use the new testMap8 encoding
|
||||
keyname = "unknown"
|
||||
for name in names:
|
||||
if encodeHash(name,testMap8) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
|
||||
# the testMap8 encoded contents data has had a length
|
||||
# of chars (always odd) cut off of the front and moved
|
||||
# to the end to prevent decoding using testMap8 from
|
||||
# working properly, and thereby preventing the ensuing
|
||||
# CryptUnprotectData call from succeeding.
|
||||
|
||||
# The offset into the testMap8 encoded contents seems to be:
|
||||
# len(contents)-largest prime number <= int(len(content)/3)
|
||||
# (in other words split "about" 2/3rds of the way through)
|
||||
|
||||
# move first offsets chars to end to align for decode by testMap8
|
||||
# by moving noffset chars from the start of the
|
||||
# string to the end of the string
|
||||
encdata = "".join(edlst)
|
||||
contlen = len(encdata)
|
||||
noffset = contlen - primes(int(contlen/3))[-1]
|
||||
pfx = encdata[0:noffset]
|
||||
encdata = encdata[noffset:]
|
||||
encdata = encdata + pfx
|
||||
|
||||
# decode using new testMap8 to get the original CryptProtect Data
|
||||
encryptedValue = decode(encdata,testMap8)
|
||||
cleartext = CryptUnprotectData(encryptedValue, entropy, 1)
|
||||
# decode using new testMap8 to get the original CryptProtect Data
|
||||
encryptedValue = decode(encdata,testMap8)
|
||||
#print "decoded data:",encryptedValue.encode('hex')
|
||||
cleartext = CryptUnprotectData(encryptedValue, entropy, 1)
|
||||
if len(cleartext)>0:
|
||||
#print "cleartext data:",cleartext,":end data"
|
||||
DB[keyname] = cleartext
|
||||
#print keyname, cleartext
|
||||
|
||||
if '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 len(DB)>6:
|
||||
# store values used in decryption
|
||||
DB['IDString'] = GetIDString()
|
||||
DB['UserName'] = GetUserName()
|
||||
print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(GetIDString(), GetUserName().encode('hex'))
|
||||
else:
|
||||
print u"Couldn't decrypt file."
|
||||
DB = {}
|
||||
return DB
|
||||
elif isosx:
|
||||
@@ -1297,11 +1241,9 @@ elif isosx:
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
out1, out2 = p.communicate()
|
||||
#print out1
|
||||
reslst = out1.split('\n')
|
||||
cnt = len(reslst)
|
||||
bsdname = None
|
||||
sernum = None
|
||||
foundIt = False
|
||||
for j in xrange(cnt):
|
||||
resline = reslst[j]
|
||||
pp = resline.find('\"Serial Number\" = \"')
|
||||
@@ -1310,31 +1252,24 @@ elif isosx:
|
||||
sernums.append(sernum.strip())
|
||||
return sernums
|
||||
|
||||
def GetUserHomeAppSupKindleDirParitionName():
|
||||
home = os.getenv('HOME')
|
||||
dpath = home + '/Library'
|
||||
def GetDiskPartitionNames():
|
||||
names = []
|
||||
cmdline = '/sbin/mount'
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
out1, out2 = p.communicate()
|
||||
reslst = out1.split('\n')
|
||||
cnt = len(reslst)
|
||||
disk = ''
|
||||
foundIt = False
|
||||
for j in xrange(cnt):
|
||||
resline = reslst[j]
|
||||
if resline.startswith('/dev'):
|
||||
(devpart, mpath) = resline.split(' on ')
|
||||
dpart = devpart[5:]
|
||||
pp = mpath.find('(')
|
||||
if pp >= 0:
|
||||
mpath = mpath[:pp-1]
|
||||
if dpath.startswith(mpath):
|
||||
disk = dpart
|
||||
return disk
|
||||
names.append(dpart)
|
||||
return names
|
||||
|
||||
# uses a sub process to get the UUID of the specified disk partition using ioreg
|
||||
def GetDiskPartitionUUIDs(diskpart):
|
||||
# uses a sub process to get the UUID of all disk partitions
|
||||
def GetDiskPartitionUUIDs():
|
||||
uuids = []
|
||||
uuidnum = os.getenv('MYUUIDNUMBER')
|
||||
if uuidnum != None:
|
||||
@@ -1343,46 +1278,16 @@ elif isosx:
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
out1, out2 = p.communicate()
|
||||
#print out1
|
||||
reslst = out1.split('\n')
|
||||
cnt = len(reslst)
|
||||
bsdname = None
|
||||
uuidnum = None
|
||||
foundIt = False
|
||||
nest = 0
|
||||
uuidnest = -1
|
||||
partnest = -2
|
||||
for j in xrange(cnt):
|
||||
resline = reslst[j]
|
||||
if resline.find('{') >= 0:
|
||||
nest += 1
|
||||
if resline.find('}') >= 0:
|
||||
nest -= 1
|
||||
pp = resline.find('\"UUID\" = \"')
|
||||
if pp >= 0:
|
||||
uuidnum = resline[pp+10:-1]
|
||||
uuidnum = uuidnum.strip()
|
||||
uuidnest = nest
|
||||
if partnest == uuidnest and uuidnest > 0:
|
||||
foundIt = True
|
||||
break
|
||||
bb = resline.find('\"BSD Name\" = \"')
|
||||
if bb >= 0:
|
||||
bsdname = resline[bb+14:-1]
|
||||
bsdname = bsdname.strip()
|
||||
if (bsdname == diskpart):
|
||||
partnest = nest
|
||||
else :
|
||||
partnest = -2
|
||||
if partnest == uuidnest and partnest > 0:
|
||||
foundIt = True
|
||||
break
|
||||
if nest == 0:
|
||||
partnest = -2
|
||||
uuidnest = -1
|
||||
uuidnum = None
|
||||
bsdname = None
|
||||
if foundIt:
|
||||
uuids.append(uuidnum)
|
||||
uuids.append(uuidnum)
|
||||
return uuids
|
||||
|
||||
def GetMACAddressesMunged():
|
||||
@@ -1390,28 +1295,26 @@ elif isosx:
|
||||
macnum = os.getenv('MYMACNUM')
|
||||
if macnum != None:
|
||||
macnums.append(macnum)
|
||||
cmdline = '/sbin/ifconfig en0'
|
||||
cmdline = 'networksetup -listallhardwareports' # en0'
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
out1, out2 = p.communicate()
|
||||
reslst = out1.split('\n')
|
||||
cnt = len(reslst)
|
||||
macnum = None
|
||||
foundIt = False
|
||||
for j in xrange(cnt):
|
||||
resline = reslst[j]
|
||||
pp = resline.find('ether ')
|
||||
pp = resline.find('Ethernet Address: ')
|
||||
if pp >= 0:
|
||||
macnum = resline[pp+6:-1]
|
||||
#print resline
|
||||
macnum = resline[pp+18:]
|
||||
macnum = macnum.strip()
|
||||
# print 'original mac', macnum
|
||||
# now munge it up the way Kindle app does
|
||||
# by xoring it with 0xa5 and swapping elements 3 and 4
|
||||
maclst = macnum.split(':')
|
||||
n = len(maclst)
|
||||
if n != 6:
|
||||
fountIt = False
|
||||
break
|
||||
continue
|
||||
#print 'original mac', macnum
|
||||
# now munge it up the way Kindle app does
|
||||
# by xoring it with 0xa5 and swapping elements 3 and 4
|
||||
for i in range(6):
|
||||
maclst[i] = int('0x' + maclst[i], 0)
|
||||
mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
|
||||
@@ -1422,16 +1325,15 @@ elif isosx:
|
||||
mlst[1] = maclst[1] ^ 0xa5
|
||||
mlst[0] = maclst[0] ^ 0xa5
|
||||
macnum = '%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x' % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5])
|
||||
foundIt = True
|
||||
break
|
||||
if foundIt:
|
||||
macnums.append(macnum)
|
||||
#print 'munged mac', macnum
|
||||
macnums.append(macnum)
|
||||
return macnums
|
||||
|
||||
|
||||
# uses unix env to get username instead of using sysctlbyname
|
||||
def GetUserName():
|
||||
username = os.getenv('USER')
|
||||
#print "Username:",username
|
||||
return username
|
||||
|
||||
def GetIDStrings():
|
||||
@@ -1439,58 +1341,13 @@ elif isosx:
|
||||
strings = []
|
||||
strings.extend(GetMACAddressesMunged())
|
||||
strings.extend(GetVolumesSerialNumbers())
|
||||
diskpart = GetUserHomeAppSupKindleDirParitionName()
|
||||
strings.extend(GetDiskPartitionUUIDs(diskpart))
|
||||
strings.extend(GetDiskPartitionNames())
|
||||
strings.extend(GetDiskPartitionUUIDs())
|
||||
strings.append('9999999999')
|
||||
#print strings
|
||||
#print "ID Strings:\n",strings
|
||||
return strings
|
||||
|
||||
|
||||
# implements an Pseudo Mac Version of Windows built-in Crypto routine
|
||||
# used by Kindle for Mac versions < 1.6.0
|
||||
class CryptUnprotectData(object):
|
||||
def __init__(self, IDString):
|
||||
sp = IDString + '!@#' + GetUserName()
|
||||
passwdData = encode(SHA256(sp),charMap1)
|
||||
salt = '16743'
|
||||
self.crp = LibCrypto()
|
||||
iter = 0x3e8
|
||||
keylen = 0x80
|
||||
key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen)
|
||||
self.key = key_iv[0:32]
|
||||
self.iv = key_iv[32:48]
|
||||
self.crp.set_decrypt_key(self.key, self.iv)
|
||||
|
||||
def decrypt(self, encryptedData):
|
||||
cleartext = self.crp.decrypt(encryptedData)
|
||||
cleartext = decode(cleartext,charMap1)
|
||||
return cleartext
|
||||
|
||||
|
||||
# implements an Pseudo Mac Version of Windows built-in Crypto routine
|
||||
# used for Kindle for Mac Versions >= 1.6.0
|
||||
class CryptUnprotectDataV2(object):
|
||||
def __init__(self, IDString):
|
||||
sp = GetUserName() + ':&%:' + IDString
|
||||
passwdData = encode(SHA256(sp),charMap5)
|
||||
# salt generation as per the code
|
||||
salt = 0x0512981d * 2 * 1 * 1
|
||||
salt = str(salt) + GetUserName()
|
||||
salt = encode(salt,charMap5)
|
||||
self.crp = LibCrypto()
|
||||
iter = 0x800
|
||||
keylen = 0x400
|
||||
key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen)
|
||||
self.key = key_iv[0:32]
|
||||
self.iv = key_iv[32:48]
|
||||
self.crp.set_decrypt_key(self.key, self.iv)
|
||||
|
||||
def decrypt(self, encryptedData):
|
||||
cleartext = self.crp.decrypt(encryptedData)
|
||||
cleartext = decode(cleartext, charMap5)
|
||||
return cleartext
|
||||
|
||||
|
||||
# unprotect the new header blob in .kinf2011
|
||||
# used in Kindle for Mac Version >= 1.9.0
|
||||
def UnprotectHeaderData(encryptedData):
|
||||
@@ -1508,8 +1365,7 @@ elif isosx:
|
||||
|
||||
|
||||
# implements an Pseudo Mac Version of Windows built-in Crypto routine
|
||||
# used for Kindle for Mac Versions >= 1.9.0
|
||||
class CryptUnprotectDataV3(object):
|
||||
class CryptUnprotectData(object):
|
||||
def __init__(self, entropy, IDString):
|
||||
sp = GetUserName() + '+@#$%+' + IDString
|
||||
passwdData = encode(SHA256(sp),charMap2)
|
||||
@@ -1577,206 +1433,118 @@ elif isosx:
|
||||
# determine type of kindle info provided and return a
|
||||
# database of keynames and values
|
||||
def getDBfromFile(kInfoFile):
|
||||
names = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber', 'max_date', 'SIGVERIF']
|
||||
names = [\
|
||||
'kindle.account.tokens',\
|
||||
'kindle.cookie.item',\
|
||||
'eulaVersionAccepted',\
|
||||
'login_date',\
|
||||
'kindle.token.item',\
|
||||
'login',\
|
||||
'kindle.key.item',\
|
||||
'kindle.name.info',\
|
||||
'kindle.device.info',\
|
||||
'MazamaRandomNumber',\
|
||||
'max_date',\
|
||||
'SIGVERIF',\
|
||||
'build_version',\
|
||||
]
|
||||
with open(kInfoFile, 'rb') as infoReader:
|
||||
filehdr = infoReader.read(1)
|
||||
filedata = infoReader.read()
|
||||
|
||||
data = filedata[:-1]
|
||||
items = data.split('/')
|
||||
IDStrings = GetIDStrings()
|
||||
for IDString in IDStrings:
|
||||
DB = {}
|
||||
#print "trying IDString:",IDString
|
||||
try:
|
||||
hdr = filehdr
|
||||
data = filedata
|
||||
if data.find('[') != -1 :
|
||||
# older style kindle-info file
|
||||
cud = CryptUnprotectData(IDString)
|
||||
items = data.split('[')
|
||||
for item in items:
|
||||
if item != '':
|
||||
keyhash, rawdata = item.split(':')
|
||||
keyname = 'unknown'
|
||||
for name in names:
|
||||
if encodeHash(name,charMap2) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == 'unknown':
|
||||
keyname = keyhash
|
||||
encryptedValue = decode(rawdata,charMap2)
|
||||
cleartext = cud.decrypt(encryptedValue)
|
||||
if len(cleartext) > 0:
|
||||
DB[keyname] = cleartext
|
||||
if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB:
|
||||
break
|
||||
elif hdr == '/':
|
||||
# else newer style .kinf file used by K4Mac >= 1.6.0
|
||||
# the .kinf file uses '/' to separate it into records
|
||||
# so remove the trailing '/' to make it easy to use split
|
||||
data = data[:-1]
|
||||
items = data.split('/')
|
||||
cud = CryptUnprotectDataV2(IDString)
|
||||
DB = {}
|
||||
items = data.split('/')
|
||||
|
||||
# the headerblob is the encrypted information needed to build the entropy string
|
||||
headerblob = items.pop(0)
|
||||
encryptedValue = decode(headerblob, charMap1)
|
||||
cleartext = UnprotectHeaderData(encryptedValue)
|
||||
|
||||
# loop through the item records until all are processed
|
||||
while len(items) > 0:
|
||||
# now extract the pieces in the same way
|
||||
# this version is different from K4PC it scales the build number by multipying by 735
|
||||
pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
|
||||
for m in re.finditer(pattern, cleartext):
|
||||
entropy = str(int(m.group(2)) * 0x2df) + m.group(4)
|
||||
|
||||
# get the first item record
|
||||
cud = CryptUnprotectData(entropy,IDString)
|
||||
|
||||
# loop through the item records until all are processed
|
||||
while len(items) > 0:
|
||||
|
||||
# get the first item record
|
||||
item = items.pop(0)
|
||||
|
||||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
keyname = 'unknown'
|
||||
|
||||
# unlike K4PC the keyhash is not used in generating entropy
|
||||
# entropy = SHA1(keyhash) + added_entropy
|
||||
# entropy = added_entropy
|
||||
|
||||
# the remainder of the first record when decoded with charMap5
|
||||
# has the ':' split char followed by the string representation
|
||||
# of the number of records that follow
|
||||
# and make up the contents
|
||||
srcnt = decode(item[34:],charMap5)
|
||||
rcnt = int(srcnt)
|
||||
|
||||
# read and store in rcnt records of data
|
||||
# that make up the contents value
|
||||
edlst = []
|
||||
for i in xrange(rcnt):
|
||||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
keyname = 'unknown'
|
||||
keyname = 'unknown'
|
||||
for name in names:
|
||||
if encodeHash(name,testMap8) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == 'unknown':
|
||||
keyname = keyhash
|
||||
|
||||
# the raw keyhash string is also used to create entropy for the actual
|
||||
# CryptProtectData Blob that represents that keys contents
|
||||
# 'entropy' not used for K4Mac only K4PC
|
||||
# entropy = SHA1(keyhash)
|
||||
# the testMap8 encoded contents data has had a length
|
||||
# of chars (always odd) cut off of the front and moved
|
||||
# to the end to prevent decoding using testMap8 from
|
||||
# working properly, and thereby preventing the ensuing
|
||||
# CryptUnprotectData call from succeeding.
|
||||
|
||||
# the remainder of the first record when decoded with charMap5
|
||||
# has the ':' split char followed by the string representation
|
||||
# of the number of records that follow
|
||||
# and make up the contents
|
||||
srcnt = decode(item[34:],charMap5)
|
||||
rcnt = int(srcnt)
|
||||
# The offset into the testMap8 encoded contents seems to be:
|
||||
# len(contents) - largest prime number less than or equal to int(len(content)/3)
|
||||
# (in other words split 'about' 2/3rds of the way through)
|
||||
|
||||
# read and store in rcnt records of data
|
||||
# that make up the contents value
|
||||
edlst = []
|
||||
for i in xrange(rcnt):
|
||||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
# move first offsets chars to end to align for decode by testMap8
|
||||
encdata = ''.join(edlst)
|
||||
contlen = len(encdata)
|
||||
|
||||
keyname = 'unknown'
|
||||
for name in names:
|
||||
if encodeHash(name,charMap5) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == 'unknown':
|
||||
keyname = keyhash
|
||||
# now properly split and recombine
|
||||
# by moving noffset chars from the start of the
|
||||
# string to the end of the string
|
||||
noffset = contlen - primes(int(contlen/3))[-1]
|
||||
pfx = encdata[0:noffset]
|
||||
encdata = encdata[noffset:]
|
||||
encdata = encdata + pfx
|
||||
|
||||
# the charMap5 encoded contents data has had a length
|
||||
# of chars (always odd) cut off of the front and moved
|
||||
# to the end to prevent decoding using charMap5 from
|
||||
# working properly, and thereby preventing the ensuing
|
||||
# CryptUnprotectData call from succeeding.
|
||||
# decode using testMap8 to get the CryptProtect Data
|
||||
encryptedValue = decode(encdata,testMap8)
|
||||
cleartext = cud.decrypt(encryptedValue)
|
||||
# print keyname
|
||||
# print cleartext
|
||||
if len(cleartext) > 0:
|
||||
DB[keyname] = cleartext
|
||||
|
||||
# The offset into the charMap5 encoded contents seems to be:
|
||||
# len(contents) - largest prime number less than or equal to int(len(content)/3)
|
||||
# (in other words split 'about' 2/3rds of the way through)
|
||||
|
||||
# move first offsets chars to end to align for decode by charMap5
|
||||
encdata = ''.join(edlst)
|
||||
contlen = len(encdata)
|
||||
|
||||
# now properly split and recombine
|
||||
# by moving noffset chars from the start of the
|
||||
# string to the end of the string
|
||||
noffset = contlen - primes(int(contlen/3))[-1]
|
||||
pfx = encdata[0:noffset]
|
||||
encdata = encdata[noffset:]
|
||||
encdata = encdata + pfx
|
||||
|
||||
# decode using charMap5 to get the CryptProtect Data
|
||||
encryptedValue = decode(encdata,charMap5)
|
||||
cleartext = cud.decrypt(encryptedValue)
|
||||
if len(cleartext) > 0:
|
||||
DB[keyname] = cleartext
|
||||
|
||||
if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB:
|
||||
break
|
||||
else:
|
||||
# the latest .kinf2011 version for K4M 1.9.1
|
||||
# put back the hdr char, it is needed
|
||||
data = hdr + data
|
||||
data = data[:-1]
|
||||
items = data.split('/')
|
||||
|
||||
# the headerblob is the encrypted information needed to build the entropy string
|
||||
headerblob = items.pop(0)
|
||||
encryptedValue = decode(headerblob, charMap1)
|
||||
cleartext = UnprotectHeaderData(encryptedValue)
|
||||
|
||||
# now extract the pieces in the same way
|
||||
# this version is different from K4PC it scales the build number by multipying by 735
|
||||
pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
|
||||
for m in re.finditer(pattern, cleartext):
|
||||
entropy = str(int(m.group(2)) * 0x2df) + m.group(4)
|
||||
|
||||
cud = CryptUnprotectDataV3(entropy,IDString)
|
||||
|
||||
# loop through the item records until all are processed
|
||||
while len(items) > 0:
|
||||
|
||||
# get the first item record
|
||||
item = items.pop(0)
|
||||
|
||||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
keyname = 'unknown'
|
||||
|
||||
# unlike K4PC the keyhash is not used in generating entropy
|
||||
# entropy = SHA1(keyhash) + added_entropy
|
||||
# entropy = added_entropy
|
||||
|
||||
# the remainder of the first record when decoded with charMap5
|
||||
# has the ':' split char followed by the string representation
|
||||
# of the number of records that follow
|
||||
# and make up the contents
|
||||
srcnt = decode(item[34:],charMap5)
|
||||
rcnt = int(srcnt)
|
||||
|
||||
# read and store in rcnt records of data
|
||||
# that make up the contents value
|
||||
edlst = []
|
||||
for i in xrange(rcnt):
|
||||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
keyname = 'unknown'
|
||||
for name in names:
|
||||
if encodeHash(name,testMap8) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == 'unknown':
|
||||
keyname = keyhash
|
||||
|
||||
# the testMap8 encoded contents data has had a length
|
||||
# of chars (always odd) cut off of the front and moved
|
||||
# to the end to prevent decoding using testMap8 from
|
||||
# working properly, and thereby preventing the ensuing
|
||||
# CryptUnprotectData call from succeeding.
|
||||
|
||||
# The offset into the testMap8 encoded contents seems to be:
|
||||
# len(contents) - largest prime number less than or equal to int(len(content)/3)
|
||||
# (in other words split 'about' 2/3rds of the way through)
|
||||
|
||||
# move first offsets chars to end to align for decode by testMap8
|
||||
encdata = ''.join(edlst)
|
||||
contlen = len(encdata)
|
||||
|
||||
# now properly split and recombine
|
||||
# by moving noffset chars from the start of the
|
||||
# string to the end of the string
|
||||
noffset = contlen - primes(int(contlen/3))[-1]
|
||||
pfx = encdata[0:noffset]
|
||||
encdata = encdata[noffset:]
|
||||
encdata = encdata + pfx
|
||||
|
||||
# decode using testMap8 to get the CryptProtect Data
|
||||
encryptedValue = decode(encdata,testMap8)
|
||||
cleartext = cud.decrypt(encryptedValue)
|
||||
# print keyname
|
||||
# print cleartext
|
||||
if len(cleartext) > 0:
|
||||
DB[keyname] = cleartext
|
||||
|
||||
if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB:
|
||||
break
|
||||
if len(DB)>6:
|
||||
break
|
||||
except:
|
||||
pass
|
||||
if 'kindle.account.tokens' in DB:
|
||||
if len(DB)>6:
|
||||
# store values used in decryption
|
||||
print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString, GetUserName())
|
||||
DB['IDString'] = IDString
|
||||
@@ -1840,7 +1608,7 @@ def cli_main():
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
argv=unicode_argv()
|
||||
progname = os.path.basename(argv[0])
|
||||
print u"{0} v{1}\nCopyright © 2010-2013 some_updates and Apprentice Alf".format(progname,__version__)
|
||||
print u"{0} v{1}\nCopyright © 2010-2016 by some_updates, Apprentice Alf and Apprentice Harper".format(progname,__version__)
|
||||
|
||||
try:
|
||||
opts, args = getopt.getopt(argv[1:], "hk:")
|
||||
@@ -1870,7 +1638,7 @@ def cli_main():
|
||||
# save to the same directory as the script
|
||||
outpath = os.path.dirname(argv[0])
|
||||
|
||||
# make sure the outpath is the
|
||||
# make sure the outpath is canonical
|
||||
outpath = os.path.realpath(os.path.normpath(outpath))
|
||||
|
||||
if not getkey(outpath, files):
|
||||
|
||||
@@ -448,6 +448,8 @@ class MobiBook:
|
||||
goodpids.append(pid[0:-2])
|
||||
elif len(pid)==8:
|
||||
goodpids.append(pid)
|
||||
else:
|
||||
print u"Warning: PID {0} has wrong number of digits".format(pid)
|
||||
|
||||
if self.crypto_type == 1:
|
||||
t1_keyvec = 'QDCVEPMU675RUBSZ'
|
||||
@@ -530,7 +532,7 @@ def cli_main():
|
||||
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])
|
||||
print u"MobiDeDRM v{0} Error: {1:s}".format(__version__,e.args[0])
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
@@ -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'] == []:
|
||||
|
||||
198
DeDRM_calibre_plugin/DeDRM_plugin/scriptinterface.py
Normal file
198
DeDRM_calibre_plugin/DeDRM_plugin/scriptinterface.py
Normal 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
|
||||
27
DeDRM_calibre_plugin/DeDRM_plugin/scrolltextwidget.py
Normal file
27
DeDRM_calibre_plugin/DeDRM_plugin/scrolltextwidget.py
Normal 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)
|
||||
77
DeDRM_calibre_plugin/DeDRM_plugin/simpleprefs.py
Normal file
77
DeDRM_calibre_plugin/DeDRM_plugin/simpleprefs.py
Normal 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
|
||||
@@ -26,11 +26,12 @@ def WineGetKeys(scriptpath, extension, wineprefix=""):
|
||||
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)
|
||||
print u"{0} v{1}: Command line: '{2}'".format(PLUGIN_NAME, PLUGIN_VERSION, cmdline)
|
||||
|
||||
try:
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
@@ -38,8 +39,20 @@ def WineGetKeys(scriptpath, extension, wineprefix=""):
|
||||
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 []
|
||||
if wineprefix != "" and os.path.exists(wineprefix):
|
||||
cmdline = u"WINEPREFIX=\"{2}\" wine C:\\Python27\\python.exe \"{0}\" \"{1}\"".format(scriptpath,outdirpath,wineprefix)
|
||||
else:
|
||||
cmdline = u"wine C:\\Python27\\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])
|
||||
|
||||
# try finding winekeys anyway, even if above code errored
|
||||
winekeys = []
|
||||
# get any files with extension in the output dir
|
||||
files = [f for f in os.listdir(outdirpath) if f.endswith(extension)]
|
||||
|
||||
@@ -1,23 +1,43 @@
|
||||
DeDRM_plugin.zip
|
||||
DeDRM_plugin.zip
|
||||
================
|
||||
|
||||
This calibre plugin replaces all previous DRM removal plugins. When you install this plugin, the older separate plugins should be removed.
|
||||
This calibre plugin replaces many previously separate DRM removal plugins. Before you install this plugin, you should uninstall any older individual DRM removal plugins, e.g. K4MobiDeDRM. The exception is the obok plugin, which should not be removed.
|
||||
|
||||
This plugin will remove the DRM from Amazon Kindle ebooks (Mobi, KF8, Topaz and Print Replica), Mobipocket, Adobe Digital Edition ePubs (including Sony and Kobo ePubs), Barnes and Noble ePubs, Adobe Digital Edition PDFs, and Fictionwise eReader ebooks.
|
||||
This plugin will remove the DRM from
|
||||
- Kindle ebooks (from Kindle for Mac/PC and eInk Kindles).
|
||||
- Barnes and Noble ePubs
|
||||
- Adobe Digital Editions (v2.0.1*) ePubs (including Kobo and Google ePubs downloaded to ADE)
|
||||
- Adobe Digital Editions (v2.0.1) PDFs
|
||||
- Mobipocket ebooks
|
||||
- eReader PDB books
|
||||
|
||||
These tools do NOT work with kepubs downloaded using Kobo's desktop app (see the separate obok plugin) nor Apple's iBooks FairPlay DRM (see details about Requiem at the end of this file.)
|
||||
|
||||
* With Adobe Digital Editions 3.0 and later, Adobe have introduced a new, optional, DRM scheme. To avoid this new scheme, you should use Adobe Digital Editions 2.0.1. Some books are required to use the new DRM scheme and so will not download with ADE 2.0.1. If you still want such a book, you will need to use ADE 3.0 or later to download it, but you should remember that no tools to remove Adobe's new DRM scheme exist as of June 2016.
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
Do NOT select "Get plugins to enhance calibre" as this is reserved for 'official' calibre plugins, instead select "Change calibre behavior" to go to Calibre's Preferences page. Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (DeDRM_plugin.zip) and click the "Add" button. Click "Yes" in the the "Are you sure?" dialog. Click OK in the "Success" dialog.
|
||||
Open calibre's Preferences dialog. Click on the "Plugins" button. Next, click on the button, "Load plugin from file". Navigate to the unzipped DeDRM_tools folder and, in the folder "DeDRM_calibre_plugin", find the file "DeDRM_plugin.zip". Click to select the file and select "Open". Click "Yes" in the "Are you sure?" dialog box. Click the "OK" button in the "Success" dialog box.
|
||||
|
||||
|
||||
Customization
|
||||
-------------
|
||||
The keys for ebooks downloaded using Kindle for Mac/PC and Adobe Digital Editions are automatically generated and saved when needed. If all your DRMed ebooks can be opened and read in Kindle for Mac/PC or Adobe Digital Editions on the same computer on which you are running calibre, you do not need to do any configuration of this plugin. (On Linux, Kindle for PC and Adobe Digital Editions along with Python and PyCrypto need to be installed under Wine for this to work, see the Linux section at the end.)
|
||||
You MUST add some key information for the following kinds of ebooks:
|
||||
- Kindle ebooks from an E-Ink based Kindle (e.g. Voyage).
|
||||
- Barnes & Noble ePubs other than those downloaded using NOOK Study
|
||||
- Mobipocket ebooks
|
||||
- eReader PDB books
|
||||
|
||||
You do not need to add any key information for eBooks
|
||||
- downloaded using Kindle for Mac/PC
|
||||
- downloaded using Adobe Digital Editions (v2.0.1)
|
||||
- downloaded using NOOK Study
|
||||
as the necessary keys are automatically retrieved from files on your computer.
|
||||
|
||||
If you have books from other sources (e.g. from an eInk Kindle), highlight the plugin (DeDRM under the "File type plugins" category) and click the "Customize Plugin" button.
|
||||
To add needed key information for other books, highlight the plugin (DeDRM under the "File type plugins" category) and click the "Customize Plugin" button.
|
||||
|
||||
The buttons in the configuration dialog will open individual configuration dialogs that will allow you to enter the needed information, depending on the type and source of your DRMed eBooks. Additional help on the information required is available in each of the the dialogs.
|
||||
The buttons in the configuration dialog will open individual configuration dialogs that will allow you to enter the needed information, depending on the type and source of your DRMed eBooks. Additional help on the information required is available in each of the the dialogs vias the [?] help button.
|
||||
|
||||
If you have used previous versions of the various DeDRM plugins on this machine, you may find that some of the configuration dialogs already contain the information you entered through those previous plugins.
|
||||
|
||||
@@ -26,40 +46,30 @@ When you have finished entering your configuration information, you must click t
|
||||
|
||||
Troubleshooting
|
||||
---------------
|
||||
If you find that it's not working for you (imported ebooks still have DRM), you can save a lot of time and trouble by deleting the DRMed ebook from calibre and then trying to add the ebook to calibre in debug mode with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests.
|
||||
If you find that the DeDRM plugin is not working for you (imported ebooks still have DRM - that is, they won't convert or open in the calibre ebook viewer), you should make a log of the import process by deleting the DRMed ebook from calibre and then adding the ebook to calibre when it's running in debug mode. This will generate a lot of helpful debugging info that can be copied into any online help requests. Here's how to do it:
|
||||
|
||||
On Macintosh only you must first run calibre, open Preferences, open Miscellaneous, and click on the “Install command line tools” button. (On Windows and Linux the command line tools are installed automatically.)
|
||||
- Remove the DRMed book from calibre.
|
||||
- Click the Preferences drop-down menu and choose 'Restart in debug mode'.
|
||||
- Once calibre has re-started, import the problem ebook.
|
||||
- Now close calibre.
|
||||
|
||||
On Windows, open a terminal/command window. (Start/Run… and then type 'cmd' (without the 's) as the program to run).
|
||||
On Macintosh, open the Terminal application (in your Utilities folder).
|
||||
On Linux open a command window. Hopefully all Linux users know how to do this.
|
||||
|
||||
You should now have a text-based command-line window open.
|
||||
|
||||
Type in "calibre-debug -g" (without the ") and press the return/enter key. Calibre will launch and run as normal, but with debugging information output to the terminal window.
|
||||
|
||||
Import the drmed eBook into calibre in any of the the normal ways. (I usually drag&drop onto the calibre window.)
|
||||
|
||||
More debug information will be written to the terminal window.
|
||||
|
||||
Copy the output from the terminal window.
|
||||
On Windows, you must use the window menu (little icon at left of window bar) to select all the text and then to copy it.
|
||||
On Macintosh and Linux, just use the normal text select and copy commands.
|
||||
|
||||
Paste the information into a comment at my blog, http://apprenticealf.wordpress.com/ describing your problem.
|
||||
A log will appear that you can copy and paste into a comment at Apprentice Alf's blog, http://apprenticealf.wordpress.com/ . You should also give details of your computer, and how you obtained the ebook file.
|
||||
|
||||
|
||||
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 original obok script was by Physisticated
|
||||
|
||||
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 plugin was based on plugins by DiapDealer 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.
|
||||
|
||||
|
||||
Linux Systems Only
|
||||
@@ -90,6 +100,6 @@ These instructions have been tested with Wine 1.4 on Ubuntu.
|
||||
Instructions for getting Kindle for PC and Adobe Digital Editions default decryption keys
|
||||
-----------------------------------------------------------------------------------------
|
||||
|
||||
If everything has been installed in wine as above, the keys will be retrieve automatically.
|
||||
If everything has been installed in wine as above, the keys will be retrieved automatically.
|
||||
|
||||
If you have a more complex wine installation, you may enter the appropriate WINEPREFIX in the configuration dialogs for Kindle for PC and Adobe Digital Editions. You can also test that you have entered the WINEPREFIX correctly by trying to add the default keys to the preferences by clicking on the green plus button in the configuration dialogs.
|
||||
|
||||
185
FAQs.md
Normal file
185
FAQs.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# Overview
|
||||
## What's this repository all about?
|
||||
Ebooks and DRM. That is, providing free tools to remove DRM from most ebooks.
|
||||
|
||||
## What's DRM?
|
||||
DRM ("Digital Rights Management") is a way of using encryption to tie the books you've bought to a specific device or to a particular piece of software.
|
||||
|
||||
## Why would I want to remove DRM from my ebooks?
|
||||
When your ebooks have DRM you are unable to convert the ebook from one format to another (e.g. Kindle KF8 to Kobo ePub), so you are restricted in the range of ebook stores you can use. DRM also allows publishers to restrict what you can do with the ebook you've bought, e.g. preventing the use of text-to-speech software. Longer term, you can never be sure that you'll be able to come back and re-read your ebooks if they have DRM, even if you save back-up copies.
|
||||
|
||||
## So how can I remove DRM from my ebooks?
|
||||
Just download and use these tools, that's all! Uh, almost. There are a few, uh, provisos, a, a couple of quid pro quos.
|
||||
* The tools don't work on all ebooks. For example, they don't work on any ebooks from Apple's iBooks store.
|
||||
* You must own the ebook - the tools won't work on library ebooks or rented ebooks or books from a friend.
|
||||
* You must not use these tools to give your ebooks to a hundred of your closest friends. Or to a million strangers. Authors need to sell books to be able to write more books. Don't be mean to the authors.
|
||||
* Do NOT use Adobe Digital Editions 3.0 or later to download your ePubs. ADE 3.0 and later can use a new encryption scheme that the tools can't handle. While major ebook stores aren't using the new scheme yet, using ADE 2.0.1 will ensure that your ebooks are downloaded using the old scheme. Once a book has been downloaded with the new scheme, it's IMPOSSIBLE to re-download using the old scheme (without buying it again).
|
||||
|
||||
But otherwise, if your ebook is from Amazon, Kobo, Barnes & Noble or any of the ebook stores selling ebooks compatible with Adobe Digital Editions 2.0.1, you should be able to free your books from DRM.
|
||||
|
||||
## Where can I get the latest version of these free DRM removal tools?
|
||||
Right here at github. Just go to the [releases page](https://github.com/apprenticeharper/DeDRM_tools/releases) and download the latest zip archive of the tools, named DeDRM\_tools\_X.X.X.zip, where X.X.X is the version number. You do not need to download the source code archive.
|
||||
|
||||
## I've downloaded the tools archive. Now what?
|
||||
First, unzip the archive. You should now have a DeDRM folder containing several other folders and a ReadMe\_First.txt file. Please read the ReadMe\_First file! That will explain what the folders are, and you'll be able to work out which of the tools you need.
|
||||
|
||||
## That's a big complicated ReadMe file! Isn't there a quick guide?
|
||||
Install calibre. Install the DeDRM\_plugin in calibre. Install the Obok\_plugin in calibre. Restart calibre. In the DeDRM_plugin customisation dialog add in any E-Ink Kindle serial numbers and your B&N account email address and password. Remember that the plugin only tries to remove DRM when ebooks are imported, not at any other time.
|
||||
|
||||
# Installing the Tools
|
||||
## The calibre plugin
|
||||
### I am trying to install the calibre plugin, but calibre says "ERROR: Unhandled exception: InvalidPlugin: The plugin in u’[path]DeDRM\_tools\_6.4.3.zip’ is invalid. It does not contain a top-level \_\_init\_\_.py file"
|
||||
You are trying to add the tools archive (e.g. DeDRM\_tools\_6.4.3.zip) instead of the plugin. The tools archive is not the plugin. It is a collection of DRM removal tools which includes the plugin. You must unzip the archive, and install the calibre plugin (DeDRM\_plugin.zip) from a folder called “DeDRM\_calibre_plugin” in the unzipped archive.
|
||||
|
||||
### I’ve unzipped the tools archive, but I can’t find the calibre plugin when I try to add them to calibre. I use Windows.
|
||||
You should select the zip file that is in the “DeDRM\_calibre\_plugin” folder, not any files inside the plugin’s zip archive. Make sure you are selecting from the folder that you created when you unzipped the tools archive and not selecting a file inside the still-zipped tools archive.
|
||||
|
||||
(The problem is that Windows will allow apps to browse inside zip archives without needing to unzip them first. If there are zip archives inside the main zip archives, Windows will show them as unzipped as well. So what happens is people will unzip the DeDRM\_tools\_X.X.X.zip to a folder, but when using calibre they will actually navigate to the still zipped file by mistake and cannot tell they have done so because they do not have file extensions showing. So to the unwary Windows user, it appears that the zip archive was unzipped and that everything inside it was unzipped as well so there is no way to install the plugins.
|
||||
|
||||
We strongly recommend renaming the DeDRM\_tools\_X.X.X.zip archive (after extracting its contents) to DeDRM\_tools\_X.X.X_archive.zip. If you do that, you are less likely to navigate to the wrong location from inside calibre.)
|
||||
|
||||
## The Windows Application
|
||||
### I've installed ActiveState Python and PyCrypto, but the Windows application won't run. What have I done wrong?
|
||||
Nothing. There's a bug in the current ActiveState Python Windows installer that puts the Tcl code in the wrong place. See [this comment of mine at ActiveState community](https://community.activestate.com/node/19090). Just move the Tcl code and the Windows app should run.
|
||||
|
||||
## The Macintosh Application
|
||||
### I can't open the Macintosh Application. Some message about it not being signed or something.
|
||||
Try right-clicking and select open. That might give you the option to open it anyway. Otherwise you'll need to change your security settings to allow unsigned applications to run. You can probably change these back after running it for the first time.
|
||||
|
||||
# Using the Tools
|
||||
## I can’t get the tools to work on my rented or library ebooks.
|
||||
Support for removing DRM from rented or library ebooks is not provided.
|
||||
|
||||
## I've unzipped the tools, but what are all the different files, and how do I use them?
|
||||
Read the ReadMe_First.txt file and the ReadMe files included in the tools folder. That's what they're for.
|
||||
|
||||
## I have installed the calibre plugin, but my books still have DRM. When I try to view or convert my books, calibre says they have DRM.
|
||||
The DRM only gets removed when the ebook is imported into calibre. If the book is already in calibre, by default calibre will discard the newly imported file. You can change this in calibre's Adding books preferences page (Automerge..../Overwrite....), so that newly imported files overwrite existing ebook formats. Then just re-import your books and the DRM-free versions will overwrite the DRMed versions while retaining your books' metadata.
|
||||
|
||||
## I have installed the calibre plugin or I am trying to use one of the other tools, but I don’t know where my ebooks are stored.
|
||||
Your ebooks are stored on your computer or on your ebook reader. You need to find them to be able to remove the DRM. If they are on your reader, you should be able to locate them easily. On your computer it’s not so obvious. Here are the default locations.
|
||||
|
||||
### Macintosh
|
||||
Navigating from your home folder,
|
||||
|
||||
Kindle for Mac ebooks are in either Library/Application Support/Kindle/My Kindle Content or Documents/My Kindle Content or Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/My Kindle Content, depending on your version of Kindle for Mac.
|
||||
|
||||
Adobe Digital Editions ebooks are in Documents/Digital Editions
|
||||
|
||||
### Windows
|
||||
Navigating from your My Documents folder
|
||||
|
||||
Kindle for PC ebooks are in My Kindle Content
|
||||
|
||||
Adobe Digital Editions ebooks are in My Digital Editions
|
||||
|
||||
|
||||
## I have installed the calibre plugin, and the book is not already in calibre, but the DRM does not get removed.
|
||||
You must use the exact file that is used by your ebook reading software or hardware. See the previous question on where to find your ebook files. Do not use an old copy you have that you can no longer read.
|
||||
If you cannot read the ebook on your current device or installed software, the tools will certainly not be able to remove the DRM. Download a fresh copy that does work with your current device or installed software.
|
||||
|
||||
## I have installed the calibre plugin, and the book is not already in calibre, but the DRM does not get removed. It is a Kindle book.
|
||||
If you are on Windows 8 and using the Windows 8 AppStore Kindle app, you must download and install the Kindle for PC application directly from the Amazon website. The tools do not work with the Windows 8 AppStore Kindle app.
|
||||
|
||||
If you are on Windows, using the Kindle for PC application, and your windows user name has accented or other non-ASCII characters in it, this might cause the plugin to fail. Create a new user account with an ASCII-only username. Install and register Kindle for PC there, and see if things work running calibre and the plugin in that user account.
|
||||
|
||||
If this book is from an eInk Kindle, you must enter the serial number into the configuration dialog. The serial number is sixteen characters long, and is case-sensitive.
|
||||
|
||||
If this book is from Kindle for Mac or Kindle for PC, you must have the Kindle Software installed on the same computer as your copy of calibre.
|
||||
|
||||
If the book is from Kindle for PC or Kindle for Mac and you think you are doing everything right, and you are getting this message, it is possible that the files containing the encryption key aren’t quite in the format the tools expect. To try to fix this:
|
||||
|
||||
1. Deregister Kindle for PC(Mac) from your Amazon account.
|
||||
1. Uninstall Kindle for PC(Mac)
|
||||
1. Delete the Kindle for PC(Mac) preferences
|
||||
* PC: Delete the directory [home folder]\AppData\Local\Amazon (it might be hidden) and [home folder]\My Documents\My Kindle Content
|
||||
* Mac: Delete the directory [home folder]/Library/Application Support/Kindle/and/or [home folder]/Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/ (one or both may be present and should be deleted)
|
||||
1. Reinstall Kindle for PC(Mac)
|
||||
1. Re-register Kindle for PC(Mac) with your Amazon account
|
||||
1. Download the ebook again. Do not use the files you have downloaded previously.
|
||||
|
||||
## Some of my books had their DRM removed, but some still say that they have DRM and will not convert.
|
||||
There are several possible reasons why only some books get their DRM removed.
|
||||
* You still don’t have the DRM removal tools working correctly, but some of your books didn’t have DRM in the first place.
|
||||
* You added your books to calibre in large batches. For some unknown reason, sometimes calibre stops using the plugin after a while on a single import. Delete the books that still have DRM and import them into calibre in batches of twenty.
|
||||
* Kindle only: It is a Topaz format book and contains some coding that the tools do not understand. You will need to get a log of the deDRM attempt, and then send the book and the PID (shown in the log) for the book to Apprentice Alf, so that the tools can be updated.
|
||||
|
||||
If you are still having problems with particular books, you will need to create a log of the DRM removal attempt for one of the problem books, and post that in a comment at Apprentice Alf's blog or in a new issue at Apprentice Harper's github repository.
|
||||
|
||||
## My Kindle book has imported and the DRM has been removed, but all the pictures are gone.
|
||||
Most likely, this is a book downloaded from Amazon directly to an eInk Kindle (e.g. Paperwhite). Unfortunately, the pictures are probably in a .azw6 file that the tools don't understand. You must download the book manually from Amazon's web site "For transfer via USB" to your Kindle. When you download the eboook in this manner, Amazon will package the pictures in the with text in a single file that the tools will be able to import successfully.
|
||||
|
||||
## My Kindle book has imported, but it's showing up as an AZW4 format. Conversions take a long time and/or are very poor.
|
||||
You have found a Print Replica Kindle ebook. This is a PDF in a Kindle wrapper. Now the DRM has been removed, you can extract the PDF from the wrapper using the KindleUnpack plugin. Conversion of PDFs rarely gives good results.
|
||||
|
||||
## The tools can't see an ebook that was downloaded directly to my eInk kindle, although it's definitely there, and I can read it on the Kindle. I can't even try to import it.
|
||||
Mostly likely, this is a book downloaded from Amazon directly to one of the newer eInk Kindles (e.g. Paperwhite). Unfortunately, it is probably in a new multi-file KFX format that the tools don't understand. You must download the book manually from Amazon's web site "For transfer via USB" to your Kindle. When you download the ebook in this manner, Amazon will send a single KF8-format file that the tools will be able to import successfully.
|
||||
|
||||
## Do the tools work on books from Kobo?
|
||||
If you use the Kobo desktop application for Mac or PC, install the obok plugin. This will import and remove the DRM from your Kobo books, and is the easiest method for Kobo ebooks.
|
||||
|
||||
## I registered Adobe Digital Editions with an Adobe ID before downloading, but my epub or PDF still has DRM.
|
||||
Adobe introduced a new DRM scheme with ADE 3.0 and later. Install ADE 2.0.1 and register with the same Adobe ID. If you can't open your book in ADE 2.01, then you have a book with the new DRM scheme. These tools can't help. You can avoid the new DRM scheme by always downloading your ebooks with ADE 2.0.1. Some retailers will require ADE 3.0+, in which case you won't be able to download with ADE 2.0.1.
|
||||
|
||||
## The DRM wasn't removed and the log says "Failed to decrypt with error: Cannot decode library or rented ebooks." What now?
|
||||
You're trying to remove the DRM from an ebook that's only on loan to you. No help will be given to remove DRM from such ebooks. If you think that you have received this message for a book you own, please create an issue at github, or comment at the blog.
|
||||
|
||||
## I cannot solve my problem with the DeDRM plugin, and now I need to ‘post a log’. How do I do that?
|
||||
Remove the DRMed book from calibre. Click the Preferences drop-down menu and choose 'Restart in debug mode'. Once calibre has re-started, import the problem ebook. Now close calibre. A log will appear that you can copy and paste into a comment at Apprentice Alf's blog, or into a new issue at Apprentice Harper's github repository.
|
||||
|
||||
## I cannot solve my problem with the Macintosh DeDRM application, and now I need to ‘post a log’. How do I do that?
|
||||
The Macintosh DeDRM application creates a log file on your desktop every time it is run. After unsuccessfully removing DRM from one ebook, copy the contents of the log file (it is a simple text file) and paste it into your comment at Apprentice Alf's blog or in a new issue at Apprentice Harper's github repository.
|
||||
|
||||
## I cannot solve my problem with the Windows DeDRM application, and now I need to ‘post a log’. How do I do that?
|
||||
The Windows DeDRM application creates a log file in your home directory (C:\Users\[username]) every time it is run. After unsuccessfully removing DRM from one ebook, copy the contents of the log file (it is a simple text file) and paste it into your comment at Apprentice Alf's blog or in a new issue at Apprentice Harper's github repository.
|
||||
|
||||
|
||||
# General Questions
|
||||
|
||||
## Once the DRM has been removed, is there any trace of my personal identity left in the ebook?
|
||||
The tools only remove the DRM. No attempt is made to remove any personally identifying information.
|
||||
|
||||
## What do some of my Kindle ebooks import as HTMLZ format in calibre?
|
||||
Most Amazon Kindle ebooks are Mobipocket format ebooks, or the new KF8 format. However, some are in a format known as Topaz. The Topaz format is only used by Amazon. A Topaz ebook is a collections of glyphs and their positions on each page tagged with some additional information from that page including OCRed text (Optical Character Recognition generated Text) to allow searching, and some additional layout information. Each page of a Topaz ebook is effectively a description of an image of that page. To convert a Topaz ebook to another format is not easy as there is not a one-to-one mapping between glyphs and characters/fonts. To account for this, two different formats are generated by the DRM removal software. The first is an html description built from the OCRtext and images stored in the Topaz file (HTMLZ). This format is easily reflowed but may suffer from typical OCRtext errors including typos, garbled text, missing italics, missing bolds, etc. The second format uses the glyph and position information to create an accurate scalable vector graphics (SVG) image of each page of the book that can be viewed in web browsers that support svg images (Safari, Firefox 4 or later, etc). Additional conversion software can be used to convert these SVG images to an image only PDF file. The DeDRM and Kindlebooks tools from this site provide both versions of the Topaz ebook so that the user can choose which one to use. The K4MobiDeDRM plugin only produces the html version of the ebook. The html version can be manually cleaned up and spell checked and then converted using Sigil/calibre to epubs, mobi ebooks, and etc.
|
||||
|
||||
## Are the tools open source? How can I be sure they are safe and not a trojan horse?
|
||||
All the DRM removal tools hosted here are almost entirely scripts of one kind or another: Python, Applescript or Windows Batch files. So they are inherently open source, and open to inspection by everyone who downloads them. The source for any compiled pieces are provided right inside the tools themselves.
|
||||
|
||||
## What ebooks do these tools work on?
|
||||
The tools linked from this blog remove DRM from PDF, ePub, kePub (Kobo), eReader, Kindle (Mobipocket, KF8, Print Replica and Topaz) format ebooks using Adobe Adept, Barnes & Noble, Amazon, Kobo and eReader DRM schemes.
|
||||
|
||||
Note these tools do NOT ‘crack’ the DRM. They simply allow the book’s owner (and only the book’s owner) to use the encryption key information already stored someplace on their computer or device to decrypt the ebook in the same manner the official ebook reading software uses.
|
||||
|
||||
## Why don’t the tools work with Kindle Fire ebooks?
|
||||
Because no-one's found out how to remove the DRM from ebooks from Kindle Fire devices yet. The workaround is to install Kindle for PC or Kindle for Mac and use books from there instead.
|
||||
|
||||
## Why don't the tools work with Kindle for iOS ebooks?
|
||||
Amazon changed the way the key was generated for Kindle for iOS books, and the tools can no longer find the key. The workaround is to install Kindle for PC or Kindle for Mac and use books from there instead.
|
||||
|
||||
## Why don't the tools work with Kindle for Android ebooks?
|
||||
Amazon turned off backup for Kindle for Android, so the tools can no longer find the key. The workaround is to install Kindle for PC or Kindle for Mac and use books from there instead.
|
||||
|
||||
## Why don't the tools work on books from the Apple iBooks Store?
|
||||
Apple regularly change the details of their DRM and so the tools in the main tools archive will not work with these ebooks. Apple’s Fairplay DRM scheme can be removed using Requiem if the appropriate version of iTunes can still be installed and used. See the post Apple and ebooks: iBookstore DRM and how to remove it at Apprentice Alf's blog for more details and links.
|
||||
|
||||
## I’ve got the tools archive and I’ve read all the FAQs but I still can’t install the tools and/or the DRM removal doesn’t work
|
||||
* Read the ReadMe_First.txt file in the top level of the tools archive
|
||||
* Read the ReadMe file in the folder of the tools you want to use.
|
||||
* If you still can’t remove the DRM, ask in the comments section of Apprentice Alf's blog, reporting the error as precisely as you can, what platform you use, what tool you have tried, what errors you get, and what versions you are using. If the problem happens when running one of the tools, post a log (see previous questions on how to do this).
|
||||
|
||||
## Who wrote these scripts?
|
||||
The authors tend to identify themselves only by pseudonyms:
|
||||
* The Adobe Adept and Barnes & Noble scripts were created by i♥cabbages
|
||||
* The Amazon Mobipocket and eReader scripts were created by The Dark Reverser
|
||||
* The Amazon K4PC DRM/format was further decoded by Bart Simpson aka Skindle
|
||||
* The Amazon K4 Mobi tool was created by by some_updates, mdlnx and others
|
||||
* The Amazon Topaz DRM removal script was created by CMBDTC
|
||||
* The Amazon Topaz format conversion was created by some_updates, clarknova, and Bart Simpson
|
||||
* The DeDRM all-in-one AppleScript application was created by Apprentice Alf
|
||||
* The DeDRM all-in-one Python application was created by some_updates
|
||||
* The DeDRM all-in-one calibre plugins was created by Apprentice Alf
|
||||
* The Scuolabooks tool was created by Hex
|
||||
* The Microsoft code was created by drs
|
||||
* The Apple DRM removal tool was created by Brahms
|
||||
|
||||
Since the original versions of the scripts and programs were released, various people have helped to maintain and improve them.
|
||||
BIN
Obok_calibre_plugin/obok_plugin.zip
Normal file
BIN
Obok_calibre_plugin/obok_plugin.zip
Normal file
Binary file not shown.
75
Obok_calibre_plugin/obok_plugin/__init__.py
Normal file
75
Obok_calibre_plugin/obok_plugin/__init__.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
#####################################################################
|
||||
# Plug-in base class
|
||||
#####################################################################
|
||||
|
||||
from calibre.customize import InterfaceActionBase
|
||||
|
||||
try:
|
||||
load_translations()
|
||||
except NameError:
|
||||
pass # load_translations() added in calibre 1.9
|
||||
|
||||
PLUGIN_NAME = 'Obok DeDRM'
|
||||
PLUGIN_SAFE_NAME = PLUGIN_NAME.strip().lower().replace(' ', '_')
|
||||
PLUGIN_DESCRIPTION = _('Removes DRM from Kobo kepubs and adds them to the library.')
|
||||
PLUGIN_VERSION_TUPLE = (6, 3, 6)
|
||||
PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE])
|
||||
HELPFILE_NAME = PLUGIN_SAFE_NAME + '_Help.htm'
|
||||
PLUGIN_AUTHORS = 'Anon'
|
||||
#####################################################################
|
||||
|
||||
class ObokDeDRMAction(InterfaceActionBase):
|
||||
|
||||
name = PLUGIN_NAME
|
||||
description = PLUGIN_DESCRIPTION
|
||||
supported_platforms = ['windows', 'osx', 'linux' ]
|
||||
author = PLUGIN_AUTHORS
|
||||
version = PLUGIN_VERSION_TUPLE
|
||||
minimum_calibre_version = (1, 0, 0)
|
||||
|
||||
#: This field defines the GUI plugin class that contains all the code
|
||||
#: that actually does something. Its format is module_path:class_name
|
||||
#: The specified class must be defined in the specified module.
|
||||
actual_plugin = 'calibre_plugins.'+PLUGIN_SAFE_NAME+'.action:InterfacePluginAction'
|
||||
|
||||
def is_customizable(self):
|
||||
'''
|
||||
This method must return True to enable customization via
|
||||
Preferences->Plugins
|
||||
'''
|
||||
return True
|
||||
|
||||
def config_widget(self):
|
||||
'''
|
||||
Implement this method and :meth:`save_settings` in your plugin to
|
||||
use a custom configuration dialog.
|
||||
|
||||
This method, if implemented, must return a QWidget. The widget can have
|
||||
an optional method validate() that takes no arguments and is called
|
||||
immediately after the user clicks OK. Changes are applied if and only
|
||||
if the method returns True.
|
||||
|
||||
If for some reason you cannot perform the configuration at this time,
|
||||
return a tuple of two strings (message, details), these will be
|
||||
displayed as a warning dialog to the user and the process will be
|
||||
aborted.
|
||||
|
||||
The base class implementation of this method raises NotImplementedError
|
||||
so by default no user configuration is possible.
|
||||
'''
|
||||
if self.actual_plugin_:
|
||||
from calibre_plugins.obok_dedrm.config import ConfigWidget
|
||||
return ConfigWidget(self.actual_plugin_)
|
||||
|
||||
def save_settings(self, config_widget):
|
||||
'''
|
||||
Save the settings specified by the user with config_widget.
|
||||
'''
|
||||
config_widget.save_settings()
|
||||
497
Obok_calibre_plugin/obok_plugin/action.py
Normal file
497
Obok_calibre_plugin/obok_plugin/action.py
Normal file
@@ -0,0 +1,497 @@
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
|
||||
import os, traceback, zipfile
|
||||
|
||||
try:
|
||||
from PyQt5.Qt import QToolButton, QUrl
|
||||
except ImportError:
|
||||
from PyQt4.Qt import QToolButton, QUrl
|
||||
|
||||
from calibre.gui2 import open_url, question_dialog
|
||||
from calibre.gui2.actions import InterfaceAction
|
||||
from calibre.utils.config import config_dir
|
||||
from calibre.ptempfile import (PersistentTemporaryDirectory,
|
||||
PersistentTemporaryFile, remove_dir)
|
||||
|
||||
from calibre.ebooks.metadata.meta import get_metadata
|
||||
|
||||
from calibre_plugins.obok_dedrm.dialogs import (SelectionDialog, DecryptAddProgressDialog,
|
||||
AddEpubFormatsProgressDialog, ResultsSummaryDialog)
|
||||
from calibre_plugins.obok_dedrm.config import plugin_prefs as cfg
|
||||
from calibre_plugins.obok_dedrm.__init__ import (PLUGIN_NAME, PLUGIN_SAFE_NAME,
|
||||
PLUGIN_VERSION, PLUGIN_DESCRIPTION, HELPFILE_NAME)
|
||||
from calibre_plugins.obok_dedrm.utilities import (
|
||||
get_icon, set_plugin_icon_resources, showErrorDlg, format_plural,
|
||||
debug_print
|
||||
)
|
||||
|
||||
from calibre_plugins.obok_dedrm.obok.obok import KoboLibrary
|
||||
from calibre_plugins.obok_dedrm.obok.legacy_obok import legacy_obok
|
||||
|
||||
PLUGIN_ICONS = ['images/obok.png']
|
||||
|
||||
try:
|
||||
debug_print("obok::action_err.py - loading translations")
|
||||
load_translations()
|
||||
except NameError:
|
||||
debug_print("obok::action_err.py - exception when loading translations")
|
||||
pass # load_translations() added in calibre 1.9
|
||||
|
||||
class InterfacePluginAction(InterfaceAction):
|
||||
name = PLUGIN_NAME
|
||||
action_spec = (PLUGIN_NAME, None,
|
||||
_(PLUGIN_DESCRIPTION), None)
|
||||
popup_type = QToolButton.InstantPopup
|
||||
action_type = 'current'
|
||||
|
||||
def genesis(self):
|
||||
icon_resources = self.load_resources(PLUGIN_ICONS)
|
||||
set_plugin_icon_resources(PLUGIN_NAME, icon_resources)
|
||||
|
||||
self.qaction.setIcon(get_icon(PLUGIN_ICONS[0]))
|
||||
self.qaction.triggered.connect(self.launchObok)
|
||||
self.gui.keyboard.finalize()
|
||||
|
||||
def launchObok(self):
|
||||
'''
|
||||
Main processing/distribution method
|
||||
'''
|
||||
self.count = 0
|
||||
self.books_to_add = []
|
||||
self.formats_to_add = []
|
||||
self.add_books_cancelled = False
|
||||
self.decryption_errors = []
|
||||
self.userkeys = []
|
||||
self.duplicate_book_list = []
|
||||
self.no_home_for_book = []
|
||||
self.ids_of_new_books = []
|
||||
self.successful_format_adds =[]
|
||||
self.add_formats_cancelled = False
|
||||
self.tdir = PersistentTemporaryDirectory('_obok', prefix='')
|
||||
self.db = self.gui.current_db.new_api
|
||||
self.current_idx = self.gui.library_view.currentIndex()
|
||||
|
||||
print ('Running {}'.format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
|
||||
#
|
||||
# search for connected device in case serials are saved
|
||||
tmpserials = cfg['kobo_serials']
|
||||
device_path = None
|
||||
try:
|
||||
device = self.parent().device_manager.connected_device
|
||||
if (device):
|
||||
device_path = device._main_prefix
|
||||
debug_print("get_device_settings - device_path=", device_path)
|
||||
else:
|
||||
debug_print("didn't find device")
|
||||
except:
|
||||
debug_print("Exception getting device path. Probably not an E-Ink Kobo device")
|
||||
|
||||
# Get the Kobo Library object (obok v3.01)
|
||||
self.library = KoboLibrary(tmpserials, device_path)
|
||||
debug_print ("got kobodir %s" % self.library.kobodir)
|
||||
if (self.library.kobodir == ''):
|
||||
# linux and no device connected, but could be extended
|
||||
# to the case where on Windows/Mac the prog is not installed
|
||||
msg = _('<p>Could not find Kobo Library\n<p>Windows/Mac: do you have Kobo Desktop installed?\n<p>Windows/Mac/Linux: In case you have an Kobo eInk device, connect the device.')
|
||||
showErrorDlg(msg, None)
|
||||
return
|
||||
|
||||
|
||||
# Get a list of Kobo titles
|
||||
books = self.build_book_list()
|
||||
if len(books) < 1:
|
||||
msg = _('<p>No books found in Kobo Library\nAre you sure it\'s installed\configured\synchronized?')
|
||||
showErrorDlg(msg, None)
|
||||
return
|
||||
|
||||
# Check to see if a key can be retrieved using the legacy obok method.
|
||||
legacy_key = legacy_obok().get_legacy_cookie_id
|
||||
if legacy_key is not None:
|
||||
print (_('Legacy key found: '), legacy_key.encode('hex_codec'))
|
||||
self.userkeys.append(legacy_key)
|
||||
# Add userkeys found through the normal obok method to the list to try.
|
||||
try:
|
||||
candidate_keys = self.library.userkeys
|
||||
except:
|
||||
print (_('Trouble retrieving keys with newer obok method.'))
|
||||
traceback.print_exc()
|
||||
else:
|
||||
if len(candidate_keys):
|
||||
self.userkeys.extend(candidate_keys)
|
||||
print (_('Found {0} possible keys to try.').format(len(self.userkeys)))
|
||||
if not len(self.userkeys):
|
||||
msg = _('<p>No userkeys found to decrypt books with. No point in proceeding.')
|
||||
showErrorDlg(msg, None)
|
||||
return
|
||||
|
||||
# Launch the Dialog so the user can select titles.
|
||||
dlg = SelectionDialog(self.gui, self, books)
|
||||
if dlg.exec_():
|
||||
books_to_import = dlg.getBooks()
|
||||
self.count = len(books_to_import)
|
||||
debug_print("InterfacePluginAction::launchObok - number of books to decrypt: %d" % self.count)
|
||||
# Feed the titles, the callback function (self.get_decrypted_kobo_books)
|
||||
# and the Kobo library object to the ProgressDialog dispatcher.
|
||||
d = DecryptAddProgressDialog(self.gui, books_to_import, self.get_decrypted_kobo_books, self.library, 'kobo',
|
||||
status_msg_type='Kobo books', action_type=('Decrypting', 'Decryption'))
|
||||
# Canceled the decryption process; clean up and exit.
|
||||
if d.wasCanceled():
|
||||
print (_('{} - Decryption canceled by user.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
|
||||
self.library.close()
|
||||
remove_dir(self.tdir)
|
||||
return
|
||||
else:
|
||||
# Canceled the selection process; clean up and exit.
|
||||
self.library.close()
|
||||
remove_dir(self.tdir)
|
||||
return
|
||||
# Close Kobo Library object
|
||||
self.library.close()
|
||||
|
||||
# If we have decrypted books to work with, feed the list of decrypted books details
|
||||
# and the callback function (self.add_new_books) to the ProgressDialog dispatcher.
|
||||
if len(self.books_to_add):
|
||||
d = DecryptAddProgressDialog(self.gui, self.books_to_add, self.add_new_books, self.db, 'calibre',
|
||||
status_msg_type='new calibre books', action_type=('Adding','Addition'))
|
||||
# Canceled the "add new books to calibre" process;
|
||||
# show the results of what got added before cancellation.
|
||||
if d.wasCanceled():
|
||||
print (_('{} - "Add books" canceled by user.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
|
||||
self.add_books_cancelled = True
|
||||
print (_('{} - wrapping up results.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
|
||||
self.wrap_up_results()
|
||||
remove_dir(self.tdir)
|
||||
return
|
||||
# If books couldn't be added because of duplicate entries in calibre, ask
|
||||
# if we should try to add the decrypted epubs to existing calibre library entries.
|
||||
if len(self.duplicate_book_list):
|
||||
if cfg['finding_homes_for_formats'] == 'Always':
|
||||
self.process_epub_formats()
|
||||
elif cfg['finding_homes_for_formats'] == 'Never':
|
||||
self.no_home_for_book.extend([entry[0] for entry in self.duplicate_book_list])
|
||||
else:
|
||||
if self.ask_about_inserting_epubs():
|
||||
# Find homes for the epub decrypted formats in existing calibre library entries.
|
||||
self.process_epub_formats()
|
||||
else:
|
||||
print (_('{} - User opted not to try to insert EPUB formats').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
|
||||
self.no_home_for_book.extend([entry[0] for entry in self.duplicate_book_list])
|
||||
|
||||
print (_('{} - wrapping up results.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
|
||||
self.wrap_up_results()
|
||||
remove_dir(self.tdir)
|
||||
return
|
||||
|
||||
def show_help(self):
|
||||
'''
|
||||
Extract on demand the help file resource
|
||||
'''
|
||||
def get_help_file_resource():
|
||||
# We will write the help file out every time, in case the user upgrades the plugin zip
|
||||
# and there is a newer help file contained within it.
|
||||
file_path = os.path.join(config_dir, 'plugins', HELPFILE_NAME)
|
||||
file_data = self.load_resources(HELPFILE_NAME)[HELPFILE_NAME]
|
||||
with open(file_path,'w') as f:
|
||||
f.write(file_data)
|
||||
return file_path
|
||||
url = 'file:///' + get_help_file_resource()
|
||||
open_url(QUrl(url))
|
||||
|
||||
def build_book_list(self):
|
||||
'''
|
||||
Connect to Kobo db and get titles.
|
||||
'''
|
||||
return self.library.books
|
||||
|
||||
def get_decrypted_kobo_books(self, book):
|
||||
'''
|
||||
This method is a call-back function used by DecryptAddProgressDialog in dialogs.py to decrypt Kobo books
|
||||
|
||||
:param book: A KoboBook object that is to be decrypted.
|
||||
'''
|
||||
print (_('{0} - Decrypting {1}').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, book.title))
|
||||
decrypted = self.decryptBook(book)
|
||||
if decrypted['success']:
|
||||
# Build a list of calibre "book maps" for calibre's add_book function.
|
||||
mi = get_metadata(decrypted['fileobj'], 'epub')
|
||||
bookmap = {'EPUB':decrypted['fileobj'].name}
|
||||
self.books_to_add.append((mi, bookmap))
|
||||
else:
|
||||
# Book is probably still encrypted.
|
||||
print (_('{0} - Couldn\'t decrypt {1}').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, book.title))
|
||||
self.decryption_errors.append((book.title, _('decryption errors')))
|
||||
return False
|
||||
return True
|
||||
|
||||
def add_new_books(self, books_to_add):
|
||||
'''
|
||||
This method is a call-back function used by DecryptAddProgressDialog in dialogs.py to add books to calibre
|
||||
(It's set up to handle multiple books, but will only be fed books one at a time by DecryptAddProgressDialog)
|
||||
|
||||
:param books_to_add: List of calibre bookmaps (created in get_decrypted_kobo_books)
|
||||
'''
|
||||
added = self.db.add_books(books_to_add, add_duplicates=False, run_hooks=False)
|
||||
if len(added[0]):
|
||||
# Record the id(s) that got added
|
||||
for id in added[0]:
|
||||
print (_('{0} - Added {1}').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, books_to_add[0][0].title))
|
||||
self.ids_of_new_books.append((id, books_to_add[0][0]))
|
||||
if len(added[1]):
|
||||
# Build a list of details about the books that didn't get added because duplicate were detected.
|
||||
for mi, map in added[1]:
|
||||
print (_('{0} - {1} already exists. Will try to add format later.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, mi.title))
|
||||
self.duplicate_book_list.append((mi, map['EPUB'], _('duplicate detected')))
|
||||
return False
|
||||
return True
|
||||
|
||||
def add_epub_format(self, book_id, mi, path):
|
||||
'''
|
||||
This method is a call-back function used by AddEpubFormatsProgressDialog in dialogs.py
|
||||
|
||||
:param book_id: calibre ID of the book to add the encrypted epub to.
|
||||
:param mi: calibre metadata object
|
||||
:param path: path to the decrypted epub (temp file)
|
||||
'''
|
||||
if self.db.add_format(book_id, 'EPUB', path, replace=False, run_hooks=False):
|
||||
self.successful_format_adds.append((book_id, mi))
|
||||
print (_('{0} - Successfully added EPUB format to existing {1}').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, mi.title))
|
||||
return True
|
||||
# we really shouldn't get here.
|
||||
print (_('{0} - Error adding EPUB format to existing {1}. This really shouldn\'t happen.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, mi.title))
|
||||
self.no_home_for_book.append(mi)
|
||||
return False
|
||||
|
||||
def process_epub_formats(self):
|
||||
'''
|
||||
Ask the user if they want to try to find homes for those books that already had an entry in calibre
|
||||
'''
|
||||
for book in self.duplicate_book_list:
|
||||
mi, tmp_file = book[0], book[1]
|
||||
dup_ids = self.db.find_identical_books(mi)
|
||||
home_id = self.find_a_home(dup_ids)
|
||||
if home_id is not None:
|
||||
# Found an epub-free duplicate to add the epub to.
|
||||
# build a list for the add_epub_format method to use.
|
||||
self.formats_to_add.append((home_id, mi, tmp_file))
|
||||
else:
|
||||
self.no_home_for_book.append(mi)
|
||||
# If we found homes for decrypted epubs in existing calibre entries, feed the list of decrypted book
|
||||
# details and the callback function (self.add_epub_format) to the ProgressDialog dispatcher.
|
||||
if self.formats_to_add:
|
||||
d = AddEpubFormatsProgressDialog(self.gui, self.formats_to_add, self.add_epub_format)
|
||||
if d.wasCanceled():
|
||||
print (_('{} - "Insert formats" canceled by user.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
|
||||
self.add_formats_cancelled = True
|
||||
return
|
||||
#return
|
||||
return
|
||||
|
||||
def wrap_up_results(self):
|
||||
'''
|
||||
Present the results
|
||||
'''
|
||||
caption = PLUGIN_NAME + ' v' + PLUGIN_VERSION
|
||||
# Refresh the gui and highlight new entries/modified entries.
|
||||
if len(self.ids_of_new_books) or len(self.successful_format_adds):
|
||||
self.refresh_gui_lib()
|
||||
|
||||
msg, log = self.build_report()
|
||||
|
||||
sd = ResultsSummaryDialog(self.gui, caption, msg, log)
|
||||
sd.exec_()
|
||||
return
|
||||
|
||||
def ask_about_inserting_epubs(self):
|
||||
'''
|
||||
Build question dialog with details about kobo books
|
||||
that couldn't be added to calibre as new books.
|
||||
'''
|
||||
''' Terisa: Improve the message
|
||||
'''
|
||||
caption = PLUGIN_NAME + ' v' + PLUGIN_VERSION
|
||||
plural = format_plural(len(self.ids_of_new_books))
|
||||
det_msg = ''
|
||||
if self.count > 1:
|
||||
msg = _('<p><b>{0}</b> EPUB{2} successfully added to library.<br /><br /><b>{1}</b> ').format(len(self.ids_of_new_books), len(self.duplicate_book_list), plural)
|
||||
msg += _('not added because books with the same title/author were detected.<br /><br />Would you like to try and add the EPUB format{0}').format(plural)
|
||||
msg += _(' to those existing entries?<br /><br />NOTE: no pre-existing EPUBs will be overwritten.')
|
||||
for entry in self.duplicate_book_list:
|
||||
det_msg += _('{0} -- not added because of {1} in your library.\n\n').format(entry[0].title, entry[2])
|
||||
else:
|
||||
msg = _('<p><b>{0}</b> -- not added because of {1} in your library.<br /><br />').format(self.duplicate_book_list[0][0].title, self.duplicate_book_list[0][2])
|
||||
msg += _('Would you like to try and add the EPUB format to an available calibre duplicate?<br /><br />')
|
||||
msg += _('NOTE: no pre-existing EPUB will be overwritten.')
|
||||
|
||||
return question_dialog(self.gui, caption, msg, det_msg)
|
||||
|
||||
def find_a_home(self, ids):
|
||||
'''
|
||||
Find the ID of the first EPUB-Free duplicate available
|
||||
|
||||
:param ids: List of calibre IDs that might serve as a home.
|
||||
'''
|
||||
for id in ids:
|
||||
# Find the first entry that matches the incoming book that doesn't have an EPUB format.
|
||||
if not self.db.has_format(id, 'EPUB'):
|
||||
return id
|
||||
break
|
||||
return None
|
||||
|
||||
def refresh_gui_lib(self):
|
||||
'''
|
||||
Update the GUI; highlight the books that were added/modified
|
||||
'''
|
||||
if self.current_idx.isValid():
|
||||
self.gui.library_view.model().current_changed(self.current_idx, self.current_idx)
|
||||
new_entries = [id for id, mi in self.ids_of_new_books]
|
||||
if new_entries:
|
||||
self.gui.library_view.model().db.data.books_added(new_entries)
|
||||
self.gui.library_view.model().books_added(len(new_entries))
|
||||
new_entries.extend([id for id, mi in self.successful_format_adds])
|
||||
self.gui.db_images.reset()
|
||||
self.gui.tags_view.recount()
|
||||
self.gui.library_view.model().set_highlight_only(True)
|
||||
self.gui.library_view.select_rows(new_entries)
|
||||
return
|
||||
|
||||
def decryptBook(self, book):
|
||||
'''
|
||||
Decrypt Kobo book
|
||||
|
||||
:param book: obok file object
|
||||
'''
|
||||
result = {}
|
||||
result['success'] = False
|
||||
result['fileobj'] = None
|
||||
|
||||
zin = zipfile.ZipFile(book.filename, 'r')
|
||||
#print ('Kobo library filename: {0}'.format(book.filename))
|
||||
for userkey in self.userkeys:
|
||||
print (_('Trying key: '), userkey.encode('hex_codec'))
|
||||
check = True
|
||||
try:
|
||||
fileout = PersistentTemporaryFile('.epub', dir=self.tdir)
|
||||
#print ('Temp file: {0}'.format(fileout.name))
|
||||
# modify the output file to be compressed by default
|
||||
zout = zipfile.ZipFile(fileout.name, "w", zipfile.ZIP_DEFLATED)
|
||||
# ensure that the mimetype file is the first written to the epub container
|
||||
# and is stored with no compression
|
||||
members = zin.namelist();
|
||||
try:
|
||||
members.remove('mimetype')
|
||||
except Exception:
|
||||
pass
|
||||
zout.writestr('mimetype', 'application/epub+zip', zipfile.ZIP_STORED)
|
||||
# end of mimetype mod
|
||||
for filename in members:
|
||||
contents = zin.read(filename)
|
||||
if filename in book.encryptedfiles:
|
||||
file = book.encryptedfiles[filename]
|
||||
contents = file.decrypt(userkey, contents)
|
||||
# Parse failures mean the key is probably wrong.
|
||||
if check:
|
||||
check = not file.check(contents)
|
||||
zout.writestr(filename, contents)
|
||||
zout.close()
|
||||
zin.close()
|
||||
result['success'] = True
|
||||
result['fileobj'] = fileout
|
||||
print ('Success!')
|
||||
return result
|
||||
except ValueError:
|
||||
print (_('Decryption failed, trying next key.'))
|
||||
zout.close()
|
||||
continue
|
||||
except Exception:
|
||||
print (_('Unknown Error decrypting, trying next key..'))
|
||||
zout.close()
|
||||
continue
|
||||
result['fileobj'] = book.filename
|
||||
zin.close()
|
||||
return result
|
||||
|
||||
def build_report(self):
|
||||
log = ''
|
||||
processed = len(self.ids_of_new_books) + len(self.successful_format_adds)
|
||||
|
||||
if processed == self.count:
|
||||
if self.count > 1:
|
||||
msg = _('<p>All selected Kobo books added as new calibre books or inserted into existing calibre ebooks.<br /><br />No issues.')
|
||||
else:
|
||||
# Single book ... don't get fancy.
|
||||
title = self.ids_of_new_books[0][1].title if self.ids_of_new_books else self.successful_format_adds[0][1].title
|
||||
msg = _('<p>{0} successfully added.').format(title)
|
||||
return (msg, log)
|
||||
else:
|
||||
if self.count != 1:
|
||||
msg = _('<p>Not all selected Kobo books made it into calibre.<br /><br />View report for details.')
|
||||
log += _('<p><b>Total attempted:</b> {}</p>\n').format(self.count)
|
||||
log += _('<p><b>Decryption errors:</b> {}</p>\n').format(len(self.decryption_errors))
|
||||
if self.decryption_errors:
|
||||
log += '<ul>\n'
|
||||
for title, reason in self.decryption_errors:
|
||||
log += '<li>{}</li>\n'.format(title)
|
||||
log += '</ul>\n'
|
||||
log += _('<p><b>New Books created:</b> {}</p>\n').format(len(self.ids_of_new_books))
|
||||
if self.ids_of_new_books:
|
||||
log += '<ul>\n'
|
||||
for id, mi in self.ids_of_new_books:
|
||||
log += '<li>{}</li>\n'.format(mi.title)
|
||||
log += '</ul>\n'
|
||||
if self.add_books_cancelled:
|
||||
log += _('<p><b>Duplicates that weren\'t added:</b> {}</p>\n').format(len(self.duplicate_book_list))
|
||||
if self.duplicate_book_list:
|
||||
log += '<ul>\n'
|
||||
for book in self.duplicate_book_list:
|
||||
log += '<li>{}</li>\n'.format(book[0].title)
|
||||
log += '</ul>\n'
|
||||
cancelled_count = self.count - (len(self.decryption_errors) + len(self.ids_of_new_books) + len(self.duplicate_book_list))
|
||||
if cancelled_count > 0:
|
||||
log += _('<p><b>Book imports cancelled by user:</b> {}</p>\n').format(cancelled_count)
|
||||
return (msg, log)
|
||||
log += _('<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n').format(len(self.successful_format_adds))
|
||||
if self.successful_format_adds:
|
||||
log += '<ul>\n'
|
||||
for id, mi in self.successful_format_adds:
|
||||
log += '<li>{}</li>\n'.format(mi.title)
|
||||
log += '</ul>\n'
|
||||
log += _('<p><b>EPUB formats NOT inserted into existing calibre books:</b> {}<br />\n').format(len(self.no_home_for_book))
|
||||
log += _('(Either because the user <i>chose</i> not to insert them, or because all duplicates already had an EPUB format)')
|
||||
if self.no_home_for_book:
|
||||
log += '<ul>\n'
|
||||
for mi in self.no_home_for_book:
|
||||
log += '<li>{}</li>\n'.format(mi.title)
|
||||
log += '</ul>\n'
|
||||
if self.add_formats_cancelled:
|
||||
cancelled_count = self.count - (len(self.decryption_errors) + len(self.ids_of_new_books) + len(self.successful_format_adds) + len(self.no_home_for_book))
|
||||
if cancelled_count > 0:
|
||||
log += _('<p><b>Format imports cancelled by user:</b> {}</p>\n').format(cancelled_count)
|
||||
return (msg, log)
|
||||
else:
|
||||
|
||||
# Single book ... don't get fancy.
|
||||
if self.ids_of_new_books:
|
||||
title = self.ids_of_new_books[0][1].title
|
||||
elif self.successful_format_adds:
|
||||
title = self.successful_format_adds[0][1].title
|
||||
elif self.no_home_for_book:
|
||||
title = self.no_home_for_book[0].title
|
||||
elif self.decryption_errors:
|
||||
title = self.decryption_errors[0][0]
|
||||
else:
|
||||
title = _('Unknown Book Title')
|
||||
if self.decryption_errors:
|
||||
reason = _('it couldn\'t be decrypted.')
|
||||
elif self.no_home_for_book:
|
||||
reason = _('user CHOSE not to insert the new EPUB format, or all existing calibre entries HAD an EPUB format already.')
|
||||
else:
|
||||
reason = _('of unknown reasons. Gosh I\'m embarrassed!')
|
||||
msg = _('<p>{0} not added because {1}').format(title, reason)
|
||||
return (msg, log)
|
||||
|
||||
589
Obok_calibre_plugin/obok_plugin/common_utils.py
Normal file
589
Obok_calibre_plugin/obok_plugin/common_utils.py
Normal file
@@ -0,0 +1,589 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2012, David Forrester <davidfor@internode.on.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os, time, re, sys
|
||||
try:
|
||||
from PyQt5.Qt import (Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout, QProgressBar,
|
||||
QTableWidgetItem, QFont, QLineEdit, QComboBox,
|
||||
QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime,
|
||||
QRegExpValidator, QRegExp, QDate, QDateEdit)
|
||||
except ImportError:
|
||||
from PyQt4.Qt import (Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout, QProgressBar,
|
||||
QTableWidgetItem, QFont, QLineEdit, QComboBox,
|
||||
QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime,
|
||||
QRegExpValidator, QRegExp, QDate, QDateEdit)
|
||||
|
||||
from calibre.constants import iswindows, filesystem_encoding, DEBUG
|
||||
from calibre.gui2 import gprefs, error_dialog, UNDEFINED_QDATETIME, Application
|
||||
from calibre.gui2.actions import menu_action_unique_name
|
||||
from calibre.gui2.keyboard import ShortcutConfig
|
||||
from calibre.utils.config import config_dir, tweaks
|
||||
from calibre.utils.date import now, format_date, qt_to_dt, UNDEFINED_DATE, as_local_time
|
||||
from calibre import prints
|
||||
|
||||
# Global definition of our plugin name. Used for common functions that require this.
|
||||
plugin_name = None
|
||||
# Global definition of our plugin resources. Used to share between the xxxAction and xxxBase
|
||||
# classes if you need any zip images to be displayed on the configuration dialog.
|
||||
plugin_icon_resources = {}
|
||||
|
||||
BASE_TIME = None
|
||||
def debug_print(*args):
|
||||
global BASE_TIME
|
||||
if BASE_TIME is None:
|
||||
BASE_TIME = time.time()
|
||||
if DEBUG:
|
||||
prints('DEBUG: %6.1f'%(time.time()-BASE_TIME), *args)
|
||||
|
||||
|
||||
try:
|
||||
debug_print("obok::common_utils.py - loading translations")
|
||||
load_translations()
|
||||
except NameError:
|
||||
debug_print("obok::common_utils.py - exception when loading translations")
|
||||
pass # load_translations() added in calibre 1.9
|
||||
|
||||
def set_plugin_icon_resources(name, resources):
|
||||
'''
|
||||
Set our global store of plugin name and icon resources for sharing between
|
||||
the InterfaceAction class which reads them and the ConfigWidget
|
||||
if needed for use on the customization dialog for this plugin.
|
||||
'''
|
||||
global plugin_icon_resources, plugin_name
|
||||
plugin_name = name
|
||||
plugin_icon_resources = resources
|
||||
|
||||
def get_icon(icon_name):
|
||||
'''
|
||||
Retrieve a QIcon for the named image from the zip file if it exists,
|
||||
or if not then from Calibre's image cache.
|
||||
'''
|
||||
if icon_name:
|
||||
pixmap = get_pixmap(icon_name)
|
||||
if pixmap is None:
|
||||
# Look in Calibre's cache for the icon
|
||||
return QIcon(I(icon_name))
|
||||
else:
|
||||
return QIcon(pixmap)
|
||||
return QIcon()
|
||||
|
||||
|
||||
def get_pixmap(icon_name):
|
||||
'''
|
||||
Retrieve a QPixmap for the named image
|
||||
Any icons belonging to the plugin must be prefixed with 'images/'
|
||||
'''
|
||||
global plugin_icon_resources, plugin_name
|
||||
|
||||
if not icon_name.startswith('images/'):
|
||||
# We know this is definitely not an icon belonging to this plugin
|
||||
pixmap = QPixmap()
|
||||
pixmap.load(I(icon_name))
|
||||
return pixmap
|
||||
|
||||
# Check to see whether the icon exists as a Calibre resource
|
||||
# This will enable skinning if the user stores icons within a folder like:
|
||||
# ...\AppData\Roaming\calibre\resources\images\Plugin Name\
|
||||
if plugin_name:
|
||||
local_images_dir = get_local_images_dir(plugin_name)
|
||||
local_image_path = os.path.join(local_images_dir, icon_name.replace('images/', ''))
|
||||
if os.path.exists(local_image_path):
|
||||
pixmap = QPixmap()
|
||||
pixmap.load(local_image_path)
|
||||
return pixmap
|
||||
|
||||
# As we did not find an icon elsewhere, look within our zip resources
|
||||
if icon_name in plugin_icon_resources:
|
||||
pixmap = QPixmap()
|
||||
pixmap.loadFromData(plugin_icon_resources[icon_name])
|
||||
return pixmap
|
||||
return None
|
||||
|
||||
|
||||
def get_local_images_dir(subfolder=None):
|
||||
'''
|
||||
Returns a path to the user's local resources/images folder
|
||||
If a subfolder name parameter is specified, appends this to the path
|
||||
'''
|
||||
images_dir = os.path.join(config_dir, 'resources/images')
|
||||
if subfolder:
|
||||
images_dir = os.path.join(images_dir, subfolder)
|
||||
if iswindows:
|
||||
images_dir = os.path.normpath(images_dir)
|
||||
return images_dir
|
||||
|
||||
|
||||
def create_menu_item(ia, parent_menu, menu_text, image=None, tooltip=None,
|
||||
shortcut=(), triggered=None, is_checked=None):
|
||||
'''
|
||||
Create a menu action with the specified criteria and action
|
||||
Note that if no shortcut is specified, will not appear in Preferences->Keyboard
|
||||
This method should only be used for actions which either have no shortcuts,
|
||||
or register their menus only once. Use create_menu_action_unique for all else.
|
||||
'''
|
||||
if shortcut is not None:
|
||||
if len(shortcut) == 0:
|
||||
shortcut = ()
|
||||
else:
|
||||
shortcut = _(shortcut)
|
||||
ac = ia.create_action(spec=(menu_text, None, tooltip, shortcut),
|
||||
attr=menu_text)
|
||||
if image:
|
||||
ac.setIcon(get_icon(image))
|
||||
if triggered is not None:
|
||||
ac.triggered.connect(triggered)
|
||||
if is_checked is not None:
|
||||
ac.setCheckable(True)
|
||||
if is_checked:
|
||||
ac.setChecked(True)
|
||||
|
||||
parent_menu.addAction(ac)
|
||||
return ac
|
||||
|
||||
|
||||
def create_menu_action_unique(ia, parent_menu, menu_text, image=None, tooltip=None,
|
||||
shortcut=None, triggered=None, is_checked=None, shortcut_name=None,
|
||||
unique_name=None):
|
||||
'''
|
||||
Create a menu action with the specified criteria and action, using the new
|
||||
InterfaceAction.create_menu_action() function which ensures that regardless of
|
||||
whether a shortcut is specified it will appear in Preferences->Keyboard
|
||||
'''
|
||||
orig_shortcut = shortcut
|
||||
kb = ia.gui.keyboard
|
||||
if unique_name is None:
|
||||
unique_name = menu_text
|
||||
if not shortcut == False:
|
||||
full_unique_name = menu_action_unique_name(ia, unique_name)
|
||||
if full_unique_name in kb.shortcuts:
|
||||
shortcut = False
|
||||
else:
|
||||
if shortcut is not None and not shortcut == False:
|
||||
if len(shortcut) == 0:
|
||||
shortcut = None
|
||||
else:
|
||||
shortcut = _(shortcut)
|
||||
|
||||
if shortcut_name is None:
|
||||
shortcut_name = menu_text.replace('&','')
|
||||
|
||||
ac = ia.create_menu_action(parent_menu, unique_name, menu_text, icon=None, shortcut=shortcut,
|
||||
description=tooltip, triggered=triggered, shortcut_name=shortcut_name)
|
||||
if shortcut == False and not orig_shortcut == False:
|
||||
if ac.calibre_shortcut_unique_name in ia.gui.keyboard.shortcuts:
|
||||
kb.replace_action(ac.calibre_shortcut_unique_name, ac)
|
||||
if image:
|
||||
ac.setIcon(get_icon(image))
|
||||
if is_checked is not None:
|
||||
ac.setCheckable(True)
|
||||
if is_checked:
|
||||
ac.setChecked(True)
|
||||
return ac
|
||||
|
||||
|
||||
def get_library_uuid(db):
|
||||
try:
|
||||
library_uuid = db.library_id
|
||||
except:
|
||||
library_uuid = ''
|
||||
return library_uuid
|
||||
|
||||
|
||||
class ImageLabel(QLabel):
|
||||
|
||||
def __init__(self, parent, icon_name, size=16):
|
||||
QLabel.__init__(self, parent)
|
||||
pixmap = get_pixmap(icon_name)
|
||||
self.setPixmap(pixmap)
|
||||
self.setMaximumSize(size, size)
|
||||
self.setScaledContents(True)
|
||||
|
||||
|
||||
class ImageTitleLayout(QHBoxLayout):
|
||||
'''
|
||||
A reusable layout widget displaying an image followed by a title
|
||||
'''
|
||||
def __init__(self, parent, icon_name, title):
|
||||
QHBoxLayout.__init__(self)
|
||||
self.title_image_label = QLabel(parent)
|
||||
self.update_title_icon(icon_name)
|
||||
self.addWidget(self.title_image_label)
|
||||
|
||||
title_font = QFont()
|
||||
title_font.setPointSize(16)
|
||||
shelf_label = QLabel(title, parent)
|
||||
shelf_label.setFont(title_font)
|
||||
self.addWidget(shelf_label)
|
||||
self.insertStretch(-1)
|
||||
|
||||
# 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/">{0}</a>').format(_("Help")), parent)
|
||||
help_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
|
||||
help_label.setAlignment(Qt.AlignRight)
|
||||
help_label.linkActivated.connect(parent.help_link_activated)
|
||||
self.addWidget(help_label)
|
||||
|
||||
def update_title_icon(self, icon_name):
|
||||
pixmap = get_pixmap(icon_name)
|
||||
if pixmap is None:
|
||||
error_dialog(self.parent(), _("Restart required"),
|
||||
_("Title image not found - you must restart Calibre before using this plugin!"), show=True)
|
||||
else:
|
||||
self.title_image_label.setPixmap(pixmap)
|
||||
self.title_image_label.setMaximumSize(32, 32)
|
||||
self.title_image_label.setScaledContents(True)
|
||||
|
||||
|
||||
class SizePersistedDialog(QDialog):
|
||||
'''
|
||||
This dialog is a base class for any dialogs that want their size/position
|
||||
restored when they are next opened.
|
||||
'''
|
||||
def __init__(self, parent, unique_pref_name):
|
||||
QDialog.__init__(self, parent)
|
||||
self.unique_pref_name = unique_pref_name
|
||||
self.geom = gprefs.get(unique_pref_name, None)
|
||||
self.finished.connect(self.dialog_closing)
|
||||
self.help_anchor = ''
|
||||
|
||||
def resize_dialog(self):
|
||||
if self.geom is None:
|
||||
self.resize(self.sizeHint())
|
||||
else:
|
||||
self.restoreGeometry(self.geom)
|
||||
|
||||
def dialog_closing(self, result):
|
||||
geom = bytearray(self.saveGeometry())
|
||||
gprefs[self.unique_pref_name] = geom
|
||||
|
||||
def help_link_activated(self, url):
|
||||
self.plugin_action.show_help(anchor=self.help_anchor)
|
||||
|
||||
|
||||
class ReadOnlyTableWidgetItem(QTableWidgetItem):
|
||||
|
||||
def __init__(self, text):
|
||||
if text is None:
|
||||
text = ''
|
||||
QTableWidgetItem.__init__(self, text, QTableWidgetItem.UserType)
|
||||
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
|
||||
|
||||
class RatingTableWidgetItem(QTableWidgetItem):
|
||||
|
||||
def __init__(self, rating, is_read_only=False):
|
||||
QTableWidgetItem.__init__(self, '', QTableWidgetItem.UserType)
|
||||
self.setData(Qt.DisplayRole, rating)
|
||||
if is_read_only:
|
||||
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
|
||||
|
||||
|
||||
class DateTableWidgetItem(QTableWidgetItem):
|
||||
|
||||
def __init__(self, date_read, is_read_only=False, default_to_today=False, fmt=None):
|
||||
# debug_print("DateTableWidgetItem:__init__ - date_read=", date_read)
|
||||
if date_read is None or date_read == UNDEFINED_DATE and default_to_today:
|
||||
date_read = now()
|
||||
if is_read_only:
|
||||
QTableWidgetItem.__init__(self, format_date(date_read, fmt), QTableWidgetItem.UserType)
|
||||
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
|
||||
self.setData(Qt.DisplayRole, QDateTime(date_read))
|
||||
else:
|
||||
QTableWidgetItem.__init__(self, '', QTableWidgetItem.UserType)
|
||||
self.setData(Qt.DisplayRole, QDateTime(date_read))
|
||||
|
||||
from calibre.gui2.library.delegates import DateDelegate as _DateDelegate
|
||||
class DateDelegate(_DateDelegate):
|
||||
'''
|
||||
Delegate for dates. Because this delegate stores the
|
||||
format as an instance variable, a new instance must be created for each
|
||||
column. This differs from all the other delegates.
|
||||
'''
|
||||
def __init__(self, parent, fmt='dd MMM yyyy', default_to_today=True):
|
||||
_DateDelegate.__init__(self, parent)
|
||||
self.format = fmt
|
||||
self.default_to_today = default_to_today
|
||||
|
||||
# def displayText(self, val, locale):
|
||||
# d = val.toDateTime()
|
||||
# if d <= UNDEFINED_QDATETIME:
|
||||
# return ''
|
||||
# return format_date(qt_to_dt(d, as_utc=False), self.format)
|
||||
|
||||
def createEditor(self, parent, option, index):
|
||||
qde = QStyledItemDelegate.createEditor(self, parent, option, index)
|
||||
qde.setDisplayFormat(self.format)
|
||||
qde.setMinimumDateTime(UNDEFINED_QDATETIME)
|
||||
qde.setSpecialValueText(_('Undefined'))
|
||||
qde.setCalendarPopup(True)
|
||||
return qde
|
||||
|
||||
def setEditorData(self, editor, index):
|
||||
val = index.model().data(index, Qt.DisplayRole).toDateTime()
|
||||
if val is None or val == UNDEFINED_QDATETIME:
|
||||
if self.default_to_today:
|
||||
val = self.default_date
|
||||
else:
|
||||
val = UNDEFINED_QDATETIME
|
||||
editor.setDateTime(val)
|
||||
|
||||
def setModelData(self, editor, model, index):
|
||||
val = editor.dateTime()
|
||||
if val <= UNDEFINED_QDATETIME:
|
||||
model.setData(index, UNDEFINED_QDATETIME, Qt.EditRole)
|
||||
else:
|
||||
model.setData(index, QDateTime(val), Qt.EditRole)
|
||||
|
||||
|
||||
class NoWheelComboBox(QComboBox):
|
||||
|
||||
def wheelEvent (self, event):
|
||||
# Disable the mouse wheel on top of the combo box changing selection as plays havoc in a grid
|
||||
event.ignore()
|
||||
|
||||
|
||||
class CheckableTableWidgetItem(QTableWidgetItem):
|
||||
|
||||
def __init__(self, checked=False, is_tristate=False):
|
||||
QTableWidgetItem.__init__(self, '')
|
||||
self.setFlags(Qt.ItemFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled ))
|
||||
if is_tristate:
|
||||
self.setFlags(self.flags() | Qt.ItemIsTristate)
|
||||
if checked:
|
||||
self.setCheckState(Qt.Checked)
|
||||
else:
|
||||
if is_tristate and checked is None:
|
||||
self.setCheckState(Qt.PartiallyChecked)
|
||||
else:
|
||||
self.setCheckState(Qt.Unchecked)
|
||||
|
||||
def get_boolean_value(self):
|
||||
'''
|
||||
Return a boolean value indicating whether checkbox is checked
|
||||
If this is a tristate checkbox, a partially checked value is returned as None
|
||||
'''
|
||||
if self.checkState() == Qt.PartiallyChecked:
|
||||
return None
|
||||
else:
|
||||
return self.checkState() == Qt.Checked
|
||||
|
||||
|
||||
class TextIconWidgetItem(QTableWidgetItem):
|
||||
|
||||
def __init__(self, text, icon):
|
||||
QTableWidgetItem.__init__(self, text)
|
||||
if icon:
|
||||
self.setIcon(icon)
|
||||
|
||||
|
||||
class ReadOnlyTextIconWidgetItem(ReadOnlyTableWidgetItem):
|
||||
|
||||
def __init__(self, text, icon):
|
||||
ReadOnlyTableWidgetItem.__init__(self, text)
|
||||
if icon:
|
||||
self.setIcon(icon)
|
||||
|
||||
|
||||
class ReadOnlyLineEdit(QLineEdit):
|
||||
|
||||
def __init__(self, text, parent):
|
||||
if text is None:
|
||||
text = ''
|
||||
QLineEdit.__init__(self, text, parent)
|
||||
self.setEnabled(False)
|
||||
|
||||
|
||||
class NumericLineEdit(QLineEdit):
|
||||
'''
|
||||
Allows a numeric value up to two decimal places, or an integer
|
||||
'''
|
||||
def __init__(self, *args):
|
||||
QLineEdit.__init__(self, *args)
|
||||
self.setValidator(QRegExpValidator(QRegExp(r'(^\d*\.[\d]{1,2}$)|(^[1-9]\d*[\.]$)'), self))
|
||||
|
||||
|
||||
class KeyValueComboBox(QComboBox):
|
||||
|
||||
def __init__(self, parent, values, selected_key):
|
||||
QComboBox.__init__(self, parent)
|
||||
self.values = values
|
||||
self.populate_combo(selected_key)
|
||||
|
||||
def populate_combo(self, selected_key):
|
||||
self.clear()
|
||||
selected_idx = idx = -1
|
||||
for key, value in self.values.iteritems():
|
||||
idx = idx + 1
|
||||
self.addItem(value)
|
||||
if key == selected_key:
|
||||
selected_idx = idx
|
||||
self.setCurrentIndex(selected_idx)
|
||||
|
||||
def selected_key(self):
|
||||
for key, value in self.values.iteritems():
|
||||
if value == unicode(self.currentText()).strip():
|
||||
return key
|
||||
|
||||
|
||||
class KeyComboBox(QComboBox):
|
||||
|
||||
def __init__(self, parent, values, selected_key):
|
||||
QComboBox.__init__(self, parent)
|
||||
self.values = values
|
||||
self.populate_combo(selected_key)
|
||||
|
||||
def populate_combo(self, selected_key):
|
||||
self.clear()
|
||||
selected_idx = idx = -1
|
||||
for key in sorted(self.values.keys()):
|
||||
idx = idx + 1
|
||||
self.addItem(key)
|
||||
if key == selected_key:
|
||||
selected_idx = idx
|
||||
self.setCurrentIndex(selected_idx)
|
||||
|
||||
def selected_key(self):
|
||||
for key, value in self.values.iteritems():
|
||||
if key == unicode(self.currentText()).strip():
|
||||
return key
|
||||
|
||||
|
||||
class CustomColumnComboBox(QComboBox):
|
||||
|
||||
def __init__(self, parent, custom_columns={}, selected_column='', initial_items=['']):
|
||||
QComboBox.__init__(self, parent)
|
||||
self.populate_combo(custom_columns, selected_column, initial_items)
|
||||
|
||||
def populate_combo(self, custom_columns, selected_column, initial_items=['']):
|
||||
self.clear()
|
||||
self.column_names = list(initial_items)
|
||||
if len(initial_items) > 0:
|
||||
self.addItems(initial_items)
|
||||
selected_idx = 0
|
||||
for idx, value in enumerate(initial_items):
|
||||
if value == selected_column:
|
||||
selected_idx = idx
|
||||
for key in sorted(custom_columns.keys()):
|
||||
self.column_names.append(key)
|
||||
self.addItem('%s (%s)'%(key, custom_columns[key]['name']))
|
||||
if key == selected_column:
|
||||
selected_idx = len(self.column_names) - 1
|
||||
self.setCurrentIndex(selected_idx)
|
||||
|
||||
def get_selected_column(self):
|
||||
return self.column_names[self.currentIndex()]
|
||||
|
||||
|
||||
class KeyboardConfigDialog(SizePersistedDialog):
|
||||
'''
|
||||
This dialog is used to allow editing of keyboard shortcuts.
|
||||
'''
|
||||
def __init__(self, gui, group_name):
|
||||
SizePersistedDialog.__init__(self, gui, 'Keyboard shortcut dialog')
|
||||
self.gui = gui
|
||||
self.setWindowTitle('Keyboard shortcuts')
|
||||
layout = QVBoxLayout(self)
|
||||
self.setLayout(layout)
|
||||
|
||||
self.keyboard_widget = ShortcutConfig(self)
|
||||
layout.addWidget(self.keyboard_widget)
|
||||
self.group_name = group_name
|
||||
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
button_box.accepted.connect(self.commit)
|
||||
button_box.rejected.connect(self.reject)
|
||||
layout.addWidget(button_box)
|
||||
|
||||
# Cause our dialog size to be restored from prefs or created on first usage
|
||||
self.resize_dialog()
|
||||
self.initialize()
|
||||
|
||||
def initialize(self):
|
||||
self.keyboard_widget.initialize(self.gui.keyboard)
|
||||
self.keyboard_widget.highlight_group(self.group_name)
|
||||
|
||||
def commit(self):
|
||||
self.keyboard_widget.commit()
|
||||
self.accept()
|
||||
|
||||
|
||||
class ProgressBar(QDialog):
|
||||
def __init__(self, parent=None, max_items=100, window_title='Progress Bar',
|
||||
label='Label goes here', on_top=False):
|
||||
if on_top:
|
||||
QDialog.__init__(self, parent=parent, flags=Qt.WindowStaysOnTopHint)
|
||||
else:
|
||||
QDialog.__init__(self, parent=parent)
|
||||
self.application = Application
|
||||
self.setWindowTitle(window_title)
|
||||
self.l = QVBoxLayout(self)
|
||||
self.setLayout(self.l)
|
||||
|
||||
self.label = QLabel(label)
|
||||
self.label.setAlignment(Qt.AlignHCenter)
|
||||
self.l.addWidget(self.label)
|
||||
|
||||
self.progressBar = QProgressBar(self)
|
||||
self.progressBar.setRange(0, max_items)
|
||||
self.progressBar.setValue(0)
|
||||
self.l.addWidget(self.progressBar)
|
||||
|
||||
def increment(self):
|
||||
self.progressBar.setValue(self.progressBar.value() + 1)
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
self.application.processEvents()
|
||||
|
||||
def set_label(self, value):
|
||||
self.label.setText(value)
|
||||
self.refresh()
|
||||
|
||||
def set_maximum(self, value):
|
||||
self.progressBar.setMaximum(value)
|
||||
self.refresh()
|
||||
|
||||
def set_value(self, value):
|
||||
self.progressBar.setValue(value)
|
||||
self.refresh()
|
||||
|
||||
def convert_kobo_date(kobo_date):
|
||||
from calibre.utils.date import utc_tz
|
||||
|
||||
try:
|
||||
converted_date = datetime.strptime(kobo_date, "%Y-%m-%dT%H:%M:%S.%f")
|
||||
converted_date = datetime.strptime(kobo_date[0:19], "%Y-%m-%dT%H:%M:%S")
|
||||
converted_date = converted_date.replace(tzinfo=utc_tz)
|
||||
# debug_print("convert_kobo_date - '%Y-%m-%dT%H:%M:%S.%f' - kobo_date={0}'".format(kobo_date))
|
||||
except:
|
||||
try:
|
||||
converted_date = datetime.strptime(kobo_date, "%Y-%m-%dT%H:%M:%S%+00:00")
|
||||
# debug_print("convert_kobo_date - '%Y-%m-%dT%H:%M:%S+00:00' - kobo_date=%s' - kobo_date={0}'".format(kobo_date))
|
||||
except:
|
||||
try:
|
||||
converted_date = datetime.strptime(kobo_date.split('+')[0], "%Y-%m-%dT%H:%M:%S")
|
||||
converted_date = converted_date.replace(tzinfo=utc_tz)
|
||||
# debug_print("convert_kobo_date - '%Y-%m-%dT%H:%M:%S' - kobo_date={0}'".format(kobo_date))
|
||||
except:
|
||||
try:
|
||||
converted_date = datetime.strptime(kobo_date.split('+')[0], "%Y-%m-%d")
|
||||
converted_date = converted_date.replace(tzinfo=utc_tz)
|
||||
# debug_print("convert_kobo_date - '%Y-%m-%d' - kobo_date={0}'".format(kobo_date))
|
||||
except:
|
||||
try:
|
||||
from calibre.utils.date import parse_date
|
||||
converted_date = parse_date(kobo_date, assume_utc=True)
|
||||
# debug_print("convert_kobo_date - parse_date - kobo_date=%s' - kobo_date={0}'".format(kobo_date))
|
||||
except:
|
||||
# try:
|
||||
# converted_date = time.gmtime(os.path.getctime(self.path))
|
||||
# debug_print("convert_kobo_date - time.gmtime(os.path.getctime(self.path)) - kobo_date={0}'".format(kobo_date))
|
||||
# except:
|
||||
converted_date = time.gmtime()
|
||||
debug_print("convert_kobo_date - time.gmtime() - kobo_date={0}'".format(kobo_date))
|
||||
return converted_date
|
||||
223
Obok_calibre_plugin/obok_plugin/config.py
Normal file
223
Obok_calibre_plugin/obok_plugin/config.py
Normal file
@@ -0,0 +1,223 @@
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
try:
|
||||
from PyQt5.Qt import (Qt, QGroupBox, QListWidget, QLineEdit, QDialogButtonBox, QWidget, QLabel, QDialog, QVBoxLayout, QAbstractItemView, QIcon, QHBoxLayout, QComboBox, QListWidgetItem)
|
||||
except ImportError:
|
||||
from PyQt4.Qt import (Qt, QGroupBox, QListWidget, QLineEdit, QDialogButtonBox, QWidget, QLabel, QDialog, QVBoxLayout, QAbstractItemView, QIcon, QHBoxLayout, QComboBox, QListWidgetItem)
|
||||
|
||||
try:
|
||||
from PyQt5 import Qt as QtGui
|
||||
except ImportError:
|
||||
from PyQt4 import QtGui
|
||||
|
||||
from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url)
|
||||
from calibre.utils.config import JSONConfig, config_dir
|
||||
|
||||
plugin_prefs = JSONConfig('plugins/obok_dedrm_prefs')
|
||||
plugin_prefs.defaults['finding_homes_for_formats'] = 'Ask'
|
||||
plugin_prefs.defaults['kobo_serials'] = []
|
||||
|
||||
from calibre_plugins.obok_dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION
|
||||
from calibre_plugins.obok_dedrm.utilities import (debug_print)
|
||||
try:
|
||||
debug_print("obok::config.py - loading translations")
|
||||
load_translations()
|
||||
except NameError:
|
||||
debug_print("obok::config.py - exception when loading translations")
|
||||
pass # load_translations() added in calibre 1.9
|
||||
|
||||
class ConfigWidget(QWidget):
|
||||
def __init__(self, plugin_action):
|
||||
QWidget.__init__(self)
|
||||
self.plugin_action = plugin_action
|
||||
layout = QVBoxLayout(self)
|
||||
self.setLayout(layout)
|
||||
|
||||
# copy of preferences
|
||||
self.tmpserials = plugin_prefs['kobo_serials']
|
||||
|
||||
combo_label = QLabel(_('When should Obok try to insert EPUBs into existing calibre entries?'), self)
|
||||
layout.addWidget(combo_label)
|
||||
self.find_homes = QComboBox()
|
||||
self.find_homes.setToolTip(_('<p>Default behavior when duplicates are detected. None of the choices will cause calibre ebooks to be overwritten'))
|
||||
layout.addWidget(self.find_homes)
|
||||
self.find_homes.addItems([_('Ask'), _('Always'), _('Never')])
|
||||
index = self.find_homes.findText(plugin_prefs['finding_homes_for_formats'])
|
||||
self.find_homes.setCurrentIndex(index)
|
||||
|
||||
self.serials_button = QtGui.QPushButton(self)
|
||||
self.serials_button.setToolTip(_(u"Click to manage Kobo serial numbers for Kobo ebooks"))
|
||||
self.serials_button.setText(u"Kobo devices serials")
|
||||
self.serials_button.clicked.connect(self.edit_serials)
|
||||
layout.addWidget(self.serials_button)
|
||||
|
||||
|
||||
def edit_serials(self):
|
||||
d = ManageKeysDialog(self,u"Kobo device serial numbers",self.tmpserials, AddSerialDialog)
|
||||
d.exec_()
|
||||
|
||||
|
||||
def save_settings(self):
|
||||
plugin_prefs['finding_homes_for_formats'] = unicode(self.find_homes.currentText())
|
||||
plugin_prefs['kobo_serials'] = self.tmpserials
|
||||
|
||||
|
||||
|
||||
|
||||
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.json_file = (keyfile_ext == u"k4i")
|
||||
|
||||
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)
|
||||
|
||||
keys_group_box = QGroupBox(_(u"{0}s".format(self.key_type_name)), self)
|
||||
layout.addWidget(keys_group_box)
|
||||
keys_group_box_layout = QHBoxLayout()
|
||||
keys_group_box.setLayout(keys_group_box_layout)
|
||||
|
||||
self.listy = QListWidget(self)
|
||||
self.listy.setToolTip(u"{0}s that will be used to decrypt ebooks".format(self.key_type_name))
|
||||
self.listy.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
self.populate_list()
|
||||
keys_group_box_layout.addWidget(self.listy)
|
||||
|
||||
button_layout = QVBoxLayout()
|
||||
keys_group_box_layout.addLayout(button_layout)
|
||||
self._add_key_button = QtGui.QToolButton(self)
|
||||
self._add_key_button.setIcon(QIcon(I('plus.png')))
|
||||
self._add_key_button.setToolTip(u"Create new {0}".format(self.key_type_name))
|
||||
self._add_key_button.clicked.connect(self.add_key)
|
||||
button_layout.addWidget(self._add_key_button)
|
||||
|
||||
self._delete_key_button = QtGui.QToolButton(self)
|
||||
self._delete_key_button.setToolTip(_(u"Delete highlighted key"))
|
||||
self._delete_key_button.setIcon(QIcon(I('list_remove.png')))
|
||||
self._delete_key_button.clicked.connect(self.delete_key)
|
||||
button_layout.addWidget(self._delete_key_button)
|
||||
|
||||
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)
|
||||
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 new_key_value in self.plugin_keys:
|
||||
info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
|
||||
u"This {0} is already in the list of {0}s has not been added.".format(self.key_type_name), show=True)
|
||||
return
|
||||
|
||||
self.plugin_keys.append(d.key_value)
|
||||
self.listy.clear()
|
||||
self.populate_list()
|
||||
|
||||
def rename_key(self):
|
||||
if not self.listy.currentItem():
|
||||
errmsg = u"No {0} selected to rename. Highlight a keyfile first.".format(self.key_type_name)
|
||||
r = error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
|
||||
_(errmsg), show=True, show_copy_button=False)
|
||||
return
|
||||
|
||||
d = RenameKeyDialog(self)
|
||||
d.exec_()
|
||||
|
||||
if d.result() != d.Accepted:
|
||||
# rename cancelled or moot.
|
||||
return
|
||||
keyname = unicode(self.listy.currentItem().text())
|
||||
if not question_dialog(self, "{0} {1}: Confirm Rename".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to rename the {2} named <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())
|
||||
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
|
||||
self.plugin_keys.remove(keyname)
|
||||
|
||||
self.listy.clear()
|
||||
self.populate_list()
|
||||
|
||||
class AddSerialDialog(QDialog):
|
||||
def __init__(self, parent=None,):
|
||||
QDialog.__init__(self, parent)
|
||||
self.parent = parent
|
||||
self.setWindowTitle(u"{0} {1}: Add New eInk Kobo 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 Kobo Serial Number:", self))
|
||||
self.key_ledit = QLineEdit("", self)
|
||||
self.key_ledit.setToolTip(u"Enter an eInk Kobo serial number. EInk Kobo serial numbers are 13 characters long and usually start with a 'N'. Kobo Serial Numbers are case-sensitive, so be sure to enter the upper and lower case letters unchanged.")
|
||||
key_group.addWidget(self.key_ledit)
|
||||
|
||||
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
self.button_box.accepted.connect(self.accept)
|
||||
self.button_box.rejected.connect(self.reject)
|
||||
layout.addWidget(self.button_box)
|
||||
|
||||
self.resize(self.sizeHint())
|
||||
|
||||
@property
|
||||
def key_name(self):
|
||||
return unicode(self.key_ledit.text()).strip()
|
||||
|
||||
@property
|
||||
def key_value(self):
|
||||
return unicode(self.key_ledit.text()).strip()
|
||||
|
||||
def accept(self):
|
||||
if len(self.key_name) == 0 or self.key_name.isspace():
|
||||
errmsg = u"Please enter an eInk Kindle Serial Number or click Cancel in the dialog."
|
||||
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
if len(self.key_name) != 13:
|
||||
errmsg = u"EInk Kobo Serial Numbers must be 13 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)
|
||||
455
Obok_calibre_plugin/obok_plugin/dialogs.py
Normal file
455
Obok_calibre_plugin/obok_plugin/dialogs.py
Normal file
@@ -0,0 +1,455 @@
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
TEXT_DRM_FREE = ' (*: drm - free)'
|
||||
LAB_DRM_FREE = '* : drm - free'
|
||||
|
||||
try:
|
||||
from PyQt5.Qt import (Qt, QVBoxLayout, QLabel, QApplication, QGroupBox,
|
||||
QDialogButtonBox, QHBoxLayout, QTextBrowser, QProgressDialog,
|
||||
QTimer, QSize, QDialog, QIcon, QTableWidget, QTableWidgetItem)
|
||||
except ImportError:
|
||||
from PyQt4.Qt import (Qt, QVBoxLayout, QLabel, QApplication, QGroupBox,
|
||||
QDialogButtonBox, QHBoxLayout, QTextBrowser, QProgressDialog,
|
||||
QTimer, QSize, QDialog, QIcon, QTableWidget, QTableWidgetItem)
|
||||
|
||||
try:
|
||||
from PyQt5.QtWidgets import (QListWidget, QAbstractItemView)
|
||||
except ImportError:
|
||||
from PyQt4.QtGui import (QListWidget, QAbstractItemView)
|
||||
|
||||
from calibre.gui2 import gprefs, warning_dialog, error_dialog
|
||||
from calibre.gui2.dialogs.message_box import MessageBox
|
||||
|
||||
#from calibre.ptempfile import remove_dir
|
||||
|
||||
from calibre_plugins.obok_dedrm.utilities import (SizePersistedDialog, ImageTitleLayout,
|
||||
showErrorDlg, get_icon, convert_qvariant, debug_print
|
||||
)
|
||||
from calibre_plugins.obok_dedrm.__init__ import (PLUGIN_NAME,
|
||||
PLUGIN_SAFE_NAME, PLUGIN_VERSION, PLUGIN_DESCRIPTION)
|
||||
|
||||
try:
|
||||
debug_print("obok::dialogs.py - loading translations")
|
||||
load_translations()
|
||||
except NameError:
|
||||
debug_print("obok::dialogs.py - exception when loading translations")
|
||||
pass # load_translations() added in calibre 1.9
|
||||
|
||||
class SelectionDialog(SizePersistedDialog):
|
||||
'''
|
||||
Dialog to select the kobo books to decrypt
|
||||
'''
|
||||
def __init__(self, gui, interface_action, books):
|
||||
'''
|
||||
:param gui: Parent gui
|
||||
:param interface_action: InterfaceActionObject (InterfacePluginAction class from action.py)
|
||||
:param books: list of Kobo book
|
||||
'''
|
||||
|
||||
self.books = books
|
||||
self.gui = gui
|
||||
self.interface_action = interface_action
|
||||
self.books = books
|
||||
|
||||
SizePersistedDialog.__init__(self, gui, PLUGIN_NAME + 'plugin:selections dialog')
|
||||
self.setWindowTitle(_(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
|
||||
self.setMinimumWidth(300)
|
||||
self.setMinimumHeight(300)
|
||||
layout = QVBoxLayout(self)
|
||||
self.setLayout(layout)
|
||||
title_layout = ImageTitleLayout(self, 'images/obok.png', _('Obok DeDRM'))
|
||||
layout.addLayout(title_layout)
|
||||
|
||||
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)
|
||||
title_layout.addWidget(help_label)
|
||||
title_layout.setAlignment(Qt.AlignTop)
|
||||
|
||||
layout.addSpacing(5)
|
||||
main_layout = QHBoxLayout()
|
||||
layout.addLayout(main_layout)
|
||||
# self.listy = QListWidget()
|
||||
# self.listy.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
||||
# main_layout.addWidget(self.listy)
|
||||
# self.listy.addItems(books)
|
||||
self.books_table = BookListTableWidget(self)
|
||||
main_layout.addWidget(self.books_table)
|
||||
|
||||
layout.addSpacing(10)
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
button_box.accepted.connect(self._ok_clicked)
|
||||
button_box.rejected.connect(self.reject)
|
||||
self.select_all_button = button_box.addButton(_("Select All"), QDialogButtonBox.ResetRole)
|
||||
self.select_all_button.setToolTip(_("Select all books to add them to the calibre library."))
|
||||
self.select_all_button.clicked.connect(self._select_all_clicked)
|
||||
self.select_drm_button = button_box.addButton(_("All with DRM"), QDialogButtonBox.ResetRole)
|
||||
self.select_drm_button.setToolTip(_("Select all books with DRM."))
|
||||
self.select_drm_button.clicked.connect(self._select_drm_clicked)
|
||||
self.select_free_button = button_box.addButton(_("All DRM free"), QDialogButtonBox.ResetRole)
|
||||
self.select_free_button.setToolTip(_("Select all books without DRM."))
|
||||
self.select_free_button.clicked.connect(self._select_free_clicked)
|
||||
layout.addWidget(button_box)
|
||||
|
||||
# Cause our dialog size to be restored from prefs or created on first usage
|
||||
self.resize_dialog()
|
||||
self.books_table.populate_table(self.books)
|
||||
|
||||
def _select_all_clicked(self):
|
||||
self.books_table.select_all()
|
||||
|
||||
def _select_drm_clicked(self):
|
||||
self.books_table.select_drm(True)
|
||||
|
||||
def _select_free_clicked(self):
|
||||
self.books_table.select_drm(False)
|
||||
|
||||
def _help_link_activated(self, url):
|
||||
'''
|
||||
:param url: Dummy url to pass to the show_help method of the InterfacePluginAction class
|
||||
'''
|
||||
self.interface_action.show_help()
|
||||
|
||||
def _ok_clicked(self):
|
||||
'''
|
||||
Build an index of the selected titles
|
||||
'''
|
||||
if len(self.books_table.selectedItems()):
|
||||
self.accept()
|
||||
else:
|
||||
msg = 'You must make a selection!'
|
||||
showErrorDlg(msg, self)
|
||||
|
||||
def getBooks(self):
|
||||
'''
|
||||
Method to return the selected books
|
||||
'''
|
||||
return self.books_table.get_books()
|
||||
|
||||
|
||||
class BookListTableWidget(QTableWidget):
|
||||
|
||||
def __init__(self, parent):
|
||||
QTableWidget.__init__(self, parent)
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
|
||||
def populate_table(self, books):
|
||||
self.clear()
|
||||
self.setAlternatingRowColors(True)
|
||||
self.setRowCount(len(books))
|
||||
header_labels = ['DRM', _('Title'), _('Author'), _('Series'), 'book_id']
|
||||
self.setColumnCount(len(header_labels))
|
||||
self.setHorizontalHeaderLabels(header_labels)
|
||||
self.verticalHeader().setDefaultSectionSize(24)
|
||||
self.horizontalHeader().setStretchLastSection(True)
|
||||
|
||||
self.books = {}
|
||||
for row, book in enumerate(books):
|
||||
self.populate_table_row(row, book)
|
||||
self.books[row] = book
|
||||
|
||||
self.setSortingEnabled(False)
|
||||
self.resizeColumnsToContents()
|
||||
self.setMinimumColumnWidth(1, 100)
|
||||
self.setMinimumColumnWidth(2, 100)
|
||||
self.setMinimumSize(300, 0)
|
||||
if len(books) > 0:
|
||||
self.selectRow(0)
|
||||
self.hideColumn(4)
|
||||
self.setSortingEnabled(True)
|
||||
|
||||
def setMinimumColumnWidth(self, col, minimum):
|
||||
if self.columnWidth(col) < minimum:
|
||||
self.setColumnWidth(col, minimum)
|
||||
|
||||
def populate_table_row(self, row, book):
|
||||
if book.has_drm:
|
||||
icon = get_icon('drm-locked.png')
|
||||
val = 1
|
||||
else:
|
||||
icon = get_icon('drm-unlocked.png')
|
||||
val = 0
|
||||
|
||||
status_cell = IconWidgetItem(None, icon, val)
|
||||
status_cell.setData(Qt.UserRole, val)
|
||||
self.setItem(row, 0, status_cell)
|
||||
self.setItem(row, 1, ReadOnlyTableWidgetItem(book.title))
|
||||
self.setItem(row, 2, AuthorTableWidgetItem(book.author, book.author))
|
||||
self.setItem(row, 3, SeriesTableWidgetItem(book.series, book.series_index))
|
||||
self.setItem(row, 4, NumericTableWidgetItem(row))
|
||||
|
||||
def get_books(self):
|
||||
# debug_print("BookListTableWidget:get_books - self.books:", self.books)
|
||||
books = []
|
||||
if len(self.selectedItems()):
|
||||
for row in range(self.rowCount()):
|
||||
# debug_print("BookListTableWidget:get_books - row:", row)
|
||||
if self.item(row, 0).isSelected():
|
||||
book_num = convert_qvariant(self.item(row, 4).data(Qt.DisplayRole))
|
||||
debug_print("BookListTableWidget:get_books - book_num:", book_num)
|
||||
book = self.books[book_num]
|
||||
debug_print("BookListTableWidget:get_books - book:", book.title)
|
||||
books.append(book)
|
||||
return books
|
||||
|
||||
def select_all(self):
|
||||
self .selectAll()
|
||||
|
||||
def select_drm(self, has_drm):
|
||||
self.clearSelection()
|
||||
current_selection_mode = self.selectionMode()
|
||||
self.setSelectionMode(QAbstractItemView.MultiSelection)
|
||||
for row in range(self.rowCount()):
|
||||
# debug_print("BookListTableWidget:select_drm - row:", row)
|
||||
if convert_qvariant(self.item(row, 0).data(Qt.UserRole)) == 1:
|
||||
# debug_print("BookListTableWidget:select_drm - has DRM:", row)
|
||||
if has_drm:
|
||||
self.selectRow(row)
|
||||
else:
|
||||
# debug_print("BookListTableWidget:select_drm - DRM free:", row)
|
||||
if not has_drm:
|
||||
self.selectRow(row)
|
||||
self.setSelectionMode(current_selection_mode)
|
||||
|
||||
|
||||
class DecryptAddProgressDialog(QProgressDialog):
|
||||
'''
|
||||
Use the QTimer singleShot method to dole out books one at
|
||||
a time to the indicated callback function from action.py
|
||||
'''
|
||||
def __init__(self, gui, indices, callback_fn, db, db_type='calibre', status_msg_type='books', action_type=('Decrypting','Decryption')):
|
||||
'''
|
||||
:param gui: Parent gui
|
||||
:param indices: List of Kobo books or list calibre book maps (indicated by param db_type)
|
||||
:param callback_fn: the function from action.py that will do the heavy lifting (get_decrypted_kobo_books or add_new_books)
|
||||
:param db: kobo database object or calibre database cache (indicated by param db_type)
|
||||
:param db_type: string indicating what kind of database param db is
|
||||
:param status_msg_type: string to indicate what the ProgressDialog is operating on (cosmetic only)
|
||||
:param action_type: 2-Tuple of strings indicating what the ProgressDialog is doing to param status_msg_type (cosmetic only)
|
||||
'''
|
||||
|
||||
self.total_count = len(indices)
|
||||
QProgressDialog.__init__(self, '', 'Cancel', 0, self.total_count, gui)
|
||||
self.setMinimumWidth(500)
|
||||
self.indices, self.callback_fn, self.db, self.db_type = indices, callback_fn, db, db_type
|
||||
self.action_type, self.status_msg_type = action_type, status_msg_type
|
||||
self.gui = gui
|
||||
self.setWindowTitle('{0} {1} {2}...'.format(self.action_type[0], self.total_count, self.status_msg_type))
|
||||
self.i, self.successes, self.failures = 0, [], []
|
||||
QTimer.singleShot(0, self.do_book_action)
|
||||
self.exec_()
|
||||
|
||||
def do_book_action(self):
|
||||
if self.wasCanceled():
|
||||
return self.do_close()
|
||||
if self.i >= self.total_count:
|
||||
return self.do_close()
|
||||
book = self.indices[self.i]
|
||||
self.i += 1
|
||||
|
||||
# Get the title and build the caption and label text from the string parameters provided
|
||||
if self.db_type == 'calibre':
|
||||
dtitle = book[0].title
|
||||
elif self.db_type == 'kobo':
|
||||
dtitle = book.title
|
||||
self.setWindowTitle('{0} {1} {2} ({3} {4} failures)...'.format(self.action_type[0], self.total_count,
|
||||
self.status_msg_type, len(self.failures), self.action_type[1]))
|
||||
self.setLabelText('{0}: {1}'.format(self.action_type[0], dtitle))
|
||||
# If a calibre db, feed the calibre bookmap to action.py's add_new_books method
|
||||
if self.db_type == 'calibre':
|
||||
if self.callback_fn([book]):
|
||||
self.successes.append(book)
|
||||
else:
|
||||
self.failures.append(book)
|
||||
# If a kobo db, feed the index to the kobo book to action.py's get_decrypted_kobo_books method
|
||||
elif self.db_type == 'kobo':
|
||||
if self.callback_fn(book):
|
||||
debug_print("DecryptAddProgressDialog::do_book_action - decrypted book: '%s'" % dtitle)
|
||||
self.successes.append(book)
|
||||
else:
|
||||
debug_print("DecryptAddProgressDialog::do_book_action - book decryption failed: '%s'" % dtitle)
|
||||
self.failures.append(book)
|
||||
self.setValue(self.i)
|
||||
|
||||
# Lather, rinse, repeat.
|
||||
QTimer.singleShot(0, self.do_book_action)
|
||||
|
||||
def do_close(self):
|
||||
self.hide()
|
||||
self.gui = None
|
||||
|
||||
class AddEpubFormatsProgressDialog(QProgressDialog):
|
||||
'''
|
||||
Use the QTimer singleShot method to dole out epub formats one at
|
||||
a time to the indicated callback function from action.py
|
||||
'''
|
||||
def __init__(self, gui, entries, callback_fn, status_msg_type='formats', action_type=('Adding','Added')):
|
||||
'''
|
||||
:param gui: Parent gui
|
||||
:param entries: List of 3-tuples [(target calibre id, calibre metadata object, path to epub file)]
|
||||
:param callback_fn: the function from action.py that will do the heavy lifting (process_epub_formats)
|
||||
:param status_msg_type: string to indicate what the ProgressDialog is operating on (cosmetic only)
|
||||
:param action_type: 2-tuple of strings indicating what the ProgressDialog is doing to param status_msg_type (cosmetic only)
|
||||
'''
|
||||
|
||||
self.total_count = len(entries)
|
||||
QProgressDialog.__init__(self, '', 'Cancel', 0, self.total_count, gui)
|
||||
self.setMinimumWidth(500)
|
||||
self.entries, self.callback_fn = entries, callback_fn
|
||||
self.action_type, self.status_msg_type = action_type, status_msg_type
|
||||
self.gui = gui
|
||||
self.setWindowTitle('{0} {1} {2}...'.format(self.action_type[0], self.total_count, self.status_msg_type))
|
||||
self.i, self.successes, self.failures = 0, [], []
|
||||
QTimer.singleShot(0, self.do_book_action)
|
||||
self.exec_()
|
||||
|
||||
def do_book_action(self):
|
||||
if self.wasCanceled():
|
||||
return self.do_close()
|
||||
if self.i >= self.total_count:
|
||||
return self.do_close()
|
||||
epub_format = self.entries[self.i]
|
||||
self.i += 1
|
||||
|
||||
# assign the elements of the 3-tuple details to legible variables
|
||||
book_id, mi, path = epub_format[0], epub_format[1], epub_format[2]
|
||||
|
||||
# Get the title and build the caption and label text from the string parameters provided
|
||||
dtitle = mi.title
|
||||
self.setWindowTitle('{0} {1} {2} ({3} {4} failures)...'.format(self.action_type[0], self.total_count,
|
||||
self.status_msg_type, len(self.failures), self.action_type[1]))
|
||||
self.setLabelText('{0}: {1}'.format(self.action_type[0], dtitle))
|
||||
# Send the necessary elements to the process_epub_formats callback function (action.py)
|
||||
# and record the results
|
||||
if self.callback_fn(book_id, mi, path):
|
||||
self.successes.append((book_id, mi, path))
|
||||
else:
|
||||
self.failures.append((book_id, mi, path))
|
||||
self.setValue(self.i)
|
||||
|
||||
# Lather, rinse, repeat
|
||||
QTimer.singleShot(0, self.do_book_action)
|
||||
|
||||
def do_close(self):
|
||||
self.hide()
|
||||
self.gui = None
|
||||
|
||||
class ViewLog(QDialog):
|
||||
'''
|
||||
Show a detailed summary of results as html.
|
||||
'''
|
||||
def __init__(self, title, html, parent=None):
|
||||
'''
|
||||
:param title: Caption for window title
|
||||
:param html: HTML string log/report
|
||||
'''
|
||||
QDialog.__init__(self, parent)
|
||||
self.l = l = QVBoxLayout()
|
||||
self.setLayout(l)
|
||||
|
||||
self.tb = QTextBrowser(self)
|
||||
QApplication.setOverrideCursor(Qt.WaitCursor)
|
||||
# Rather than formatting the text in <pre> blocks like the calibre
|
||||
# ViewLog does, instead just format it inside divs to keep style formatting
|
||||
html = html.replace('\t',' ')#.replace('\n', '<br/>')
|
||||
html = html.replace('> ','> ')
|
||||
self.tb.setHtml('<div>{0}</div>'.format(html))
|
||||
QApplication.restoreOverrideCursor()
|
||||
l.addWidget(self.tb)
|
||||
|
||||
self.bb = QDialogButtonBox(QDialogButtonBox.Ok)
|
||||
self.bb.accepted.connect(self.accept)
|
||||
self.bb.rejected.connect(self.reject)
|
||||
self.copy_button = self.bb.addButton(_('Copy to clipboard'),
|
||||
self.bb.ActionRole)
|
||||
self.copy_button.setIcon(QIcon(I('edit-copy.png')))
|
||||
self.copy_button.clicked.connect(self.copy_to_clipboard)
|
||||
l.addWidget(self.bb)
|
||||
self.setModal(False)
|
||||
self.resize(QSize(700, 500))
|
||||
self.setWindowTitle(title)
|
||||
self.setWindowIcon(QIcon(I('dialog_information.png')))
|
||||
self.show()
|
||||
|
||||
def copy_to_clipboard(self):
|
||||
txt = self.tb.toPlainText()
|
||||
QApplication.clipboard().setText(txt)
|
||||
|
||||
|
||||
class ResultsSummaryDialog(MessageBox):
|
||||
def __init__(self, parent, title, msg, log='', det_msg=''):
|
||||
'''
|
||||
:param log: An HTML log
|
||||
:param title: The title for this popup
|
||||
:param msg: The msg to display
|
||||
:param det_msg: Detailed message
|
||||
'''
|
||||
MessageBox.__init__(self, MessageBox.INFO, title, msg,
|
||||
det_msg=det_msg, show_copy_button=False,
|
||||
parent=parent)
|
||||
self.log = log
|
||||
self.vlb = self.bb.addButton(_('View Report'), self.bb.ActionRole)
|
||||
self.vlb.setIcon(QIcon(I('dialog_information.png')))
|
||||
self.vlb.clicked.connect(self.show_log)
|
||||
self.det_msg_toggle.setVisible(bool(det_msg))
|
||||
self.vlb.setVisible(bool(log))
|
||||
|
||||
def show_log(self):
|
||||
self.log_viewer = ViewLog(PLUGIN_NAME + ' v' + PLUGIN_VERSION, self.log,
|
||||
parent=self)
|
||||
|
||||
|
||||
class ReadOnlyTableWidgetItem(QTableWidgetItem):
|
||||
def __init__(self, text):
|
||||
if text is None:
|
||||
text = ''
|
||||
QTableWidgetItem.__init__(self, text, QTableWidgetItem.UserType)
|
||||
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
|
||||
|
||||
class AuthorTableWidgetItem(ReadOnlyTableWidgetItem):
|
||||
def __init__(self, text, sort_key):
|
||||
ReadOnlyTableWidgetItem.__init__(self, text)
|
||||
self.sort_key = sort_key
|
||||
|
||||
#Qt uses a simple < check for sorting items, override this to use the sortKey
|
||||
def __lt__(self, other):
|
||||
return self.sort_key < other.sort_key
|
||||
|
||||
class SeriesTableWidgetItem(ReadOnlyTableWidgetItem):
|
||||
def __init__(self, series, series_index=None):
|
||||
display = ''
|
||||
if series:
|
||||
if series_index:
|
||||
from calibre.ebooks.metadata import fmt_sidx
|
||||
display = '%s [%s]' % (series, fmt_sidx(series_index))
|
||||
self.sortKey = '%s%04d' % (series, series_index)
|
||||
else:
|
||||
display = series
|
||||
self.sortKey = series
|
||||
ReadOnlyTableWidgetItem.__init__(self, display)
|
||||
|
||||
class IconWidgetItem(ReadOnlyTableWidgetItem):
|
||||
def __init__(self, text, icon, sort_key):
|
||||
ReadOnlyTableWidgetItem.__init__(self, text)
|
||||
if icon:
|
||||
self.setIcon(icon)
|
||||
self.sort_key = sort_key
|
||||
|
||||
#Qt uses a simple < check for sorting items, override this to use the sortKey
|
||||
def __lt__(self, other):
|
||||
return self.sort_key < other.sort_key
|
||||
|
||||
class NumericTableWidgetItem(QTableWidgetItem):
|
||||
|
||||
def __init__(self, number, is_read_only=False):
|
||||
QTableWidgetItem.__init__(self, '', QTableWidgetItem.UserType)
|
||||
self.setData(Qt.DisplayRole, number)
|
||||
if is_read_only:
|
||||
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user