mirror of
https://github.com/noDRM/DeDRM_tools.git
synced 2026-03-20 13:08:55 +00:00
Compare commits
109 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85e3db8f7c | ||
|
|
29338db228 | ||
|
|
608159d71b | ||
|
|
20e0850001 | ||
|
|
ffd7d41bcd | ||
|
|
75cad40804 | ||
|
|
18d6413467 | ||
|
|
7619ee4e0f | ||
|
|
a390d7a207 | ||
|
|
f04f9eca04 | ||
|
|
1ece09023c | ||
|
|
8d9f384492 | ||
|
|
c3fbb83dbc | ||
|
|
0f23eac3b8 | ||
|
|
bcdcb23d0d | ||
|
|
a252dd0da6 | ||
|
|
2042354788 | ||
|
|
9bea20c7ab | ||
|
|
415df655a1 | ||
|
|
346b3e312c | ||
|
|
f981a548a3 | ||
|
|
4084a49872 | ||
|
|
347ad3cc05 | ||
|
|
e4b702e241 | ||
|
|
691a3d6955 | ||
|
|
fa317dc1cf | ||
|
|
6f0c36b67a | ||
|
|
ceacdbbb1b | ||
|
|
ff03c68674 | ||
|
|
84d4e4e0c8 | ||
|
|
a553df50d7 | ||
|
|
0b244b5781 | ||
|
|
ab4597dfd7 | ||
|
|
82e9927ace | ||
|
|
528092c05d | ||
|
|
faa19cc19b | ||
|
|
e6592841b6 | ||
|
|
482ac15055 | ||
|
|
cb74bd8cef | ||
|
|
956f3034ad | ||
|
|
fca7eaab8e | ||
|
|
0df66bcfc0 | ||
|
|
20ab5b354d | ||
|
|
46ce2ce0ea | ||
|
|
ca59704dc4 | ||
|
|
17300283d0 | ||
|
|
92ce0396fe | ||
|
|
5eb3338423 | ||
|
|
d65dd1ab87 | ||
|
|
5d75018719 | ||
|
|
1c3a12425e | ||
|
|
6b4d621159 | ||
|
|
53c16c916b | ||
|
|
34231cc252 | ||
|
|
c2615c4d3b | ||
|
|
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 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -37,7 +37,6 @@ nosetests.xml
|
||||
coverage.xml
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
|
||||
@@ -1,19 +1,32 @@
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf1348\cocoasubrtf170
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf470
|
||||
{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fnil\fcharset134 STHeitiSC-Light;}
|
||||
{\colortbl;\red255\green255\blue255;}
|
||||
\paperw11900\paperh16840\vieww12000\viewh15840\viewkind0
|
||||
\deftab720
|
||||
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720\qc
|
||||
\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\pardeftab720
|
||||
\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\pardeftab720\qj
|
||||
\cf0 DeDRM is an application that packs all of the python dm removal software into one easy to use program that remembers preferences and settings.\
|
||||
\
|
||||
\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 \
|
||||
\
|
||||
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, Kindle for Android, other Barnes & Noble ePubs, eReader pdb ebooks, or Mobipocket ebooks, you must first run DeDRM application (by double-clicking it) and set some additional Preferences, depending on the origin of your ebook files:\
|
||||
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)
|
||||
@@ -22,11 +35,6 @@ To remove the DRM of Kindle ebooks from eInk Kindles, Kindle for Android, other
|
||||
|
||||
\b0 16 digit Serial Number, found in your Amazon account web pages.\
|
||||
|
||||
\b Android Kindle app
|
||||
\b0 : \
|
||||
backup.ab file retrieved with adb command-line tool using command\
|
||||
adb backup com.amazon.kindle\
|
||||
|
||||
\b Barnes & Noble (not from NOOK Study)
|
||||
\b0 : \
|
||||
Your account email and password, so the key can be retrieved from the Barnes & Noble servers.\
|
||||
@@ -44,35 +52,18 @@ A final preference is the destination folder for the DRM-free copies of your ebo
|
||||
\
|
||||
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\pardeftab720
|
||||
\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 or later 2.7.x version from {\field{\*\fldinst{HYPERLINK "http://python.org/"}}{\fldrslt http://python.org/}}. At the time of writing, the direct download link is {\field{\*\fldinst{HYPERLINK "http://www.python.org/ftp/python/2.7.3/python-2.7.3-macosx10.3.dmg"}}{\fldrslt http://www.python.org/ftp/python/2.7.3/python-2.7.3-macosx10.3.dmg}}.\
|
||||
\
|
||||
Mac OS X 10.5 and above: You do
|
||||
\i not
|
||||
\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 First Use for Mac OS X 10.9 and later\
|
||||
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720
|
||||
|
||||
\b0 \cf0 The application is not signed, so the first time you run it you will need to change your security options, or hold down the option key when double-clicking on the icon, or control-click or right-button to get the contextual menu to open it.\
|
||||
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720
|
||||
|
||||
\b \cf0 \
|
||||
\
|
||||
\b \
|
||||
Use
|
||||
\b0 \
|
||||
1. To set the preferences, double-click the application and follow the instructions in the dialogs.\
|
||||
@@ -81,15 +72,16 @@ Use
|
||||
\
|
||||
|
||||
\b Troubleshooting\
|
||||
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720
|
||||
\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.\
|
||||
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720
|
||||
\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\pardeftab720\partightenfactor0
|
||||
|
||||
\b Credits\
|
||||
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720
|
||||
\b \cf0 Credits\
|
||||
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720\partightenfactor0
|
||||
|
||||
\b0 \cf0 The original inept and ignoble scripts were by i
|
||||
\f1 \uc0\u9829
|
||||
|
||||
Binary file not shown.
@@ -24,7 +24,7 @@
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>droplet</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>DeDRM AppleScript 6.3.1 Written 2010–2015 by Apprentice Alf et al.</string>
|
||||
<string>DeDRM AppleScript 6.6.0 Written 2010–2018 by Apprentice Alf et al.</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>DeDRM</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -36,13 +36,13 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>6.3.1</string>
|
||||
<string>6.6.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>dplt</string>
|
||||
<key>LSRequiresCarbon</key>
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2010–2015 Apprentice Alf and Apprentice Harper</string>
|
||||
<string>Copyright © 2010–2018 Apprentice Alf</string>
|
||||
<key>WindowState</key>
|
||||
<dict>
|
||||
<key>bundleDividerCollapsed</key>
|
||||
@@ -56,9 +56,9 @@
|
||||
<key>name</key>
|
||||
<string>ScriptWindowState</string>
|
||||
<key>positionOfDivider</key>
|
||||
<real>652</real>
|
||||
<real>651</real>
|
||||
<key>savedFrame</key>
|
||||
<string>0 36 1680 991 0 0 1680 1027 </string>
|
||||
<string>0 37 1680 990 0 0 1680 1027 </string>
|
||||
<key>selectedTab</key>
|
||||
<string>log</string>
|
||||
</dict>
|
||||
|
||||
@@ -55,7 +55,7 @@ p {margin-top: 0}
|
||||
<li>And probably many more.</li>
|
||||
</ul>
|
||||
|
||||
<h3> For additional help read the <a href="http://apprenticealf.wordpress.com/2011/01/17/frequently-asked-questions-about-the-drm-removal-tools/">FAQs</a> at <a href="http://apprenticealf.wordpress.com">Apprentice Alf’s Blog</a> and ask questions in the comments section of the <a href="http://apprenticealf.wordpress.com/2012/09/10/drm-removal-tools-for-ebooks/">first post</a>.</h3>
|
||||
<h3> For additional help read the <a href="https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md">FAQs</a> at <a href="https://github.com/apprenticeharper/DeDRM_tools/">Apprentice Harpers’s GitHub repository</a>. You can ask questions in the comments section of the <a href="http://apprenticealf.wordpress.com/2012/09/10/drm-removal-tools-for-ebooks/">first post</a> at <a href="http://wordpress.com/apprenticealf/">Apprentice Alf's blog</a> or <a href="https://github.com/apprenticeharper/DeDRM_tools/issues">raise an issue</a>. </h3>
|
||||
|
||||
<h2>Linux Systems Only</h2>
|
||||
<h3>Generating decryption keys for Adobe Digital Editions and Kindle for PC</h3>
|
||||
|
||||
@@ -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>
|
||||
@@ -1,52 +0,0 @@
|
||||
<!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 serial numbers</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 serial numbers</h1>
|
||||
|
||||
<p>Amazon's Kindle for Android application uses an internal serial number that's 72 character long. Extracting that serial number 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 the Kindle for Android serial number</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 the ‘backup.ab’ file you obtained by using the adb command. The backup file will be processed to extract any serial numbers in it, and the numbers will be added to the list.</p>
|
||||
|
||||
<h3>Adding the Kindle for Android serial number manually</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 a new Kindle for Android serial number.</p>
|
||||
<ul>
|
||||
<li><span class="bold">Kindle for Android Serial Number:</span> this is the unique serial number of your device. You may have obtained this through using the old android.py script.</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>
|
||||
|
||||
<h3>Deleting Kindle for Android serial numbers:</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 Kindle serial number from the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.</p>
|
||||
|
||||
<p>Once done creating/deleting serial numbers, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.</p>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Binary file not shown.
@@ -2,6 +2,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# __init__.py for DeDRM_plugin
|
||||
# Copyright © 2008-2018 Apprentice Harper et al.
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
@@ -9,8 +13,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.
|
||||
#
|
||||
@@ -43,6 +45,26 @@ __docformat__ = 'restructuredtext en'
|
||||
# 6.2.2 - Added URL method for B&N/nook books
|
||||
# 6.3.0 - Added in Kindle for Android serial number solution
|
||||
# 6.3.1 - Version number bump for clarity
|
||||
# 6.3.2 - Fixed Kindle for Android help file
|
||||
# 6.3.3 - Bug fix for Kindle for PC support
|
||||
# 6.3.4 - Fixes for Kindle for Android, Linux, and Kobo 3.17
|
||||
# 6.3.5 - Fixes for Linux, and Kobo 3.19 and more logging
|
||||
# 6.3.6 - Fixes for ADE ePub and PDF introduced in 6.3.5
|
||||
# 6.4.0 - Updated for new Kindle for PC encryption
|
||||
# 6.4.1 - Fix for some new tags in Topaz ebooks.
|
||||
# 6.4.2 - Fix for more new tags in Topaz ebooks and very small Topaz ebooks
|
||||
# 6.4.3 - Fix for error that only appears when not in debug mode
|
||||
# Also includes fix for Macs with bonded ethernet ports
|
||||
# 6.5.0 - Big update to Macintosh app
|
||||
# Fix for some more 'new' tags in Topaz ebooks.
|
||||
# Fix an error in wineutils.py
|
||||
# 6.5.1 - Updated version number, added PDF check for DRM-free documents
|
||||
# 6.5.2 - Another Topaz fix
|
||||
# 6.5.3 - Warn about KFX files explicitly
|
||||
# 6.5.4 - Mac App Fix, improve PDF decryption, handle latest tcl changes in ActivePython
|
||||
# 6.5.5 - Finally a fix for the Windows non-ASCII user names.
|
||||
# 6.6.0 - Add kfx and kfx-zip as supported file types (also invoke this plugin if the original
|
||||
# imported format was azw8 since that may be converted to kfx)
|
||||
|
||||
|
||||
"""
|
||||
@@ -50,7 +72,7 @@ Decrypt DRMed ebooks.
|
||||
"""
|
||||
|
||||
PLUGIN_NAME = u"DeDRM"
|
||||
PLUGIN_VERSION_TUPLE = (6, 3, 1)
|
||||
PLUGIN_VERSION_TUPLE = (6, 6, 0)
|
||||
PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE])
|
||||
# Include an html helpfile in the plugin's zipfile with the following name.
|
||||
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
|
||||
@@ -82,8 +104,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)
|
||||
|
||||
@@ -93,8 +119,8 @@ class DeDRM(FileTypePlugin):
|
||||
supported_platforms = ['linux', 'osx', 'windows']
|
||||
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','pobi','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','azw8','tpz','kfx','kfx-zip'])
|
||||
on_import = True
|
||||
priority = 600
|
||||
|
||||
@@ -146,7 +172,7 @@ class DeDRM(FileTypePlugin):
|
||||
try:
|
||||
open(file_path,'wb').write(data)
|
||||
except:
|
||||
print u"{0} v{1}: Exception when copying needed library files after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
|
||||
print u"{0} v{1}: Exception when copying needed library files".format(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
traceback.print_exc()
|
||||
pass
|
||||
|
||||
@@ -270,8 +296,8 @@ class DeDRM(FileTypePlugin):
|
||||
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 after {2:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
|
||||
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
|
||||
# import the Adobe Adept ePub handler
|
||||
import calibre_plugins.dedrm.ineptepub as ineptepub
|
||||
@@ -293,11 +319,15 @@ class DeDRM(FileTypePlugin):
|
||||
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)
|
||||
@@ -357,16 +387,19 @@ class DeDRM(FileTypePlugin):
|
||||
except:
|
||||
print u"{0} v{1}: Exception when saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
|
||||
traceback.print_exc()
|
||||
print u"{0} v{1}: Decrypted with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
|
||||
# Return the modified PersistentTemporary file to calibre.
|
||||
return of.name
|
||||
|
||||
print u"{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
|
||||
except Exception, e:
|
||||
print u"{0} v{1}: Unexpected Exception trying a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
|
||||
traceback.print_exc()
|
||||
pass
|
||||
|
||||
# Something went wrong with decryption.
|
||||
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\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 after {2:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
|
||||
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
|
||||
# Not a Barnes & Noble nor an Adobe Adept
|
||||
# Import the fixed epub.
|
||||
@@ -465,8 +498,8 @@ class DeDRM(FileTypePlugin):
|
||||
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 after {2:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
|
||||
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
|
||||
|
||||
def KindleMobiDecrypt(self,path_to_ebook):
|
||||
@@ -522,7 +555,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")
|
||||
@@ -533,8 +566,8 @@ class DeDRM(FileTypePlugin):
|
||||
pass
|
||||
if not decoded:
|
||||
#if you reached here then no luck raise and exception
|
||||
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\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 after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
|
||||
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
|
||||
of = self.temporary_file(book.getBookExtension())
|
||||
book.getFile(of.name)
|
||||
@@ -568,8 +601,8 @@ 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)
|
||||
|
||||
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 after {2:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
|
||||
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
|
||||
|
||||
def run(self, path_to_ebook):
|
||||
@@ -582,7 +615,7 @@ class DeDRM(FileTypePlugin):
|
||||
self.starttime = time.time()
|
||||
|
||||
booktype = os.path.splitext(path_to_ebook)[1].lower()[1:]
|
||||
if booktype in ['prc','mobi','pobi','azw','azw1','azw3','azw4','tpz']:
|
||||
if booktype in ['prc','mobi','pobi','azw','azw1','azw3','azw4','tpz','kfx-zip']:
|
||||
# Kindle/Mobipocket
|
||||
decrypted_ebook = self.KindleMobiDecrypt(path_to_ebook)
|
||||
elif booktype == 'pdb':
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# adobekey.pyw, version 5.7
|
||||
# adobekey.pyw, version 6.0
|
||||
# Copyright © 2009-2010 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 2010–2016 by several people
|
||||
|
||||
# Windows users: Before running this program, you must first install Python.
|
||||
# We recommend ActiveState Python 2.7.X for Windows (x86) from
|
||||
|
||||
@@ -16,16 +16,18 @@ from __future__ import with_statement
|
||||
# - 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.4'
|
||||
__version__ = '1.5'
|
||||
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
import getopt
|
||||
import tempfile
|
||||
import zlib
|
||||
@@ -220,20 +222,30 @@ def get_serials2(path=STORAGE2):
|
||||
userdata_keys = cursor.fetchall()
|
||||
dsns = []
|
||||
for userdata_row in userdata_keys:
|
||||
if userdata_row:
|
||||
userdata_utf8 = userdata_row[0].encode('utf8')
|
||||
if len(userdata_utf8) > 0:
|
||||
dsns.append(userdata_utf8)
|
||||
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:
|
||||
if userdata_row:
|
||||
userdata_utf8 = userdata_row[0].encode('utf8')
|
||||
if len(userdata_utf8) > 0:
|
||||
tokens.append(userdata_utf8)
|
||||
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 = []
|
||||
@@ -377,7 +389,6 @@ def gui_main():
|
||||
import Tkconstants
|
||||
import tkMessageBox
|
||||
import tkFileDialog
|
||||
import traceback
|
||||
except:
|
||||
print "Tkinter not installed"
|
||||
return cli_main()
|
||||
|
||||
@@ -124,7 +124,7 @@ class ConfigWidget(QWidget):
|
||||
d.exec_()
|
||||
|
||||
def kindle_android(self):
|
||||
d = ManageKeysDialog(self,u"Kindle for Android Keys File",self.tempdedrmprefs['androidkeys'], AddAndroidDialog, 'k4a')
|
||||
d = ManageKeysDialog(self,u"Kindle for Android Key",self.tempdedrmprefs['androidkeys'], AddAndroidDialog, 'k4a')
|
||||
d.exec_()
|
||||
|
||||
def kindle_keys(self):
|
||||
@@ -566,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)
|
||||
@@ -579,8 +592,7 @@ class AddBandNKeyDialog(QDialog):
|
||||
|
||||
@property
|
||||
def key_value(self):
|
||||
from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key
|
||||
return fetch_bandn_key(self.user_name,self.cc_number)
|
||||
return unicode(self.key_display.text()).strip()
|
||||
|
||||
@property
|
||||
def user_name(self):
|
||||
@@ -590,6 +602,14 @@ class AddBandNKeyDialog(QDialog):
|
||||
def cc_number(self):
|
||||
return unicode(self.cc_ledit.text()).strip()
|
||||
|
||||
def retrieve_key(self):
|
||||
from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key
|
||||
fetched_key = fetch_bandn_key(self.user_name,self.cc_number)
|
||||
if fetched_key == "":
|
||||
errmsg = u"Could not retrieve key. Check username, password and intenet connectivity and try again."
|
||||
error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
else:
|
||||
self.key_display.setText(fetched_key)
|
||||
|
||||
def accept(self):
|
||||
if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace():
|
||||
@@ -598,6 +618,10 @@ class AddBandNKeyDialog(QDialog):
|
||||
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):
|
||||
|
||||
@@ -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,19 @@ 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),
|
||||
'src' : (1, 'text', 0, 0),
|
||||
|
||||
}
|
||||
|
||||
@@ -506,8 +528,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\cocoartf1348\cocoasubrtf170
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf470
|
||||
{\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)
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ignobleepub.pyw, version 3.8
|
||||
# ignobleepub.pyw, version 4.1
|
||||
# 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–2017 by Apprentice Harper
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# from <http://www.python.org/download/> and PyCrypto from
|
||||
@@ -35,13 +36,14 @@ from __future__ import with_statement
|
||||
# 3.8 - Fixed to retain zip file metadata (e.g. file modification date)
|
||||
# 3.9 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 4.0 - Work if TkInter is missing
|
||||
# 4.1 - Import tkFileDialog, don't assume something else will import it.
|
||||
|
||||
"""
|
||||
Decrypt Barnes & Noble encrypted ePub books.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "4.0"
|
||||
__version__ = "4.1"
|
||||
|
||||
import sys
|
||||
import os
|
||||
@@ -337,6 +339,7 @@ def gui_main():
|
||||
try:
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
import traceback
|
||||
except:
|
||||
|
||||
@@ -3,18 +3,19 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ineptepub.pyw, version 6.1
|
||||
# ineptepub.pyw, version 6.6
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2013 by some_updates, DiapDealer and Apprentice Alf
|
||||
# Modified 2015–2017 by Apprentice Harper
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# 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
|
||||
@@ -38,13 +39,17 @@ from __future__ import with_statement
|
||||
# 6.0 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 6.1 - Work if TkInter is missing
|
||||
# 6.2 - Handle UTF-8 file names inside an ePub, fix by Jose Luis
|
||||
# 6.3 - Add additional check on DER file sanity
|
||||
# 6.4 - Remove erroneous check on DER file sanity
|
||||
# 6.5 - Completely remove erroneous check on DER file sanity
|
||||
# 6.6 - Import tkFileDialog, don't assume something else will import it.
|
||||
|
||||
"""
|
||||
Decrypt Adobe Digital Editions encrypted ePub books.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "6.2"
|
||||
__version__ = "6.6"
|
||||
|
||||
import sys
|
||||
import os
|
||||
@@ -480,6 +485,7 @@ def gui_main():
|
||||
try:
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
import traceback
|
||||
except:
|
||||
|
||||
@@ -3,18 +3,19 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ineptpdf.pyw, version 7.11
|
||||
# ineptpdf.pyw, version 8.0.6
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
# Modified 2015-2017 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,19 @@ 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
|
||||
# 8.0.5 - Do not process DRM-free documents
|
||||
# 8.0.6 - Replace use of float by Decimal for greater precision, and import tkFileDialog
|
||||
|
||||
|
||||
"""
|
||||
Decrypts Adobe ADEPT-encrypted PDF files.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "8.0.1"
|
||||
__version__ = "8.0.6"
|
||||
|
||||
import sys
|
||||
import os
|
||||
@@ -67,6 +74,7 @@ import re
|
||||
import zlib
|
||||
import struct
|
||||
import hashlib
|
||||
from decimal import *
|
||||
from itertools import chain, islice
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
@@ -647,7 +655,7 @@ class PSBaseParser(object):
|
||||
return (self.parse_number, j+1)
|
||||
if c == '.':
|
||||
self.token = c
|
||||
return (self.parse_float, j+1)
|
||||
return (self.parse_decimal, j+1)
|
||||
if c.isalpha():
|
||||
self.token = c
|
||||
return (self.parse_keyword, j+1)
|
||||
@@ -712,20 +720,21 @@ class PSBaseParser(object):
|
||||
c = s[j]
|
||||
if c == '.':
|
||||
self.token += c
|
||||
return (self.parse_float, j+1)
|
||||
return (self.parse_decimal, j+1)
|
||||
try:
|
||||
self.add_token(int(self.token))
|
||||
except ValueError:
|
||||
pass
|
||||
return (self.parse_main, j)
|
||||
def parse_float(self, s, i):
|
||||
|
||||
def parse_decimal(self, s, i):
|
||||
m = END_NUMBER.search(s, i)
|
||||
if not m:
|
||||
self.token += s[i:]
|
||||
return (self.parse_float, len(s))
|
||||
return (self.parse_decimal, len(s))
|
||||
j = m.start(0)
|
||||
self.token += s[i:j]
|
||||
self.add_token(float(self.token))
|
||||
self.add_token(Decimal(self.token))
|
||||
return (self.parse_main, j)
|
||||
|
||||
def parse_keyword(self, s, i):
|
||||
@@ -927,7 +936,7 @@ class PSStackParser(PSBaseParser):
|
||||
(pos, token) = self.nexttoken()
|
||||
##print (pos,token), (self.curtype, self.curstack)
|
||||
if (isinstance(token, int) or
|
||||
isinstance(token, float) or
|
||||
isinstance(token, Decimal) or
|
||||
isinstance(token, bool) or
|
||||
isinstance(token, str) or
|
||||
isinstance(token, PSLiteral)):
|
||||
@@ -1056,17 +1065,17 @@ def int_value(x):
|
||||
return 0
|
||||
return x
|
||||
|
||||
def float_value(x):
|
||||
def decimal_value(x):
|
||||
x = resolve1(x)
|
||||
if not isinstance(x, float):
|
||||
if not isinstance(x, Decimal):
|
||||
if STRICT:
|
||||
raise PDFTypeError('Float required: %r' % x)
|
||||
raise PDFTypeError('Decimal required: %r' % x)
|
||||
return 0.0
|
||||
return x
|
||||
|
||||
def num_value(x):
|
||||
x = resolve1(x)
|
||||
if not (isinstance(x, int) or isinstance(x, float)):
|
||||
if not (isinstance(x, int) or isinstance(x, Decimal)):
|
||||
if STRICT:
|
||||
raise PDFTypeError('Int or Float required: %r' % x)
|
||||
return 0
|
||||
@@ -1463,6 +1472,7 @@ class PDFDocument(object):
|
||||
if not self.encryption:
|
||||
self.is_printable = self.is_modifiable = self.is_extractable = True
|
||||
self.ready = True
|
||||
raise PDFEncryptionError('Document is not encrypted.')
|
||||
return
|
||||
(docid, param) = self.encryption
|
||||
type = literal_name(param['Filter'])
|
||||
@@ -2135,7 +2145,11 @@ class PDFSerializer(object):
|
||||
if self.last.isalnum():
|
||||
self.write(' ')
|
||||
self.write(str(obj).lower())
|
||||
elif isinstance(obj, (int, long, float)):
|
||||
elif isinstance(obj, (int, long)):
|
||||
if self.last.isalnum():
|
||||
self.write(' ')
|
||||
self.write(str(obj))
|
||||
elif isinstance(obj, Decimal):
|
||||
if self.last.isalnum():
|
||||
self.write(' ')
|
||||
self.write(str(obj))
|
||||
@@ -2175,11 +2189,11 @@ def decryptBook(userkey, inpath, outpath):
|
||||
if RSA is None:
|
||||
raise ADEPTError(u"PyCrypto or OpenSSL must be installed.")
|
||||
with open(inpath, 'rb') as inf:
|
||||
try:
|
||||
serializer = PDFSerializer(inf, userkey)
|
||||
except:
|
||||
print u"Error serializing pdf {0}. Probably wrong key.".format(os.path.basename(inpath))
|
||||
return 2
|
||||
#try:
|
||||
serializer = PDFSerializer(inf, userkey)
|
||||
#except:
|
||||
# print u"Error serializing pdf {0}. Probably wrong key.".format(os.path.basename(inpath))
|
||||
# return 2
|
||||
# hope this will fix the 'bad file descriptor' problem
|
||||
with open(outpath, 'wb') as outf:
|
||||
# help construct to make sure the method runs to the end
|
||||
@@ -2211,6 +2225,7 @@ def gui_main():
|
||||
try:
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
import traceback
|
||||
except:
|
||||
|
||||
981
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ion.py
Normal file
981
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ion.py
Normal file
@@ -0,0 +1,981 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Pascal implementation by lulzkabulz. Python translation by apprenticenaomi. DeDRM integration by anon.
|
||||
# BinaryIon.pas + DrmIon.pas + IonSymbols.pas
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import collections
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
import os.path
|
||||
import struct
|
||||
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except ImportError:
|
||||
from StringIO import StringIO
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.py3compat import bchr, bord
|
||||
|
||||
try:
|
||||
# lzma library from calibre 2.35.0 or later
|
||||
import lzma.lzma1 as calibre_lzma
|
||||
except:
|
||||
calibre_lzma = None
|
||||
try:
|
||||
import lzma
|
||||
except:
|
||||
# Need pip backports.lzma on Python <3.3
|
||||
from backports import lzma
|
||||
|
||||
|
||||
TID_NULL = 0
|
||||
TID_BOOLEAN = 1
|
||||
TID_POSINT = 2
|
||||
TID_NEGINT = 3
|
||||
TID_FLOAT = 4
|
||||
TID_DECIMAL = 5
|
||||
TID_TIMESTAMP = 6
|
||||
TID_SYMBOL = 7
|
||||
TID_STRING = 8
|
||||
TID_CLOB = 9
|
||||
TID_BLOB = 0xA
|
||||
TID_LIST = 0xB
|
||||
TID_SEXP = 0xC
|
||||
TID_STRUCT = 0xD
|
||||
TID_TYPEDECL = 0xE
|
||||
TID_UNUSED = 0xF
|
||||
|
||||
|
||||
SID_UNKNOWN = -1
|
||||
SID_ION = 1
|
||||
SID_ION_1_0 = 2
|
||||
SID_ION_SYMBOL_TABLE = 3
|
||||
SID_NAME = 4
|
||||
SID_VERSION = 5
|
||||
SID_IMPORTS = 6
|
||||
SID_SYMBOLS = 7
|
||||
SID_MAX_ID = 8
|
||||
SID_ION_SHARED_SYMBOL_TABLE = 9
|
||||
SID_ION_1_0_MAX = 10
|
||||
|
||||
|
||||
LEN_IS_VAR_LEN = 0xE
|
||||
LEN_IS_NULL = 0xF
|
||||
|
||||
|
||||
VERSION_MARKER = b"\x01\x00\xEA"
|
||||
|
||||
|
||||
# asserts must always raise exceptions for proper functioning
|
||||
def _assert(test, msg="Exception"):
|
||||
if not test:
|
||||
raise Exception(msg)
|
||||
|
||||
|
||||
class SystemSymbols(object):
|
||||
ION = '$ion'
|
||||
ION_1_0 = '$ion_1_0'
|
||||
ION_SYMBOL_TABLE = '$ion_symbol_table'
|
||||
NAME = 'name'
|
||||
VERSION = 'version'
|
||||
IMPORTS = 'imports'
|
||||
SYMBOLS = 'symbols'
|
||||
MAX_ID = 'max_id'
|
||||
ION_SHARED_SYMBOL_TABLE = '$ion_shared_symbol_table'
|
||||
|
||||
|
||||
class IonCatalogItem(object):
|
||||
name = ""
|
||||
version = 0
|
||||
symnames = []
|
||||
|
||||
def __init__(self, name, version, symnames):
|
||||
self.name = name
|
||||
self.version = version
|
||||
self.symnames = symnames
|
||||
|
||||
|
||||
class SymbolToken(object):
|
||||
text = ""
|
||||
sid = 0
|
||||
|
||||
def __init__(self, text, sid):
|
||||
if text == "" and sid == 0:
|
||||
raise ValueError("Symbol token must have Text or SID")
|
||||
|
||||
self.text = text
|
||||
self.sid = sid
|
||||
|
||||
|
||||
class SymbolTable(object):
|
||||
table = None
|
||||
|
||||
def __init__(self):
|
||||
self.table = [None] * SID_ION_1_0_MAX
|
||||
self.table[SID_ION] = SystemSymbols.ION
|
||||
self.table[SID_ION_1_0] = SystemSymbols.ION_1_0
|
||||
self.table[SID_ION_SYMBOL_TABLE] = SystemSymbols.ION_SYMBOL_TABLE
|
||||
self.table[SID_NAME] = SystemSymbols.NAME
|
||||
self.table[SID_VERSION] = SystemSymbols.VERSION
|
||||
self.table[SID_IMPORTS] = SystemSymbols.IMPORTS
|
||||
self.table[SID_SYMBOLS] = SystemSymbols.SYMBOLS
|
||||
self.table[SID_MAX_ID] = SystemSymbols.MAX_ID
|
||||
self.table[SID_ION_SHARED_SYMBOL_TABLE] = SystemSymbols.ION_SHARED_SYMBOL_TABLE
|
||||
|
||||
def findbyid(self, sid):
|
||||
if sid < 1:
|
||||
raise ValueError("Invalid symbol id")
|
||||
|
||||
if sid < len(self.table):
|
||||
return self.table[sid]
|
||||
else:
|
||||
return ""
|
||||
|
||||
def import_(self, table, maxid):
|
||||
for i in range(maxid):
|
||||
self.table.append(table.symnames[i])
|
||||
|
||||
def importunknown(self, name, maxid):
|
||||
for i in range(maxid):
|
||||
self.table.append("%s#%d" % (name, i + 1))
|
||||
|
||||
|
||||
class ParserState:
|
||||
Invalid,BeforeField,BeforeTID,BeforeValue,AfterValue,EOF = 1,2,3,4,5,6
|
||||
|
||||
ContainerRec = collections.namedtuple("ContainerRec", "nextpos, tid, remaining")
|
||||
|
||||
|
||||
class BinaryIonParser(object):
|
||||
eof = False
|
||||
state = None
|
||||
localremaining = 0
|
||||
needhasnext = False
|
||||
isinstruct = False
|
||||
valuetid = 0
|
||||
valuefieldid = 0
|
||||
parenttid = 0
|
||||
valuelen = 0
|
||||
valueisnull = False
|
||||
valueistrue = False
|
||||
value = None
|
||||
didimports = False
|
||||
|
||||
def __init__(self, stream):
|
||||
self.annotations = []
|
||||
self.catalog = []
|
||||
|
||||
self.stream = stream
|
||||
self.initpos = stream.tell()
|
||||
self.reset()
|
||||
self.symbols = SymbolTable()
|
||||
|
||||
def reset(self):
|
||||
self.state = ParserState.BeforeTID
|
||||
self.needhasnext = True
|
||||
self.localremaining = -1
|
||||
self.eof = False
|
||||
self.isinstruct = False
|
||||
self.containerstack = []
|
||||
self.stream.seek(self.initpos)
|
||||
|
||||
def addtocatalog(self, name, version, symbols):
|
||||
self.catalog.append(IonCatalogItem(name, version, symbols))
|
||||
|
||||
def hasnext(self):
|
||||
while self.needhasnext and not self.eof:
|
||||
self.hasnextraw()
|
||||
if len(self.containerstack) == 0 and not self.valueisnull:
|
||||
if self.valuetid == TID_SYMBOL:
|
||||
if self.value == SID_ION_1_0:
|
||||
self.needhasnext = True
|
||||
elif self.valuetid == TID_STRUCT:
|
||||
for a in self.annotations:
|
||||
if a == SID_ION_SYMBOL_TABLE:
|
||||
self.parsesymboltable()
|
||||
self.needhasnext = True
|
||||
break
|
||||
return not self.eof
|
||||
|
||||
def hasnextraw(self):
|
||||
self.clearvalue()
|
||||
while self.valuetid == -1 and not self.eof:
|
||||
self.needhasnext = False
|
||||
if self.state == ParserState.BeforeField:
|
||||
_assert(self.valuefieldid == SID_UNKNOWN)
|
||||
|
||||
self.valuefieldid = self.readfieldid()
|
||||
if self.valuefieldid != SID_UNKNOWN:
|
||||
self.state = ParserState.BeforeTID
|
||||
else:
|
||||
self.eof = True
|
||||
|
||||
elif self.state == ParserState.BeforeTID:
|
||||
self.state = ParserState.BeforeValue
|
||||
self.valuetid = self.readtypeid()
|
||||
if self.valuetid == -1:
|
||||
self.state = ParserState.EOF
|
||||
self.eof = True
|
||||
break
|
||||
|
||||
if self.valuetid == TID_TYPEDECL:
|
||||
if self.valuelen == 0:
|
||||
self.checkversionmarker()
|
||||
else:
|
||||
self.loadannotations()
|
||||
|
||||
elif self.state == ParserState.BeforeValue:
|
||||
self.skip(self.valuelen)
|
||||
self.state = ParserState.AfterValue
|
||||
|
||||
elif self.state == ParserState.AfterValue:
|
||||
if self.isinstruct:
|
||||
self.state = ParserState.BeforeField
|
||||
else:
|
||||
self.state = ParserState.BeforeTID
|
||||
|
||||
else:
|
||||
_assert(self.state == ParserState.EOF)
|
||||
|
||||
def next(self):
|
||||
if self.hasnext():
|
||||
self.needhasnext = True
|
||||
return self.valuetid
|
||||
else:
|
||||
return -1
|
||||
|
||||
def push(self, typeid, nextposition, nextremaining):
|
||||
self.containerstack.append(ContainerRec(nextpos=nextposition, tid=typeid, remaining=nextremaining))
|
||||
|
||||
def stepin(self):
|
||||
_assert(self.valuetid in [TID_STRUCT, TID_LIST, TID_SEXP] and not self.eof,
|
||||
"valuetid=%s eof=%s" % (self.valuetid, self.eof))
|
||||
_assert((not self.valueisnull or self.state == ParserState.AfterValue) and
|
||||
(self.valueisnull or self.state == ParserState.BeforeValue))
|
||||
|
||||
nextrem = self.localremaining
|
||||
if nextrem != -1:
|
||||
nextrem -= self.valuelen
|
||||
if nextrem < 0:
|
||||
nextrem = 0
|
||||
self.push(self.parenttid, self.stream.tell() + self.valuelen, nextrem)
|
||||
|
||||
self.isinstruct = (self.valuetid == TID_STRUCT)
|
||||
if self.isinstruct:
|
||||
self.state = ParserState.BeforeField
|
||||
else:
|
||||
self.state = ParserState.BeforeTID
|
||||
|
||||
self.localremaining = self.valuelen
|
||||
self.parenttid = self.valuetid
|
||||
self.clearvalue()
|
||||
self.needhasnext = True
|
||||
|
||||
def stepout(self):
|
||||
rec = self.containerstack.pop()
|
||||
|
||||
self.eof = False
|
||||
self.parenttid = rec.tid
|
||||
if self.parenttid == TID_STRUCT:
|
||||
self.isinstruct = True
|
||||
self.state = ParserState.BeforeField
|
||||
else:
|
||||
self.isinstruct = False
|
||||
self.state = ParserState.BeforeTID
|
||||
self.needhasnext = True
|
||||
|
||||
self.clearvalue()
|
||||
curpos = self.stream.tell()
|
||||
if rec.nextpos > curpos:
|
||||
self.skip(rec.nextpos - curpos)
|
||||
else:
|
||||
_assert(rec.nextpos == curpos)
|
||||
|
||||
self.localremaining = rec.remaining
|
||||
|
||||
def read(self, count=1):
|
||||
if self.localremaining != -1:
|
||||
self.localremaining -= count
|
||||
_assert(self.localremaining >= 0)
|
||||
|
||||
result = self.stream.read(count)
|
||||
if len(result) == 0:
|
||||
raise EOFError()
|
||||
return result
|
||||
|
||||
def readfieldid(self):
|
||||
if self.localremaining != -1 and self.localremaining < 1:
|
||||
return -1
|
||||
|
||||
try:
|
||||
return self.readvaruint()
|
||||
except EOFError:
|
||||
return -1
|
||||
|
||||
def readtypeid(self):
|
||||
if self.localremaining != -1:
|
||||
if self.localremaining < 1:
|
||||
return -1
|
||||
self.localremaining -= 1
|
||||
|
||||
b = self.stream.read(1)
|
||||
if len(b) < 1:
|
||||
return -1
|
||||
b = bord(b)
|
||||
result = b >> 4
|
||||
ln = b & 0xF
|
||||
|
||||
if ln == LEN_IS_VAR_LEN:
|
||||
ln = self.readvaruint()
|
||||
elif ln == LEN_IS_NULL:
|
||||
ln = 0
|
||||
self.state = ParserState.AfterValue
|
||||
elif result == TID_NULL:
|
||||
# Must have LEN_IS_NULL
|
||||
_assert(False)
|
||||
elif result == TID_BOOLEAN:
|
||||
_assert(ln <= 1)
|
||||
self.valueistrue = (ln == 1)
|
||||
ln = 0
|
||||
self.state = ParserState.AfterValue
|
||||
elif result == TID_STRUCT:
|
||||
if ln == 1:
|
||||
ln = self.readvaruint()
|
||||
|
||||
self.valuelen = ln
|
||||
return result
|
||||
|
||||
def readvarint(self):
|
||||
b = bord(self.read())
|
||||
negative = ((b & 0x40) != 0)
|
||||
result = (b & 0x3F)
|
||||
|
||||
i = 0
|
||||
while (b & 0x80) == 0 and i < 4:
|
||||
b = bord(self.read())
|
||||
result = (result << 7) | (b & 0x7F)
|
||||
i += 1
|
||||
|
||||
_assert(i < 4 or (b & 0x80) != 0, "int overflow")
|
||||
|
||||
if negative:
|
||||
return -result
|
||||
return result
|
||||
|
||||
def readvaruint(self):
|
||||
b = bord(self.read())
|
||||
result = (b & 0x7F)
|
||||
|
||||
i = 0
|
||||
while (b & 0x80) == 0 and i < 4:
|
||||
b = bord(self.read())
|
||||
result = (result << 7) | (b & 0x7F)
|
||||
i += 1
|
||||
|
||||
_assert(i < 4 or (b & 0x80) != 0, "int overflow")
|
||||
|
||||
return result
|
||||
|
||||
def readdecimal(self):
|
||||
if self.valuelen == 0:
|
||||
return 0.
|
||||
|
||||
rem = self.localremaining - self.valuelen
|
||||
self.localremaining = self.valuelen
|
||||
exponent = self.readvarint()
|
||||
|
||||
_assert(self.localremaining > 0, "Only exponent in ReadDecimal")
|
||||
_assert(self.localremaining <= 8, "Decimal overflow")
|
||||
|
||||
signed = False
|
||||
b = [bord(x) for x in self.read(self.localremaining)]
|
||||
if (b[0] & 0x80) != 0:
|
||||
b[0] = b[0] & 0x7F
|
||||
signed = True
|
||||
|
||||
# Convert variably sized network order integer into 64-bit little endian
|
||||
j = 0
|
||||
vb = [0] * 8
|
||||
for i in range(len(b), -1, -1):
|
||||
vb[i] = b[j]
|
||||
j += 1
|
||||
|
||||
v = struct.unpack("<Q", b"".join(bchr(x) for x in vb))[0]
|
||||
|
||||
result = v * (10 ** exponent)
|
||||
if signed:
|
||||
result = -result
|
||||
|
||||
self.localremaining = rem
|
||||
return result
|
||||
|
||||
def skip(self, count):
|
||||
if self.localremaining != -1:
|
||||
self.localremaining -= count
|
||||
if self.localremaining < 0:
|
||||
raise EOFError()
|
||||
|
||||
self.stream.seek(count, os.SEEK_CUR)
|
||||
|
||||
def parsesymboltable(self):
|
||||
self.next() # shouldn't do anything?
|
||||
|
||||
_assert(self.valuetid == TID_STRUCT)
|
||||
|
||||
if self.didimports:
|
||||
return
|
||||
|
||||
self.stepin()
|
||||
|
||||
fieldtype = self.next()
|
||||
while fieldtype != -1:
|
||||
if not self.valueisnull:
|
||||
_assert(self.valuefieldid == SID_IMPORTS, "Unsupported symbol table field id")
|
||||
|
||||
if fieldtype == TID_LIST:
|
||||
self.gatherimports()
|
||||
|
||||
fieldtype = self.next()
|
||||
|
||||
self.stepout()
|
||||
self.didimports = True
|
||||
|
||||
def gatherimports(self):
|
||||
self.stepin()
|
||||
|
||||
t = self.next()
|
||||
while t != -1:
|
||||
if not self.valueisnull and t == TID_STRUCT:
|
||||
self.readimport()
|
||||
|
||||
t = self.next()
|
||||
|
||||
self.stepout()
|
||||
|
||||
def readimport(self):
|
||||
version = -1
|
||||
maxid = -1
|
||||
name = ""
|
||||
|
||||
self.stepin()
|
||||
|
||||
t = self.next()
|
||||
while t != -1:
|
||||
if not self.valueisnull and self.valuefieldid != SID_UNKNOWN:
|
||||
if self.valuefieldid == SID_NAME:
|
||||
name = self.stringvalue()
|
||||
elif self.valuefieldid == SID_VERSION:
|
||||
version = self.intvalue()
|
||||
elif self.valuefieldid == SID_MAX_ID:
|
||||
maxid = self.intvalue()
|
||||
|
||||
t = self.next()
|
||||
|
||||
self.stepout()
|
||||
|
||||
if name == "" or name == SystemSymbols.ION:
|
||||
return
|
||||
|
||||
if version < 1:
|
||||
version = 1
|
||||
|
||||
table = self.findcatalogitem(name)
|
||||
if maxid < 0:
|
||||
_assert(table is not None and version == table.version, "Import %s lacks maxid" % name)
|
||||
maxid = len(table.symnames)
|
||||
|
||||
if table is not None:
|
||||
self.symbols.import_(table, min(maxid, len(table.symnames)))
|
||||
else:
|
||||
self.symbols.importunknown(name, maxid)
|
||||
|
||||
def intvalue(self):
|
||||
_assert(self.valuetid in [TID_POSINT, TID_NEGINT], "Not an int")
|
||||
|
||||
self.preparevalue()
|
||||
return self.value
|
||||
|
||||
def stringvalue(self):
|
||||
_assert(self.valuetid == TID_STRING, "Not a string")
|
||||
|
||||
if self.valueisnull:
|
||||
return ""
|
||||
|
||||
self.preparevalue()
|
||||
return self.value
|
||||
|
||||
def symbolvalue(self):
|
||||
_assert(self.valuetid == TID_SYMBOL, "Not a symbol")
|
||||
|
||||
self.preparevalue()
|
||||
result = self.symbols.findbyid(self.value)
|
||||
if result == "":
|
||||
result = "SYMBOL#%d" % self.value
|
||||
return result
|
||||
|
||||
def lobvalue(self):
|
||||
_assert(self.valuetid in [TID_CLOB, TID_BLOB], "Not a LOB type: %s" % self.getfieldname())
|
||||
|
||||
if self.valueisnull:
|
||||
return None
|
||||
|
||||
result = self.read(self.valuelen)
|
||||
self.state = ParserState.AfterValue
|
||||
return result
|
||||
|
||||
def decimalvalue(self):
|
||||
_assert(self.valuetid == TID_DECIMAL, "Not a decimal")
|
||||
|
||||
self.preparevalue()
|
||||
return self.value
|
||||
|
||||
def preparevalue(self):
|
||||
if self.value is None:
|
||||
self.loadscalarvalue()
|
||||
|
||||
def loadscalarvalue(self):
|
||||
if self.valuetid not in [TID_NULL, TID_BOOLEAN, TID_POSINT, TID_NEGINT,
|
||||
TID_FLOAT, TID_DECIMAL, TID_TIMESTAMP,
|
||||
TID_SYMBOL, TID_STRING]:
|
||||
return
|
||||
|
||||
if self.valueisnull:
|
||||
self.value = None
|
||||
return
|
||||
|
||||
if self.valuetid == TID_STRING:
|
||||
self.value = self.read(self.valuelen).decode("UTF-8")
|
||||
|
||||
elif self.valuetid in (TID_POSINT, TID_NEGINT, TID_SYMBOL):
|
||||
if self.valuelen == 0:
|
||||
self.value = 0
|
||||
else:
|
||||
_assert(self.valuelen <= 4, "int too long: %d" % self.valuelen)
|
||||
v = 0
|
||||
for i in range(self.valuelen - 1, -1, -1):
|
||||
v = (v | (bord(self.read()) << (i * 8)))
|
||||
|
||||
if self.valuetid == TID_NEGINT:
|
||||
self.value = -v
|
||||
else:
|
||||
self.value = v
|
||||
|
||||
elif self.valuetid == TID_DECIMAL:
|
||||
self.value = self.readdecimal()
|
||||
|
||||
#else:
|
||||
# _assert(False, "Unhandled scalar type %d" % self.valuetid)
|
||||
|
||||
self.state = ParserState.AfterValue
|
||||
|
||||
def clearvalue(self):
|
||||
self.valuetid = -1
|
||||
self.value = None
|
||||
self.valueisnull = False
|
||||
self.valuefieldid = SID_UNKNOWN
|
||||
self.annotations = []
|
||||
|
||||
def loadannotations(self):
|
||||
ln = self.readvaruint()
|
||||
maxpos = self.stream.tell() + ln
|
||||
while self.stream.tell() < maxpos:
|
||||
self.annotations.append(self.readvaruint())
|
||||
self.valuetid = self.readtypeid()
|
||||
|
||||
def checkversionmarker(self):
|
||||
for i in VERSION_MARKER:
|
||||
_assert(self.read() == i, "Unknown version marker")
|
||||
|
||||
self.valuelen = 0
|
||||
self.valuetid = TID_SYMBOL
|
||||
self.value = SID_ION_1_0
|
||||
self.valueisnull = False
|
||||
self.valuefieldid = SID_UNKNOWN
|
||||
self.state = ParserState.AfterValue
|
||||
|
||||
def findcatalogitem(self, name):
|
||||
for result in self.catalog:
|
||||
if result.name == name:
|
||||
return result
|
||||
|
||||
def forceimport(self, symbols):
|
||||
item = IonCatalogItem("Forced", 1, symbols)
|
||||
self.symbols.import_(item, len(symbols))
|
||||
|
||||
def getfieldname(self):
|
||||
if self.valuefieldid == SID_UNKNOWN:
|
||||
return ""
|
||||
return self.symbols.findbyid(self.valuefieldid)
|
||||
|
||||
def getfieldnamesymbol(self):
|
||||
return SymbolToken(self.getfieldname(), self.valuefieldid)
|
||||
|
||||
def gettypename(self):
|
||||
if len(self.annotations) == 0:
|
||||
return ""
|
||||
|
||||
return self.symbols.findbyid(self.annotations[0])
|
||||
|
||||
@staticmethod
|
||||
def printlob(b):
|
||||
if b is None:
|
||||
return "null"
|
||||
|
||||
result = ""
|
||||
for i in b:
|
||||
result += ("%02x " % bord(i))
|
||||
|
||||
if len(result) > 0:
|
||||
result = result[:-1]
|
||||
return result
|
||||
|
||||
def ionwalk(self, supert, indent, lst):
|
||||
while self.hasnext():
|
||||
if supert == TID_STRUCT:
|
||||
L = self.getfieldname() + ":"
|
||||
else:
|
||||
L = ""
|
||||
|
||||
t = self.next()
|
||||
if t in [TID_STRUCT, TID_LIST]:
|
||||
if L != "":
|
||||
lst.append(indent + L)
|
||||
L = self.gettypename()
|
||||
if L != "":
|
||||
lst.append(indent + L + "::")
|
||||
if t == TID_STRUCT:
|
||||
lst.append(indent + "{")
|
||||
else:
|
||||
lst.append(indent + "[")
|
||||
|
||||
self.stepin()
|
||||
self.ionwalk(t, indent + " ", lst)
|
||||
self.stepout()
|
||||
|
||||
if t == TID_STRUCT:
|
||||
lst.append(indent + "}")
|
||||
else:
|
||||
lst.append(indent + "]")
|
||||
|
||||
else:
|
||||
if t == TID_STRING:
|
||||
L += ('"%s"' % self.stringvalue())
|
||||
elif t in [TID_CLOB, TID_BLOB]:
|
||||
L += ("{%s}" % self.printlob(self.lobvalue()))
|
||||
elif t == TID_POSINT:
|
||||
L += str(self.intvalue())
|
||||
elif t == TID_SYMBOL:
|
||||
tn = self.gettypename()
|
||||
if tn != "":
|
||||
tn += "::"
|
||||
L += tn + self.symbolvalue()
|
||||
elif t == TID_DECIMAL:
|
||||
L += str(self.decimalvalue())
|
||||
else:
|
||||
L += ("TID %d" % t)
|
||||
lst.append(indent + L)
|
||||
|
||||
def print_(self, lst):
|
||||
self.reset()
|
||||
self.ionwalk(-1, "", lst)
|
||||
|
||||
|
||||
SYM_NAMES = [ 'com.amazon.drm.Envelope@1.0',
|
||||
'com.amazon.drm.EnvelopeMetadata@1.0', 'size', 'page_size',
|
||||
'encryption_key', 'encryption_transformation',
|
||||
'encryption_voucher', 'signing_key', 'signing_algorithm',
|
||||
'signing_voucher', 'com.amazon.drm.EncryptedPage@1.0',
|
||||
'cipher_text', 'cipher_iv', 'com.amazon.drm.Signature@1.0',
|
||||
'data', 'com.amazon.drm.EnvelopeIndexTable@1.0', 'length',
|
||||
'offset', 'algorithm', 'encoded', 'encryption_algorithm',
|
||||
'hashing_algorithm', 'expires', 'format', 'id',
|
||||
'lock_parameters', 'strategy', 'com.amazon.drm.Key@1.0',
|
||||
'com.amazon.drm.KeySet@1.0', 'com.amazon.drm.PIDv3@1.0',
|
||||
'com.amazon.drm.PlainTextPage@1.0',
|
||||
'com.amazon.drm.PlainText@1.0', 'com.amazon.drm.PrivateKey@1.0',
|
||||
'com.amazon.drm.PublicKey@1.0', 'com.amazon.drm.SecretKey@1.0',
|
||||
'com.amazon.drm.Voucher@1.0', 'public_key', 'private_key',
|
||||
'com.amazon.drm.KeyPair@1.0', 'com.amazon.drm.ProtectedData@1.0',
|
||||
'doctype', 'com.amazon.drm.EnvelopeIndexTableOffset@1.0',
|
||||
'enddoc', 'license_type', 'license', 'watermark', 'key', 'value',
|
||||
'com.amazon.drm.License@1.0', 'category', 'metadata',
|
||||
'categorized_metadata', 'com.amazon.drm.CategorizedMetadata@1.0',
|
||||
'com.amazon.drm.VoucherEnvelope@1.0', 'mac', 'voucher',
|
||||
'com.amazon.drm.ProtectedData@2.0',
|
||||
'com.amazon.drm.Envelope@2.0',
|
||||
'com.amazon.drm.EnvelopeMetadata@2.0',
|
||||
'com.amazon.drm.EncryptedPage@2.0',
|
||||
'com.amazon.drm.PlainText@2.0', 'compression_algorithm',
|
||||
'com.amazon.drm.Compressed@1.0', 'priority', 'refines']
|
||||
|
||||
def addprottable(ion):
|
||||
ion.addtocatalog("ProtectedData", 1, SYM_NAMES)
|
||||
|
||||
|
||||
def pkcs7pad(msg, blocklen):
|
||||
paddinglen = blocklen - len(msg) % blocklen
|
||||
padding = bchr(paddinglen) * paddinglen
|
||||
return msg + padding
|
||||
|
||||
|
||||
def pkcs7unpad(msg, blocklen):
|
||||
_assert(len(msg) % blocklen == 0)
|
||||
|
||||
paddinglen = bord(msg[-1])
|
||||
_assert(paddinglen > 0 and paddinglen <= blocklen, "Incorrect padding - Wrong key")
|
||||
_assert(msg[-paddinglen:] == bchr(paddinglen) * paddinglen, "Incorrect padding - Wrong key")
|
||||
|
||||
return msg[:-paddinglen]
|
||||
|
||||
|
||||
class DrmIonVoucher(object):
|
||||
envelope = None
|
||||
voucher = None
|
||||
drmkey = None
|
||||
license_type = "Unknown"
|
||||
|
||||
encalgorithm = ""
|
||||
enctransformation = ""
|
||||
hashalgorithm = ""
|
||||
|
||||
lockparams = None
|
||||
|
||||
ciphertext = b""
|
||||
cipheriv = b""
|
||||
secretkey = b""
|
||||
|
||||
def __init__(self, voucherenv, dsn, secret):
|
||||
self.dsn,self.secret = dsn,secret
|
||||
|
||||
self.lockparams = []
|
||||
|
||||
self.envelope = BinaryIonParser(voucherenv)
|
||||
addprottable(self.envelope)
|
||||
|
||||
def decryptvoucher(self):
|
||||
shared = "PIDv3" + self.encalgorithm + self.enctransformation + self.hashalgorithm
|
||||
|
||||
self.lockparams.sort()
|
||||
for param in self.lockparams:
|
||||
if param == "ACCOUNT_SECRET":
|
||||
shared += param + self.secret
|
||||
elif param == "CLIENT_ID":
|
||||
shared += param + self.dsn
|
||||
else:
|
||||
_assert(False, "Unknown lock parameter: %s" % param)
|
||||
|
||||
sharedsecret = shared.encode("UTF-8")
|
||||
|
||||
key = hmac.new(sharedsecret, sharedsecret[:5], digestmod=hashlib.sha256).digest()
|
||||
aes = AES.new(key[:32], AES.MODE_CBC, self.cipheriv[:16])
|
||||
b = aes.decrypt(self.ciphertext)
|
||||
b = pkcs7unpad(b, 16)
|
||||
|
||||
self.drmkey = BinaryIonParser(StringIO(b))
|
||||
addprottable(self.drmkey)
|
||||
|
||||
_assert(self.drmkey.hasnext() and self.drmkey.next() == TID_LIST and self.drmkey.gettypename() == "com.amazon.drm.KeySet@1.0",
|
||||
"Expected KeySet, got %s" % self.drmkey.gettypename())
|
||||
|
||||
self.drmkey.stepin()
|
||||
while self.drmkey.hasnext():
|
||||
self.drmkey.next()
|
||||
if self.drmkey.gettypename() != "com.amazon.drm.SecretKey@1.0":
|
||||
continue
|
||||
|
||||
self.drmkey.stepin()
|
||||
while self.drmkey.hasnext():
|
||||
self.drmkey.next()
|
||||
if self.drmkey.getfieldname() == "algorithm":
|
||||
_assert(self.drmkey.stringvalue() == "AES", "Unknown cipher algorithm: %s" % self.drmkey.stringvalue())
|
||||
elif self.drmkey.getfieldname() == "format":
|
||||
_assert(self.drmkey.stringvalue() == "RAW", "Unknown key format: %s" % self.drmkey.stringvalue())
|
||||
elif self.drmkey.getfieldname() == "encoded":
|
||||
self.secretkey = self.drmkey.lobvalue()
|
||||
|
||||
self.drmkey.stepout()
|
||||
break
|
||||
|
||||
self.drmkey.stepout()
|
||||
|
||||
def parse(self):
|
||||
self.envelope.reset()
|
||||
_assert(self.envelope.hasnext(), "Envelope is empty")
|
||||
_assert(self.envelope.next() == TID_STRUCT and self.envelope.gettypename() == "com.amazon.drm.VoucherEnvelope@1.0",
|
||||
"Unknown type encountered in envelope, expected VoucherEnvelope")
|
||||
|
||||
self.envelope.stepin()
|
||||
while self.envelope.hasnext():
|
||||
self.envelope.next()
|
||||
field = self.envelope.getfieldname()
|
||||
if field == "voucher":
|
||||
self.voucher = BinaryIonParser(StringIO(self.envelope.lobvalue()))
|
||||
addprottable(self.voucher)
|
||||
continue
|
||||
elif field != "strategy":
|
||||
continue
|
||||
|
||||
_assert(self.envelope.gettypename() == "com.amazon.drm.PIDv3@1.0", "Unknown strategy: %s" % self.envelope.gettypename())
|
||||
|
||||
self.envelope.stepin()
|
||||
while self.envelope.hasnext():
|
||||
self.envelope.next()
|
||||
field = self.envelope.getfieldname()
|
||||
if field == "encryption_algorithm":
|
||||
self.encalgorithm = self.envelope.stringvalue()
|
||||
elif field == "encryption_transformation":
|
||||
self.enctransformation = self.envelope.stringvalue()
|
||||
elif field == "hashing_algorithm":
|
||||
self.hashalgorithm = self.envelope.stringvalue()
|
||||
elif field == "lock_parameters":
|
||||
self.envelope.stepin()
|
||||
while self.envelope.hasnext():
|
||||
_assert(self.envelope.next() == TID_STRING, "Expected string list for lock_parameters")
|
||||
self.lockparams.append(self.envelope.stringvalue())
|
||||
self.envelope.stepout()
|
||||
|
||||
self.envelope.stepout()
|
||||
|
||||
self.parsevoucher()
|
||||
|
||||
def parsevoucher(self):
|
||||
_assert(self.voucher.hasnext(), "Voucher is empty")
|
||||
_assert(self.voucher.next() == TID_STRUCT and self.voucher.gettypename() == "com.amazon.drm.Voucher@1.0",
|
||||
"Unknown type, expected Voucher")
|
||||
|
||||
self.voucher.stepin()
|
||||
while self.voucher.hasnext():
|
||||
self.voucher.next()
|
||||
|
||||
if self.voucher.getfieldname() == "cipher_iv":
|
||||
self.cipheriv = self.voucher.lobvalue()
|
||||
elif self.voucher.getfieldname() == "cipher_text":
|
||||
self.ciphertext = self.voucher.lobvalue()
|
||||
elif self.voucher.getfieldname() == "license":
|
||||
_assert(self.voucher.gettypename() == "com.amazon.drm.License@1.0",
|
||||
"Unknown license: %s" % self.voucher.gettypename())
|
||||
self.voucher.stepin()
|
||||
while self.voucher.hasnext():
|
||||
self.voucher.next()
|
||||
if self.voucher.getfieldname() == "license_type":
|
||||
self.license_type = self.voucher.stringvalue()
|
||||
self.voucher.stepout()
|
||||
|
||||
def printenvelope(self, lst):
|
||||
self.envelope.print_(lst)
|
||||
|
||||
def printkey(self, lst):
|
||||
if self.voucher is None:
|
||||
self.parse()
|
||||
if self.drmkey is None:
|
||||
self.decryptvoucher()
|
||||
|
||||
self.drmkey.print_(lst)
|
||||
|
||||
def printvoucher(self, lst):
|
||||
if self.voucher is None:
|
||||
self.parse()
|
||||
|
||||
self.voucher.print_(lst)
|
||||
|
||||
def getlicensetype(self):
|
||||
return self.license_type
|
||||
|
||||
|
||||
class DrmIon(object):
|
||||
ion = None
|
||||
voucher = None
|
||||
vouchername = ""
|
||||
key = b""
|
||||
onvoucherrequired = None
|
||||
|
||||
def __init__(self, ionstream, onvoucherrequired):
|
||||
self.ion = BinaryIonParser(ionstream)
|
||||
addprottable(self.ion)
|
||||
self.onvoucherrequired = onvoucherrequired
|
||||
|
||||
def parse(self, outpages):
|
||||
self.ion.reset()
|
||||
|
||||
_assert(self.ion.hasnext(), "DRMION envelope is empty")
|
||||
_assert(self.ion.next() == TID_SYMBOL and self.ion.gettypename() == "doctype", "Expected doctype symbol")
|
||||
_assert(self.ion.next() == TID_LIST and self.ion.gettypename() in ["com.amazon.drm.Envelope@1.0", "com.amazon.drm.Envelope@2.0"],
|
||||
"Unknown type encountered in DRMION envelope, expected Envelope, got %s" % self.ion.gettypename())
|
||||
|
||||
while True:
|
||||
if self.ion.gettypename() == "enddoc":
|
||||
break
|
||||
|
||||
self.ion.stepin()
|
||||
while self.ion.hasnext():
|
||||
self.ion.next()
|
||||
|
||||
if self.ion.gettypename() in ["com.amazon.drm.EnvelopeMetadata@1.0", "com.amazon.drm.EnvelopeMetadata@2.0"]:
|
||||
self.ion.stepin()
|
||||
while self.ion.hasnext():
|
||||
self.ion.next()
|
||||
if self.ion.getfieldname() != "encryption_voucher":
|
||||
continue
|
||||
|
||||
if self.vouchername == "":
|
||||
self.vouchername = self.ion.stringvalue()
|
||||
self.voucher = self.onvoucherrequired(self.vouchername)
|
||||
self.key = self.voucher.secretkey
|
||||
_assert(self.key is not None, "Unable to obtain secret key from voucher")
|
||||
else:
|
||||
_assert(self.vouchername == self.ion.stringvalue(),
|
||||
"Unexpected: Different vouchers required for same file?")
|
||||
|
||||
self.ion.stepout()
|
||||
|
||||
elif self.ion.gettypename() in ["com.amazon.drm.EncryptedPage@1.0", "com.amazon.drm.EncryptedPage@2.0"]:
|
||||
decompress = False
|
||||
ct = None
|
||||
civ = None
|
||||
self.ion.stepin()
|
||||
while self.ion.hasnext():
|
||||
self.ion.next()
|
||||
if self.ion.gettypename() == "com.amazon.drm.Compressed@1.0":
|
||||
decompress = True
|
||||
if self.ion.getfieldname() == "cipher_text":
|
||||
ct = self.ion.lobvalue()
|
||||
elif self.ion.getfieldname() == "cipher_iv":
|
||||
civ = self.ion.lobvalue()
|
||||
|
||||
if ct is not None and civ is not None:
|
||||
self.processpage(ct, civ, outpages, decompress)
|
||||
self.ion.stepout()
|
||||
|
||||
self.ion.stepout()
|
||||
if not self.ion.hasnext():
|
||||
break
|
||||
self.ion.next()
|
||||
|
||||
def print_(self, lst):
|
||||
self.ion.print_(lst)
|
||||
|
||||
def processpage(self, ct, civ, outpages, decompress):
|
||||
aes = AES.new(self.key[:16], AES.MODE_CBC, civ[:16])
|
||||
msg = pkcs7unpad(aes.decrypt(ct), 16)
|
||||
|
||||
if not decompress:
|
||||
outpages.write(msg)
|
||||
return
|
||||
|
||||
_assert(msg[0] == b"\x00", "LZMA UseFilter not supported")
|
||||
|
||||
if calibre_lzma is not None:
|
||||
with calibre_lzma.decompress(msg[1:], bufsize=0x1000000) as f:
|
||||
f.seek(0)
|
||||
outpages.write(f.read())
|
||||
return
|
||||
|
||||
decomp = lzma.LZMADecompressor(format=lzma.FORMAT_ALONE)
|
||||
while not decomp.eof:
|
||||
segment = decomp.decompress(msg[1:])
|
||||
msg = b"" # Contents were internally buffered after the first call
|
||||
outpages.write(segment)
|
||||
@@ -3,10 +3,13 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# k4mobidedrm.py, version 5.3
|
||||
# Copyright © 2009-2015 by ApprenticeHarper et al.
|
||||
# k4mobidedrm.py
|
||||
# Copyright © 2008-2017 by Apprentice Harper et al.
|
||||
|
||||
# engine to remove drm from Kindle and Mobipocket ebooks
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '5.5'
|
||||
|
||||
# Engine to remove drm from Kindle and Mobipocket ebooks
|
||||
# for personal use for archiving and converting your ebooks
|
||||
|
||||
# PLEASE DO NOT PIRATE EBOOKS!
|
||||
@@ -17,12 +20,11 @@ from __future__ import with_statement
|
||||
# readable for a long, long time
|
||||
|
||||
# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
|
||||
# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates
|
||||
# and many many others
|
||||
# unswindle, DarkReverser, ApprenticeAlf, and many many others
|
||||
|
||||
# Special thanks to The Dark Reverser for MobiDeDrm and CMBDTC for cmbdtc_dump
|
||||
# from which this script borrows most unashamedly.
|
||||
|
||||
|
||||
# Changelog
|
||||
# 1.0 - Name change to k4mobidedrm. Adds Mac support, Adds plugin code
|
||||
# 1.1 - Adds support for additional kindle.info files
|
||||
@@ -56,9 +58,9 @@ from __future__ import with_statement
|
||||
# 5.1 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 5.2 - Fixed error in command line processing of unicode arguments
|
||||
# 5.3 - Changed Android support to allow passing of backup .ab files
|
||||
|
||||
__version__ = '5.3'
|
||||
|
||||
# 5.4 - Recognise KFX files masquerading as azw, even if we can't decrypt them yet.
|
||||
# 5.5 - Added GPL v3 licence explicitly.
|
||||
# 5.x - Invoke KFXZipBook to handle zipped KFX files
|
||||
|
||||
import sys, os, re
|
||||
import csv
|
||||
@@ -82,11 +84,13 @@ if inCalibre:
|
||||
from calibre_plugins.dedrm import topazextract
|
||||
from calibre_plugins.dedrm import kgenpids
|
||||
from calibre_plugins.dedrm import androidkindlekey
|
||||
from calibre_plugins.dedrm import kfxdedrm
|
||||
else:
|
||||
import mobidedrm
|
||||
import topazextract
|
||||
import kgenpids
|
||||
import androidkindlekey
|
||||
import kfxdedrm
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
@@ -194,11 +198,17 @@ def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime
|
||||
raise DrmException(u"Input file does not exist.")
|
||||
|
||||
mobi = True
|
||||
magic3 = open(infile,'rb').read(3)
|
||||
magic8 = open(infile,'rb').read(8)
|
||||
if magic8 == '\xeaDRMION\xee':
|
||||
raise DrmException(u"The .kfx DRMION file cannot be decrypted by itself. A .kfx-zip archive containing a DRM voucher is required.")
|
||||
|
||||
magic3 = magic8[:3]
|
||||
if magic3 == 'TPZ':
|
||||
mobi = False
|
||||
|
||||
if mobi:
|
||||
if magic8[:4] == 'PK\x03\x04':
|
||||
mb = kfxdedrm.KFXZipBook(infile)
|
||||
elif mobi:
|
||||
mb = mobidedrm.MobiBook(infile)
|
||||
else:
|
||||
mb = topazextract.TopazBook(infile)
|
||||
@@ -215,8 +225,9 @@ def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime
|
||||
md1, md2 = mb.getPIDMetaInfo()
|
||||
totalpids.extend(kgenpids.getPidList(md1, md2, serials, kDatabases))
|
||||
# remove any duplicates
|
||||
totalpid = list(set(totalpids))
|
||||
totalpids = list(set(totalpids))
|
||||
print u"Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(totalpids))
|
||||
#print totalpids
|
||||
|
||||
try:
|
||||
mb.processBook(totalpids)
|
||||
@@ -289,7 +300,7 @@ def usage(progname):
|
||||
def cli_main():
|
||||
argv=unicode_argv()
|
||||
progname = os.path.basename(argv[0])
|
||||
print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2013 The Dark Reverser et al.".format(__version__)
|
||||
print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2017 Apprentice Harper et al.".format(__version__)
|
||||
|
||||
try:
|
||||
opts, args = getopt.getopt(argv[1:], "k:p:s:a:")
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# Engine to remove drm from Kindle KFX ebooks
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import zipfile
|
||||
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except ImportError:
|
||||
from StringIO import StringIO
|
||||
|
||||
try:
|
||||
import ion
|
||||
except:
|
||||
from calibre_plugins.dedrm import ion
|
||||
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '1.0'
|
||||
|
||||
|
||||
class KFXZipBook:
|
||||
def __init__(self, infile):
|
||||
self.infile = infile
|
||||
self.voucher = None
|
||||
self.decrypted = {}
|
||||
|
||||
def getPIDMetaInfo(self):
|
||||
return (None, None)
|
||||
|
||||
def processBook(self, totalpids):
|
||||
with zipfile.ZipFile(self.infile, 'r') as zf:
|
||||
for filename in zf.namelist():
|
||||
data = zf.read(filename)
|
||||
if data.startswith('\xeaDRMION\xee'):
|
||||
if self.voucher is None:
|
||||
self.decrypt_voucher(totalpids)
|
||||
print u'Decrypting KFX DRMION: {0}'.format(filename)
|
||||
outfile = StringIO()
|
||||
ion.DrmIon(StringIO(data[8:-8]), lambda name: self.voucher).parse(outfile)
|
||||
self.decrypted[filename] = outfile.getvalue()
|
||||
|
||||
if not self.decrypted:
|
||||
print(u'The .kfx-zip archive does not contain an encrypted DRMION file')
|
||||
|
||||
def decrypt_voucher(self, totalpids):
|
||||
with zipfile.ZipFile(self.infile, 'r') as zf:
|
||||
for info in zf.infolist():
|
||||
if info.file_size < 0x10000:
|
||||
data = zf.read(info.filename)
|
||||
if data.startswith('\xe0\x01\x00\xea') and 'ProtectedData' in data:
|
||||
break # found DRM voucher
|
||||
else:
|
||||
raise Exception(u'The .kfx-zip archive contains an encrypted DRMION file without a DRM voucher')
|
||||
|
||||
print u'Decrypting KFX DRM voucher: {0}'.format(info.filename)
|
||||
|
||||
for pid in [''] + totalpids:
|
||||
for dsn_len,secret_len in [(0,0), (16,0), (16,40), (32,40), (40,40)]:
|
||||
if len(pid) == dsn_len + secret_len:
|
||||
break # split pid into DSN and account secret
|
||||
else:
|
||||
continue
|
||||
|
||||
try:
|
||||
voucher = ion.DrmIonVoucher(StringIO(data), pid[:dsn_len], pid[dsn_len:])
|
||||
voucher.parse()
|
||||
voucher.decryptvoucher()
|
||||
break
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
raise Exception(u'Failed to decrypt KFX DRM voucher with any key')
|
||||
|
||||
print u'KFX DRM voucher successfully decrypted'
|
||||
|
||||
license_type = voucher.getlicensetype()
|
||||
if license_type != "Purchase":
|
||||
raise Exception((u'This book is licensed as {0}. '
|
||||
'These tools are intended for use on purchased books.').format(license_type))
|
||||
|
||||
self.voucher = voucher
|
||||
|
||||
def getBookTitle(self):
|
||||
return os.path.splitext(os.path.split(self.infile)[1])[0]
|
||||
|
||||
def getBookExtension(self):
|
||||
return '.kfx-zip'
|
||||
|
||||
def getBookType(self):
|
||||
return 'KFX-ZIP'
|
||||
|
||||
def cleanup(self):
|
||||
pass
|
||||
|
||||
def getFile(self, outpath):
|
||||
if not self.decrypted:
|
||||
shutil.copyfile(self.infile, outpath)
|
||||
else:
|
||||
with zipfile.ZipFile(self.infile, 'r') as zif:
|
||||
with zipfile.ZipFile(outpath, 'w') as zof:
|
||||
for info in zif.infolist():
|
||||
zof.writestr(info, self.decrypted.get(info.filename, zif.read(info.filename)))
|
||||
@@ -4,10 +4,15 @@
|
||||
from __future__ import with_statement
|
||||
|
||||
# kgenpids.py
|
||||
# Copyright © 2010-2015 by some_updates, Apprentice Alf and Apprentice Harper
|
||||
# Copyright © 2008-2017 Apprentice Harper et al.
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '2.1'
|
||||
|
||||
# Revision history:
|
||||
# 2.0 - Fix for non-ascii Windows user names
|
||||
# 2.1 - Actual fix for non-ascii WIndows user names.
|
||||
# x.x - Return information needed for KFX decryption
|
||||
|
||||
import sys
|
||||
import os, csv
|
||||
@@ -168,6 +173,9 @@ def pidFromSerial(s, l):
|
||||
|
||||
# Parse the EXTH header records and use the Kindle serial number to calculate the book pid.
|
||||
def getKindlePids(rec209, token, serialnum):
|
||||
if rec209 is None:
|
||||
return [serialnum]
|
||||
|
||||
pids=[]
|
||||
|
||||
if isinstance(serialnum,unicode):
|
||||
@@ -196,31 +204,59 @@ def getK4Pids(rec209, token, kindleDatabase):
|
||||
pids = []
|
||||
|
||||
try:
|
||||
# Get the Mazama Random number
|
||||
MazamaRandomNumber = (kindleDatabase[1])['MazamaRandomNumber'].decode('hex')
|
||||
|
||||
# Get the kindle account token
|
||||
# Get the kindle account token, if present
|
||||
kindleAccountToken = (kindleDatabase[1])['kindle.account.tokens'].decode('hex')
|
||||
|
||||
# Get the IDString used to decode the Kindle Info file
|
||||
IDString = (kindleDatabase[1])['IDString'].decode('hex')
|
||||
|
||||
# Get the UserName stored when the Kindle Info file was decoded
|
||||
UserName = (kindleDatabase[1])['UserName'].decode('hex')
|
||||
|
||||
except KeyError:
|
||||
print u"Keys not found in the database {0}.".format(kindleDatabase[0])
|
||||
kindleAccountToken=""
|
||||
pass
|
||||
|
||||
try:
|
||||
# Get the DSN token, if present
|
||||
DSN = (kindleDatabase[1])['DSN'].decode('hex')
|
||||
print u"Got DSN key from database {0}".format(kindleDatabase[0])
|
||||
except KeyError:
|
||||
# See if we have the info to generate the DSN
|
||||
try:
|
||||
# Get the Mazama Random number
|
||||
MazamaRandomNumber = (kindleDatabase[1])['MazamaRandomNumber'].decode('hex')
|
||||
#print u"Got MazamaRandomNumber from database {0}".format(kindleDatabase[0])
|
||||
|
||||
try:
|
||||
# Get the SerialNumber token, if present
|
||||
IDString = (kindleDatabase[1])['SerialNumber'].decode('hex')
|
||||
print u"Got SerialNumber from database {0}".format(kindleDatabase[0])
|
||||
except KeyError:
|
||||
# Get the IDString we added
|
||||
IDString = (kindleDatabase[1])['IDString'].decode('hex')
|
||||
|
||||
try:
|
||||
# Get the UsernameHash token, if present
|
||||
encodedUsername = (kindleDatabase[1])['UsernameHash'].decode('hex')
|
||||
print u"Got UsernameHash from database {0}".format(kindleDatabase[0])
|
||||
except KeyError:
|
||||
# Get the UserName we added
|
||||
UserName = (kindleDatabase[1])['UserName'].decode('hex')
|
||||
# encode it
|
||||
encodedUsername = encodeHash(UserName,charMap1)
|
||||
#print u"encodedUsername",encodedUsername.encode('hex')
|
||||
except KeyError:
|
||||
print u"Keys not found in the database {0}.".format(kindleDatabase[0])
|
||||
return pids
|
||||
|
||||
# Get the ID string used
|
||||
encodedIDString = encodeHash(IDString,charMap1)
|
||||
#print u"encodedIDString",encodedIDString.encode('hex')
|
||||
|
||||
# concat, hash and encode to calculate the DSN
|
||||
DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1)
|
||||
#print u"DSN",DSN.encode('hex')
|
||||
pass
|
||||
|
||||
if rec209 is None:
|
||||
pids.append(DSN+kindleAccountToken)
|
||||
return pids
|
||||
|
||||
# Get the ID string used
|
||||
encodedIDString = encodeHash(IDString,charMap1)
|
||||
|
||||
# Get the current user name
|
||||
encodedUsername = encodeHash(UserName,charMap1)
|
||||
|
||||
# concat, hash and encode to calculate the DSN
|
||||
DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1)
|
||||
|
||||
# Compute the device PID (for which I can tell, is used for nothing).
|
||||
table = generatePidEncryptionTable()
|
||||
devicePID = generateDevicePID(table,DSN,4)
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
from __future__ import with_statement
|
||||
|
||||
# kindlekey.py
|
||||
# Copyright © 2010-2015 by some_updates, Apprentice Alf and Apprentice Harper
|
||||
# Copyright © 2008-2017 Apprentice Harper et al.
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '2.5'
|
||||
|
||||
# Revision history:
|
||||
# 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc.
|
||||
@@ -19,15 +22,18 @@ from __future__ import with_statement
|
||||
# 1.8 - Fixes for Kindle for Mac, and non-ascii in Windows user names
|
||||
# 1.9 - Fixes for Unicode in Windows user names
|
||||
# 2.0 - Added comments and extra fix for non-ascii Windows user names
|
||||
# 2.1 - Fixed Kindle for PC encryption changes March 2016
|
||||
# 2.2 - Fixes for Macs with bonded ethernet ports
|
||||
# Also removed old .kinfo file support (pre-2011)
|
||||
# 2.3 - Added more field names thanks to concavegit's KFX code.
|
||||
# 2.4 - Fix for complex Mac disk setups, thanks to Tibs
|
||||
# 2.5 - Final Fix for Windows user names with non-ascii characters, thanks to oneofusoneofus
|
||||
|
||||
|
||||
"""
|
||||
Retrieve Kindle for PC/Mac user key.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '1.9'
|
||||
|
||||
import sys, os, re
|
||||
from struct import pack, unpack, unpack_from
|
||||
import json
|
||||
@@ -882,10 +888,18 @@ if iswindows:
|
||||
if errcd == 234:
|
||||
# bad wine implementation up through wine 1.3.21
|
||||
return "AlternateUserName"
|
||||
# double the buffer size
|
||||
buffer = create_unicode_buffer(len(buffer) * 2)
|
||||
size.value = len(buffer)
|
||||
# return low byte of the unicode value of each character of the username
|
||||
return buffer.value.encode('utf-16-le')[::2]
|
||||
|
||||
# replace any non-ASCII values with 0xfffd
|
||||
for i in xrange(0,len(buffer)):
|
||||
if buffer[i]>u"\u007f":
|
||||
#print u"swapping char "+str(i)+" ("+buffer[i]+")"
|
||||
buffer[i] = u"\ufffd"
|
||||
# return utf-8 encoding of modified username
|
||||
#print u"modified username:"+buffer.value
|
||||
return buffer.value.encode('utf-8')
|
||||
return GetUserName
|
||||
GetUserName = GetUserName()
|
||||
|
||||
@@ -926,7 +940,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 +1007,128 @@ 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',\
|
||||
'SerialNumber',\
|
||||
'UsernameHash',\
|
||||
'kindle.directedid.info',\
|
||||
'DSN',\
|
||||
'kindle.accounttype.info',\
|
||||
'krx.flashcardsplugin.data.encryption_key',\
|
||||
'krx.notebookexportplugin.data.encryption_key',\
|
||||
'proxy.http.password',\
|
||||
'proxy.http.username'
|
||||
]
|
||||
DB = {}
|
||||
with open(kInfoFile, 'rb') as infoReader:
|
||||
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:
|
||||
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 +1260,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 +1271,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 ')
|
||||
(devpart, mpath) = resline.split(' on ')[:2]
|
||||
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 +1297,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 +1314,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 +1344,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 +1360,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 +1384,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 +1452,122 @@ 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',\
|
||||
'SerialNumber',\
|
||||
'UsernameHash',\
|
||||
'kindle.directedid.info',\
|
||||
'DSN'
|
||||
]
|
||||
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 +1631,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 +1661,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):
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# mobidedrm.py, version 0.38
|
||||
# mobidedrm.py
|
||||
# Copyright © 2008 The Dark Reverser
|
||||
#
|
||||
# Modified 2008–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
# Portions © 2008–2017 Apprentice Harper et al.
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = u"0.42"
|
||||
|
||||
# This is a python script. You need a Python interpreter to run it.
|
||||
# For example, ActiveState Python, which exists for windows.
|
||||
@@ -69,9 +71,7 @@
|
||||
# 0.39 - Fixed problem with TEXtREAd and getBookType interface
|
||||
# 0.40 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 0.41 - Fixed potential unicode problem in command line calls
|
||||
|
||||
|
||||
__version__ = u"0.41"
|
||||
# 0.42 - Added GPL v3 licence. updated/removed some print statements
|
||||
|
||||
import sys
|
||||
import os
|
||||
@@ -244,7 +244,7 @@ class MobiBook:
|
||||
pass
|
||||
|
||||
def __init__(self, infile):
|
||||
print u"MobiDeDrm v{0:s}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__)
|
||||
print u"MobiDeDrm v{0:s}.\nCopyright © 2008-2017 The Dark Reverser, Apprentice Harper et al.".format(__version__)
|
||||
|
||||
try:
|
||||
from alfcrypto import Pukall_Cipher
|
||||
@@ -288,10 +288,10 @@ class MobiBook:
|
||||
self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18])
|
||||
self.mobi_codepage, = struct.unpack('>L',self.sect[0x1c:0x20])
|
||||
self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C])
|
||||
print u"MOBI header version {0:d}, header length {1:d}".format(self.mobi_version, self.mobi_length)
|
||||
#print u"MOBI header version {0:d}, header length {1:d}".format(self.mobi_version, self.mobi_length)
|
||||
if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
|
||||
self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4])
|
||||
print u"Extra Data Flags: {0:d}".format(self.extra_data_flags)
|
||||
#print u"Extra Data Flags: {0:d}".format(self.extra_data_flags)
|
||||
if (self.compression != 17480):
|
||||
# multibyte utf8 data is included in the encryption for PalmDoc compression
|
||||
# so clear that byte so that we leave it to be decrypted.
|
||||
@@ -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'
|
||||
@@ -514,7 +516,7 @@ def cli_main():
|
||||
argv=unicode_argv()
|
||||
progname = os.path.basename(argv[0])
|
||||
if len(argv)<3 or len(argv)>4:
|
||||
print u"MobiDeDrm v{0}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__)
|
||||
print u"MobiDeDrm v{0:s}.\nCopyright © 2008-2017 The Dark Reverser, Apprentice Harper et al.".format(__version__)
|
||||
print u"Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks"
|
||||
print u"Usage:"
|
||||
print u" {0} <infile> <outfile> [<Comma separated list of PIDs to try>]".format(progname)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -352,7 +352,7 @@ class TopazBook:
|
||||
break
|
||||
|
||||
if not bookKey:
|
||||
raise DrmException(u"No key found in {0:d} keys tried. Read the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(len(pidlst)))
|
||||
raise DrmException(u"No key found in {0:d} keys tried. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(len(pidlst)))
|
||||
|
||||
self.setBookKey(bookKey)
|
||||
self.createBookDirectory()
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# DeDRM.pyw
|
||||
# Copyright 2010-2015 some_updates, Apprentice Alf and Apprentice Harper
|
||||
# Copyright 2010-2016 some_updates, Apprentice Alf and Apprentice Harper
|
||||
|
||||
# Revision history:
|
||||
# 6.0.0 - Release along with unified plugin
|
||||
@@ -16,8 +16,24 @@
|
||||
# 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
|
||||
# 6.5.1 - Version bump to match plugin & Mac app
|
||||
# 6.5.2 - Fix for a new tag in Topaz ebooks
|
||||
# 6.5.3 - Explicitly warn about KFX files
|
||||
# 6.5.4 - PDF float fix.
|
||||
# 6.5.5 - Kindle for PC/Accented characters in username fix.
|
||||
# 6.6.0 - Initial KFX support from TomThumb
|
||||
|
||||
__version__ = '6.3.1'
|
||||
__version__ = '6.6.0'
|
||||
|
||||
import sys
|
||||
import os, os.path
|
||||
@@ -368,6 +384,9 @@ class PrefsDialog(Toplevel):
|
||||
('Kindle','.azw3'),
|
||||
('Kindle','.azw4'),
|
||||
('Kindle','.tpz'),
|
||||
('Kindle','.azw8'),
|
||||
('Kindle','.kfx'),
|
||||
('Kindle','.kfx-zip'),
|
||||
('Kindle','.mobi'),
|
||||
('Kindle','.prc'),
|
||||
('eReader','.pdb'),
|
||||
@@ -583,7 +602,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', '.pobi', '.tpz']:
|
||||
if ext in ['.azw', '.azw1', '.azw3', '.azw4', '.prc', '.mobi', '.pobi', '.tpz', '.azw8', '.kfx', '.kfx-zip']:
|
||||
self.p2 = Process(target=processK4MOBI,args=(q, infile, outdir, rscpath))
|
||||
self.p2.start()
|
||||
return 0
|
||||
@@ -55,7 +55,7 @@ p {margin-top: 0}
|
||||
<li>And probably many more.</li>
|
||||
</ul>
|
||||
|
||||
<h3> For additional help read the <a href="http://apprenticealf.wordpress.com/2011/01/17/frequently-asked-questions-about-the-drm-removal-tools/">FAQs</a> at <a href="http://apprenticealf.wordpress.com">Apprentice Alf’s Blog</a> and ask questions in the comments section of the <a href="http://apprenticealf.wordpress.com/2012/09/10/drm-removal-tools-for-ebooks/">first post</a>.</h3>
|
||||
<h3> For additional help read the <a href="https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md">FAQs</a> at <a href="https://github.com/apprenticeharper/DeDRM_tools/">Apprentice Harpers’s GitHub repository</a>. You can ask questions in the comments section of the <a href="http://apprenticealf.wordpress.com/2012/09/10/drm-removal-tools-for-ebooks/">first post</a> at <a href="http://wordpress.com/apprenticealf/">Apprentice Alf's blog</a> or <a href="https://github.com/apprenticeharper/DeDRM_tools/issues">raise an issue</a>. </h3>
|
||||
|
||||
<h2>Linux Systems Only</h2>
|
||||
<h3>Generating decryption keys for Adobe Digital Editions and Kindle for PC</h3>
|
||||
|
||||
@@ -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>
|
||||
@@ -1,52 +0,0 @@
|
||||
<!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 serial numbers</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 serial numbers</h1>
|
||||
|
||||
<p>Amazon's Kindle for Android application uses an internal serial number that's 72 character long. Extracting that serial number 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 the Kindle for Android serial number</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 the ‘backup.ab’ file you obtained by using the adb command. The backup file will be processed to extract any serial numbers in it, and the numbers will be added to the list.</p>
|
||||
|
||||
<h3>Adding the Kindle for Android serial number manually</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 a new Kindle for Android serial number.</p>
|
||||
<ul>
|
||||
<li><span class="bold">Kindle for Android Serial Number:</span> this is the unique serial number of your device. You may have obtained this through using the old android.py script.</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>
|
||||
|
||||
<h3>Deleting Kindle for Android serial numbers:</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 Kindle serial number from the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.</p>
|
||||
|
||||
<p>Once done creating/deleting serial numbers, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.</p>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -2,6 +2,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# __init__.py for DeDRM_plugin
|
||||
# Copyright © 2008-2018 Apprentice Harper et al.
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
@@ -9,8 +13,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.
|
||||
#
|
||||
@@ -43,6 +45,26 @@ __docformat__ = 'restructuredtext en'
|
||||
# 6.2.2 - Added URL method for B&N/nook books
|
||||
# 6.3.0 - Added in Kindle for Android serial number solution
|
||||
# 6.3.1 - Version number bump for clarity
|
||||
# 6.3.2 - Fixed Kindle for Android help file
|
||||
# 6.3.3 - Bug fix for Kindle for PC support
|
||||
# 6.3.4 - Fixes for Kindle for Android, Linux, and Kobo 3.17
|
||||
# 6.3.5 - Fixes for Linux, and Kobo 3.19 and more logging
|
||||
# 6.3.6 - Fixes for ADE ePub and PDF introduced in 6.3.5
|
||||
# 6.4.0 - Updated for new Kindle for PC encryption
|
||||
# 6.4.1 - Fix for some new tags in Topaz ebooks.
|
||||
# 6.4.2 - Fix for more new tags in Topaz ebooks and very small Topaz ebooks
|
||||
# 6.4.3 - Fix for error that only appears when not in debug mode
|
||||
# Also includes fix for Macs with bonded ethernet ports
|
||||
# 6.5.0 - Big update to Macintosh app
|
||||
# Fix for some more 'new' tags in Topaz ebooks.
|
||||
# Fix an error in wineutils.py
|
||||
# 6.5.1 - Updated version number, added PDF check for DRM-free documents
|
||||
# 6.5.2 - Another Topaz fix
|
||||
# 6.5.3 - Warn about KFX files explicitly
|
||||
# 6.5.4 - Mac App Fix, improve PDF decryption, handle latest tcl changes in ActivePython
|
||||
# 6.5.5 - Finally a fix for the Windows non-ASCII user names.
|
||||
# 6.6.0 - Add kfx and kfx-zip as supported file types (also invoke this plugin if the original
|
||||
# imported format was azw8 since that may be converted to kfx)
|
||||
|
||||
|
||||
"""
|
||||
@@ -50,7 +72,7 @@ Decrypt DRMed ebooks.
|
||||
"""
|
||||
|
||||
PLUGIN_NAME = u"DeDRM"
|
||||
PLUGIN_VERSION_TUPLE = (6, 3, 1)
|
||||
PLUGIN_VERSION_TUPLE = (6, 6, 0)
|
||||
PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE])
|
||||
# Include an html helpfile in the plugin's zipfile with the following name.
|
||||
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
|
||||
@@ -82,8 +104,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)
|
||||
|
||||
@@ -93,8 +119,8 @@ class DeDRM(FileTypePlugin):
|
||||
supported_platforms = ['linux', 'osx', 'windows']
|
||||
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','pobi','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','azw8','tpz','kfx','kfx-zip'])
|
||||
on_import = True
|
||||
priority = 600
|
||||
|
||||
@@ -146,7 +172,7 @@ class DeDRM(FileTypePlugin):
|
||||
try:
|
||||
open(file_path,'wb').write(data)
|
||||
except:
|
||||
print u"{0} v{1}: Exception when copying needed library files after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
|
||||
print u"{0} v{1}: Exception when copying needed library files".format(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
traceback.print_exc()
|
||||
pass
|
||||
|
||||
@@ -270,8 +296,8 @@ class DeDRM(FileTypePlugin):
|
||||
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 after {2:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
|
||||
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
|
||||
# import the Adobe Adept ePub handler
|
||||
import calibre_plugins.dedrm.ineptepub as ineptepub
|
||||
@@ -293,11 +319,15 @@ class DeDRM(FileTypePlugin):
|
||||
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)
|
||||
@@ -357,16 +387,19 @@ class DeDRM(FileTypePlugin):
|
||||
except:
|
||||
print u"{0} v{1}: Exception when saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
|
||||
traceback.print_exc()
|
||||
print u"{0} v{1}: Decrypted with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
|
||||
# Return the modified PersistentTemporary file to calibre.
|
||||
return of.name
|
||||
|
||||
print u"{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
|
||||
except Exception, e:
|
||||
print u"{0} v{1}: Unexpected Exception trying a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
|
||||
traceback.print_exc()
|
||||
pass
|
||||
|
||||
# Something went wrong with decryption.
|
||||
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\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 after {2:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
|
||||
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
|
||||
# Not a Barnes & Noble nor an Adobe Adept
|
||||
# Import the fixed epub.
|
||||
@@ -465,8 +498,8 @@ class DeDRM(FileTypePlugin):
|
||||
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 after {2:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
|
||||
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
|
||||
|
||||
def KindleMobiDecrypt(self,path_to_ebook):
|
||||
@@ -522,7 +555,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")
|
||||
@@ -533,8 +566,8 @@ class DeDRM(FileTypePlugin):
|
||||
pass
|
||||
if not decoded:
|
||||
#if you reached here then no luck raise and exception
|
||||
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\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 after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
|
||||
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
|
||||
of = self.temporary_file(book.getBookExtension())
|
||||
book.getFile(of.name)
|
||||
@@ -568,8 +601,8 @@ 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)
|
||||
|
||||
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 after {2:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
|
||||
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
|
||||
|
||||
def run(self, path_to_ebook):
|
||||
@@ -582,7 +615,7 @@ class DeDRM(FileTypePlugin):
|
||||
self.starttime = time.time()
|
||||
|
||||
booktype = os.path.splitext(path_to_ebook)[1].lower()[1:]
|
||||
if booktype in ['prc','mobi','pobi','azw','azw1','azw3','azw4','tpz']:
|
||||
if booktype in ['prc','mobi','pobi','azw','azw1','azw3','azw4','tpz','kfx-zip']:
|
||||
# Kindle/Mobipocket
|
||||
decrypted_ebook = self.KindleMobiDecrypt(path_to_ebook)
|
||||
elif booktype == 'pdb':
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# adobekey.pyw, version 5.7
|
||||
# adobekey.pyw, version 6.0
|
||||
# Copyright © 2009-2010 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 2010–2016 by several people
|
||||
|
||||
# Windows users: Before running this program, you must first install Python.
|
||||
# We recommend ActiveState Python 2.7.X for Windows (x86) from
|
||||
|
||||
@@ -16,16 +16,18 @@ from __future__ import with_statement
|
||||
# - 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.4'
|
||||
__version__ = '1.5'
|
||||
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
import getopt
|
||||
import tempfile
|
||||
import zlib
|
||||
@@ -220,20 +222,30 @@ def get_serials2(path=STORAGE2):
|
||||
userdata_keys = cursor.fetchall()
|
||||
dsns = []
|
||||
for userdata_row in userdata_keys:
|
||||
if userdata_row:
|
||||
userdata_utf8 = userdata_row[0].encode('utf8')
|
||||
if len(userdata_utf8) > 0:
|
||||
dsns.append(userdata_utf8)
|
||||
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:
|
||||
if userdata_row:
|
||||
userdata_utf8 = userdata_row[0].encode('utf8')
|
||||
if len(userdata_utf8) > 0:
|
||||
tokens.append(userdata_utf8)
|
||||
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 = []
|
||||
@@ -377,7 +389,6 @@ def gui_main():
|
||||
import Tkconstants
|
||||
import tkMessageBox
|
||||
import tkFileDialog
|
||||
import traceback
|
||||
except:
|
||||
print "Tkinter not installed"
|
||||
return cli_main()
|
||||
|
||||
@@ -124,7 +124,7 @@ class ConfigWidget(QWidget):
|
||||
d.exec_()
|
||||
|
||||
def kindle_android(self):
|
||||
d = ManageKeysDialog(self,u"Kindle for Android Keys File",self.tempdedrmprefs['androidkeys'], AddAndroidDialog, 'k4a')
|
||||
d = ManageKeysDialog(self,u"Kindle for Android Key",self.tempdedrmprefs['androidkeys'], AddAndroidDialog, 'k4a')
|
||||
d.exec_()
|
||||
|
||||
def kindle_keys(self):
|
||||
@@ -566,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)
|
||||
@@ -579,8 +592,7 @@ class AddBandNKeyDialog(QDialog):
|
||||
|
||||
@property
|
||||
def key_value(self):
|
||||
from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key
|
||||
return fetch_bandn_key(self.user_name,self.cc_number)
|
||||
return unicode(self.key_display.text()).strip()
|
||||
|
||||
@property
|
||||
def user_name(self):
|
||||
@@ -590,6 +602,14 @@ class AddBandNKeyDialog(QDialog):
|
||||
def cc_number(self):
|
||||
return unicode(self.cc_ledit.text()).strip()
|
||||
|
||||
def retrieve_key(self):
|
||||
from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key
|
||||
fetched_key = fetch_bandn_key(self.user_name,self.cc_number)
|
||||
if fetched_key == "":
|
||||
errmsg = u"Could not retrieve key. Check username, password and intenet connectivity and try again."
|
||||
error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
else:
|
||||
self.key_display.setText(fetched_key)
|
||||
|
||||
def accept(self):
|
||||
if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace():
|
||||
@@ -598,6 +618,10 @@ class AddBandNKeyDialog(QDialog):
|
||||
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):
|
||||
|
||||
@@ -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,19 @@ 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),
|
||||
'src' : (1, 'text', 0, 0),
|
||||
|
||||
}
|
||||
|
||||
@@ -506,8 +528,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)
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ignobleepub.pyw, version 3.8
|
||||
# ignobleepub.pyw, version 4.1
|
||||
# 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–2017 by Apprentice Harper
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# from <http://www.python.org/download/> and PyCrypto from
|
||||
@@ -35,13 +36,14 @@ from __future__ import with_statement
|
||||
# 3.8 - Fixed to retain zip file metadata (e.g. file modification date)
|
||||
# 3.9 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 4.0 - Work if TkInter is missing
|
||||
# 4.1 - Import tkFileDialog, don't assume something else will import it.
|
||||
|
||||
"""
|
||||
Decrypt Barnes & Noble encrypted ePub books.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "4.0"
|
||||
__version__ = "4.1"
|
||||
|
||||
import sys
|
||||
import os
|
||||
@@ -337,6 +339,7 @@ def gui_main():
|
||||
try:
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
import traceback
|
||||
except:
|
||||
|
||||
@@ -3,18 +3,19 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ineptepub.pyw, version 6.1
|
||||
# ineptepub.pyw, version 6.6
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2013 by some_updates, DiapDealer and Apprentice Alf
|
||||
# Modified 2015–2017 by Apprentice Harper
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# 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
|
||||
@@ -38,13 +39,17 @@ from __future__ import with_statement
|
||||
# 6.0 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 6.1 - Work if TkInter is missing
|
||||
# 6.2 - Handle UTF-8 file names inside an ePub, fix by Jose Luis
|
||||
# 6.3 - Add additional check on DER file sanity
|
||||
# 6.4 - Remove erroneous check on DER file sanity
|
||||
# 6.5 - Completely remove erroneous check on DER file sanity
|
||||
# 6.6 - Import tkFileDialog, don't assume something else will import it.
|
||||
|
||||
"""
|
||||
Decrypt Adobe Digital Editions encrypted ePub books.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "6.2"
|
||||
__version__ = "6.6"
|
||||
|
||||
import sys
|
||||
import os
|
||||
@@ -480,6 +485,7 @@ def gui_main():
|
||||
try:
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
import traceback
|
||||
except:
|
||||
|
||||
@@ -3,18 +3,19 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ineptpdf.pyw, version 7.11
|
||||
# ineptpdf.pyw, version 8.0.6
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
# Modified 2015-2017 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,19 @@ 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
|
||||
# 8.0.5 - Do not process DRM-free documents
|
||||
# 8.0.6 - Replace use of float by Decimal for greater precision, and import tkFileDialog
|
||||
|
||||
|
||||
"""
|
||||
Decrypts Adobe ADEPT-encrypted PDF files.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "8.0.1"
|
||||
__version__ = "8.0.6"
|
||||
|
||||
import sys
|
||||
import os
|
||||
@@ -67,6 +74,7 @@ import re
|
||||
import zlib
|
||||
import struct
|
||||
import hashlib
|
||||
from decimal import *
|
||||
from itertools import chain, islice
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
@@ -647,7 +655,7 @@ class PSBaseParser(object):
|
||||
return (self.parse_number, j+1)
|
||||
if c == '.':
|
||||
self.token = c
|
||||
return (self.parse_float, j+1)
|
||||
return (self.parse_decimal, j+1)
|
||||
if c.isalpha():
|
||||
self.token = c
|
||||
return (self.parse_keyword, j+1)
|
||||
@@ -712,20 +720,21 @@ class PSBaseParser(object):
|
||||
c = s[j]
|
||||
if c == '.':
|
||||
self.token += c
|
||||
return (self.parse_float, j+1)
|
||||
return (self.parse_decimal, j+1)
|
||||
try:
|
||||
self.add_token(int(self.token))
|
||||
except ValueError:
|
||||
pass
|
||||
return (self.parse_main, j)
|
||||
def parse_float(self, s, i):
|
||||
|
||||
def parse_decimal(self, s, i):
|
||||
m = END_NUMBER.search(s, i)
|
||||
if not m:
|
||||
self.token += s[i:]
|
||||
return (self.parse_float, len(s))
|
||||
return (self.parse_decimal, len(s))
|
||||
j = m.start(0)
|
||||
self.token += s[i:j]
|
||||
self.add_token(float(self.token))
|
||||
self.add_token(Decimal(self.token))
|
||||
return (self.parse_main, j)
|
||||
|
||||
def parse_keyword(self, s, i):
|
||||
@@ -927,7 +936,7 @@ class PSStackParser(PSBaseParser):
|
||||
(pos, token) = self.nexttoken()
|
||||
##print (pos,token), (self.curtype, self.curstack)
|
||||
if (isinstance(token, int) or
|
||||
isinstance(token, float) or
|
||||
isinstance(token, Decimal) or
|
||||
isinstance(token, bool) or
|
||||
isinstance(token, str) or
|
||||
isinstance(token, PSLiteral)):
|
||||
@@ -1056,17 +1065,17 @@ def int_value(x):
|
||||
return 0
|
||||
return x
|
||||
|
||||
def float_value(x):
|
||||
def decimal_value(x):
|
||||
x = resolve1(x)
|
||||
if not isinstance(x, float):
|
||||
if not isinstance(x, Decimal):
|
||||
if STRICT:
|
||||
raise PDFTypeError('Float required: %r' % x)
|
||||
raise PDFTypeError('Decimal required: %r' % x)
|
||||
return 0.0
|
||||
return x
|
||||
|
||||
def num_value(x):
|
||||
x = resolve1(x)
|
||||
if not (isinstance(x, int) or isinstance(x, float)):
|
||||
if not (isinstance(x, int) or isinstance(x, Decimal)):
|
||||
if STRICT:
|
||||
raise PDFTypeError('Int or Float required: %r' % x)
|
||||
return 0
|
||||
@@ -1463,6 +1472,7 @@ class PDFDocument(object):
|
||||
if not self.encryption:
|
||||
self.is_printable = self.is_modifiable = self.is_extractable = True
|
||||
self.ready = True
|
||||
raise PDFEncryptionError('Document is not encrypted.')
|
||||
return
|
||||
(docid, param) = self.encryption
|
||||
type = literal_name(param['Filter'])
|
||||
@@ -2135,7 +2145,11 @@ class PDFSerializer(object):
|
||||
if self.last.isalnum():
|
||||
self.write(' ')
|
||||
self.write(str(obj).lower())
|
||||
elif isinstance(obj, (int, long, float)):
|
||||
elif isinstance(obj, (int, long)):
|
||||
if self.last.isalnum():
|
||||
self.write(' ')
|
||||
self.write(str(obj))
|
||||
elif isinstance(obj, Decimal):
|
||||
if self.last.isalnum():
|
||||
self.write(' ')
|
||||
self.write(str(obj))
|
||||
@@ -2175,11 +2189,11 @@ def decryptBook(userkey, inpath, outpath):
|
||||
if RSA is None:
|
||||
raise ADEPTError(u"PyCrypto or OpenSSL must be installed.")
|
||||
with open(inpath, 'rb') as inf:
|
||||
try:
|
||||
serializer = PDFSerializer(inf, userkey)
|
||||
except:
|
||||
print u"Error serializing pdf {0}. Probably wrong key.".format(os.path.basename(inpath))
|
||||
return 2
|
||||
#try:
|
||||
serializer = PDFSerializer(inf, userkey)
|
||||
#except:
|
||||
# print u"Error serializing pdf {0}. Probably wrong key.".format(os.path.basename(inpath))
|
||||
# return 2
|
||||
# hope this will fix the 'bad file descriptor' problem
|
||||
with open(outpath, 'wb') as outf:
|
||||
# help construct to make sure the method runs to the end
|
||||
@@ -2211,6 +2225,7 @@ def gui_main():
|
||||
try:
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
import traceback
|
||||
except:
|
||||
|
||||
981
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ion.py
Normal file
981
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ion.py
Normal file
@@ -0,0 +1,981 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Pascal implementation by lulzkabulz. Python translation by apprenticenaomi. DeDRM integration by anon.
|
||||
# BinaryIon.pas + DrmIon.pas + IonSymbols.pas
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import collections
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
import os.path
|
||||
import struct
|
||||
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except ImportError:
|
||||
from StringIO import StringIO
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.py3compat import bchr, bord
|
||||
|
||||
try:
|
||||
# lzma library from calibre 2.35.0 or later
|
||||
import lzma.lzma1 as calibre_lzma
|
||||
except:
|
||||
calibre_lzma = None
|
||||
try:
|
||||
import lzma
|
||||
except:
|
||||
# Need pip backports.lzma on Python <3.3
|
||||
from backports import lzma
|
||||
|
||||
|
||||
TID_NULL = 0
|
||||
TID_BOOLEAN = 1
|
||||
TID_POSINT = 2
|
||||
TID_NEGINT = 3
|
||||
TID_FLOAT = 4
|
||||
TID_DECIMAL = 5
|
||||
TID_TIMESTAMP = 6
|
||||
TID_SYMBOL = 7
|
||||
TID_STRING = 8
|
||||
TID_CLOB = 9
|
||||
TID_BLOB = 0xA
|
||||
TID_LIST = 0xB
|
||||
TID_SEXP = 0xC
|
||||
TID_STRUCT = 0xD
|
||||
TID_TYPEDECL = 0xE
|
||||
TID_UNUSED = 0xF
|
||||
|
||||
|
||||
SID_UNKNOWN = -1
|
||||
SID_ION = 1
|
||||
SID_ION_1_0 = 2
|
||||
SID_ION_SYMBOL_TABLE = 3
|
||||
SID_NAME = 4
|
||||
SID_VERSION = 5
|
||||
SID_IMPORTS = 6
|
||||
SID_SYMBOLS = 7
|
||||
SID_MAX_ID = 8
|
||||
SID_ION_SHARED_SYMBOL_TABLE = 9
|
||||
SID_ION_1_0_MAX = 10
|
||||
|
||||
|
||||
LEN_IS_VAR_LEN = 0xE
|
||||
LEN_IS_NULL = 0xF
|
||||
|
||||
|
||||
VERSION_MARKER = b"\x01\x00\xEA"
|
||||
|
||||
|
||||
# asserts must always raise exceptions for proper functioning
|
||||
def _assert(test, msg="Exception"):
|
||||
if not test:
|
||||
raise Exception(msg)
|
||||
|
||||
|
||||
class SystemSymbols(object):
|
||||
ION = '$ion'
|
||||
ION_1_0 = '$ion_1_0'
|
||||
ION_SYMBOL_TABLE = '$ion_symbol_table'
|
||||
NAME = 'name'
|
||||
VERSION = 'version'
|
||||
IMPORTS = 'imports'
|
||||
SYMBOLS = 'symbols'
|
||||
MAX_ID = 'max_id'
|
||||
ION_SHARED_SYMBOL_TABLE = '$ion_shared_symbol_table'
|
||||
|
||||
|
||||
class IonCatalogItem(object):
|
||||
name = ""
|
||||
version = 0
|
||||
symnames = []
|
||||
|
||||
def __init__(self, name, version, symnames):
|
||||
self.name = name
|
||||
self.version = version
|
||||
self.symnames = symnames
|
||||
|
||||
|
||||
class SymbolToken(object):
|
||||
text = ""
|
||||
sid = 0
|
||||
|
||||
def __init__(self, text, sid):
|
||||
if text == "" and sid == 0:
|
||||
raise ValueError("Symbol token must have Text or SID")
|
||||
|
||||
self.text = text
|
||||
self.sid = sid
|
||||
|
||||
|
||||
class SymbolTable(object):
|
||||
table = None
|
||||
|
||||
def __init__(self):
|
||||
self.table = [None] * SID_ION_1_0_MAX
|
||||
self.table[SID_ION] = SystemSymbols.ION
|
||||
self.table[SID_ION_1_0] = SystemSymbols.ION_1_0
|
||||
self.table[SID_ION_SYMBOL_TABLE] = SystemSymbols.ION_SYMBOL_TABLE
|
||||
self.table[SID_NAME] = SystemSymbols.NAME
|
||||
self.table[SID_VERSION] = SystemSymbols.VERSION
|
||||
self.table[SID_IMPORTS] = SystemSymbols.IMPORTS
|
||||
self.table[SID_SYMBOLS] = SystemSymbols.SYMBOLS
|
||||
self.table[SID_MAX_ID] = SystemSymbols.MAX_ID
|
||||
self.table[SID_ION_SHARED_SYMBOL_TABLE] = SystemSymbols.ION_SHARED_SYMBOL_TABLE
|
||||
|
||||
def findbyid(self, sid):
|
||||
if sid < 1:
|
||||
raise ValueError("Invalid symbol id")
|
||||
|
||||
if sid < len(self.table):
|
||||
return self.table[sid]
|
||||
else:
|
||||
return ""
|
||||
|
||||
def import_(self, table, maxid):
|
||||
for i in range(maxid):
|
||||
self.table.append(table.symnames[i])
|
||||
|
||||
def importunknown(self, name, maxid):
|
||||
for i in range(maxid):
|
||||
self.table.append("%s#%d" % (name, i + 1))
|
||||
|
||||
|
||||
class ParserState:
|
||||
Invalid,BeforeField,BeforeTID,BeforeValue,AfterValue,EOF = 1,2,3,4,5,6
|
||||
|
||||
ContainerRec = collections.namedtuple("ContainerRec", "nextpos, tid, remaining")
|
||||
|
||||
|
||||
class BinaryIonParser(object):
|
||||
eof = False
|
||||
state = None
|
||||
localremaining = 0
|
||||
needhasnext = False
|
||||
isinstruct = False
|
||||
valuetid = 0
|
||||
valuefieldid = 0
|
||||
parenttid = 0
|
||||
valuelen = 0
|
||||
valueisnull = False
|
||||
valueistrue = False
|
||||
value = None
|
||||
didimports = False
|
||||
|
||||
def __init__(self, stream):
|
||||
self.annotations = []
|
||||
self.catalog = []
|
||||
|
||||
self.stream = stream
|
||||
self.initpos = stream.tell()
|
||||
self.reset()
|
||||
self.symbols = SymbolTable()
|
||||
|
||||
def reset(self):
|
||||
self.state = ParserState.BeforeTID
|
||||
self.needhasnext = True
|
||||
self.localremaining = -1
|
||||
self.eof = False
|
||||
self.isinstruct = False
|
||||
self.containerstack = []
|
||||
self.stream.seek(self.initpos)
|
||||
|
||||
def addtocatalog(self, name, version, symbols):
|
||||
self.catalog.append(IonCatalogItem(name, version, symbols))
|
||||
|
||||
def hasnext(self):
|
||||
while self.needhasnext and not self.eof:
|
||||
self.hasnextraw()
|
||||
if len(self.containerstack) == 0 and not self.valueisnull:
|
||||
if self.valuetid == TID_SYMBOL:
|
||||
if self.value == SID_ION_1_0:
|
||||
self.needhasnext = True
|
||||
elif self.valuetid == TID_STRUCT:
|
||||
for a in self.annotations:
|
||||
if a == SID_ION_SYMBOL_TABLE:
|
||||
self.parsesymboltable()
|
||||
self.needhasnext = True
|
||||
break
|
||||
return not self.eof
|
||||
|
||||
def hasnextraw(self):
|
||||
self.clearvalue()
|
||||
while self.valuetid == -1 and not self.eof:
|
||||
self.needhasnext = False
|
||||
if self.state == ParserState.BeforeField:
|
||||
_assert(self.valuefieldid == SID_UNKNOWN)
|
||||
|
||||
self.valuefieldid = self.readfieldid()
|
||||
if self.valuefieldid != SID_UNKNOWN:
|
||||
self.state = ParserState.BeforeTID
|
||||
else:
|
||||
self.eof = True
|
||||
|
||||
elif self.state == ParserState.BeforeTID:
|
||||
self.state = ParserState.BeforeValue
|
||||
self.valuetid = self.readtypeid()
|
||||
if self.valuetid == -1:
|
||||
self.state = ParserState.EOF
|
||||
self.eof = True
|
||||
break
|
||||
|
||||
if self.valuetid == TID_TYPEDECL:
|
||||
if self.valuelen == 0:
|
||||
self.checkversionmarker()
|
||||
else:
|
||||
self.loadannotations()
|
||||
|
||||
elif self.state == ParserState.BeforeValue:
|
||||
self.skip(self.valuelen)
|
||||
self.state = ParserState.AfterValue
|
||||
|
||||
elif self.state == ParserState.AfterValue:
|
||||
if self.isinstruct:
|
||||
self.state = ParserState.BeforeField
|
||||
else:
|
||||
self.state = ParserState.BeforeTID
|
||||
|
||||
else:
|
||||
_assert(self.state == ParserState.EOF)
|
||||
|
||||
def next(self):
|
||||
if self.hasnext():
|
||||
self.needhasnext = True
|
||||
return self.valuetid
|
||||
else:
|
||||
return -1
|
||||
|
||||
def push(self, typeid, nextposition, nextremaining):
|
||||
self.containerstack.append(ContainerRec(nextpos=nextposition, tid=typeid, remaining=nextremaining))
|
||||
|
||||
def stepin(self):
|
||||
_assert(self.valuetid in [TID_STRUCT, TID_LIST, TID_SEXP] and not self.eof,
|
||||
"valuetid=%s eof=%s" % (self.valuetid, self.eof))
|
||||
_assert((not self.valueisnull or self.state == ParserState.AfterValue) and
|
||||
(self.valueisnull or self.state == ParserState.BeforeValue))
|
||||
|
||||
nextrem = self.localremaining
|
||||
if nextrem != -1:
|
||||
nextrem -= self.valuelen
|
||||
if nextrem < 0:
|
||||
nextrem = 0
|
||||
self.push(self.parenttid, self.stream.tell() + self.valuelen, nextrem)
|
||||
|
||||
self.isinstruct = (self.valuetid == TID_STRUCT)
|
||||
if self.isinstruct:
|
||||
self.state = ParserState.BeforeField
|
||||
else:
|
||||
self.state = ParserState.BeforeTID
|
||||
|
||||
self.localremaining = self.valuelen
|
||||
self.parenttid = self.valuetid
|
||||
self.clearvalue()
|
||||
self.needhasnext = True
|
||||
|
||||
def stepout(self):
|
||||
rec = self.containerstack.pop()
|
||||
|
||||
self.eof = False
|
||||
self.parenttid = rec.tid
|
||||
if self.parenttid == TID_STRUCT:
|
||||
self.isinstruct = True
|
||||
self.state = ParserState.BeforeField
|
||||
else:
|
||||
self.isinstruct = False
|
||||
self.state = ParserState.BeforeTID
|
||||
self.needhasnext = True
|
||||
|
||||
self.clearvalue()
|
||||
curpos = self.stream.tell()
|
||||
if rec.nextpos > curpos:
|
||||
self.skip(rec.nextpos - curpos)
|
||||
else:
|
||||
_assert(rec.nextpos == curpos)
|
||||
|
||||
self.localremaining = rec.remaining
|
||||
|
||||
def read(self, count=1):
|
||||
if self.localremaining != -1:
|
||||
self.localremaining -= count
|
||||
_assert(self.localremaining >= 0)
|
||||
|
||||
result = self.stream.read(count)
|
||||
if len(result) == 0:
|
||||
raise EOFError()
|
||||
return result
|
||||
|
||||
def readfieldid(self):
|
||||
if self.localremaining != -1 and self.localremaining < 1:
|
||||
return -1
|
||||
|
||||
try:
|
||||
return self.readvaruint()
|
||||
except EOFError:
|
||||
return -1
|
||||
|
||||
def readtypeid(self):
|
||||
if self.localremaining != -1:
|
||||
if self.localremaining < 1:
|
||||
return -1
|
||||
self.localremaining -= 1
|
||||
|
||||
b = self.stream.read(1)
|
||||
if len(b) < 1:
|
||||
return -1
|
||||
b = bord(b)
|
||||
result = b >> 4
|
||||
ln = b & 0xF
|
||||
|
||||
if ln == LEN_IS_VAR_LEN:
|
||||
ln = self.readvaruint()
|
||||
elif ln == LEN_IS_NULL:
|
||||
ln = 0
|
||||
self.state = ParserState.AfterValue
|
||||
elif result == TID_NULL:
|
||||
# Must have LEN_IS_NULL
|
||||
_assert(False)
|
||||
elif result == TID_BOOLEAN:
|
||||
_assert(ln <= 1)
|
||||
self.valueistrue = (ln == 1)
|
||||
ln = 0
|
||||
self.state = ParserState.AfterValue
|
||||
elif result == TID_STRUCT:
|
||||
if ln == 1:
|
||||
ln = self.readvaruint()
|
||||
|
||||
self.valuelen = ln
|
||||
return result
|
||||
|
||||
def readvarint(self):
|
||||
b = bord(self.read())
|
||||
negative = ((b & 0x40) != 0)
|
||||
result = (b & 0x3F)
|
||||
|
||||
i = 0
|
||||
while (b & 0x80) == 0 and i < 4:
|
||||
b = bord(self.read())
|
||||
result = (result << 7) | (b & 0x7F)
|
||||
i += 1
|
||||
|
||||
_assert(i < 4 or (b & 0x80) != 0, "int overflow")
|
||||
|
||||
if negative:
|
||||
return -result
|
||||
return result
|
||||
|
||||
def readvaruint(self):
|
||||
b = bord(self.read())
|
||||
result = (b & 0x7F)
|
||||
|
||||
i = 0
|
||||
while (b & 0x80) == 0 and i < 4:
|
||||
b = bord(self.read())
|
||||
result = (result << 7) | (b & 0x7F)
|
||||
i += 1
|
||||
|
||||
_assert(i < 4 or (b & 0x80) != 0, "int overflow")
|
||||
|
||||
return result
|
||||
|
||||
def readdecimal(self):
|
||||
if self.valuelen == 0:
|
||||
return 0.
|
||||
|
||||
rem = self.localremaining - self.valuelen
|
||||
self.localremaining = self.valuelen
|
||||
exponent = self.readvarint()
|
||||
|
||||
_assert(self.localremaining > 0, "Only exponent in ReadDecimal")
|
||||
_assert(self.localremaining <= 8, "Decimal overflow")
|
||||
|
||||
signed = False
|
||||
b = [bord(x) for x in self.read(self.localremaining)]
|
||||
if (b[0] & 0x80) != 0:
|
||||
b[0] = b[0] & 0x7F
|
||||
signed = True
|
||||
|
||||
# Convert variably sized network order integer into 64-bit little endian
|
||||
j = 0
|
||||
vb = [0] * 8
|
||||
for i in range(len(b), -1, -1):
|
||||
vb[i] = b[j]
|
||||
j += 1
|
||||
|
||||
v = struct.unpack("<Q", b"".join(bchr(x) for x in vb))[0]
|
||||
|
||||
result = v * (10 ** exponent)
|
||||
if signed:
|
||||
result = -result
|
||||
|
||||
self.localremaining = rem
|
||||
return result
|
||||
|
||||
def skip(self, count):
|
||||
if self.localremaining != -1:
|
||||
self.localremaining -= count
|
||||
if self.localremaining < 0:
|
||||
raise EOFError()
|
||||
|
||||
self.stream.seek(count, os.SEEK_CUR)
|
||||
|
||||
def parsesymboltable(self):
|
||||
self.next() # shouldn't do anything?
|
||||
|
||||
_assert(self.valuetid == TID_STRUCT)
|
||||
|
||||
if self.didimports:
|
||||
return
|
||||
|
||||
self.stepin()
|
||||
|
||||
fieldtype = self.next()
|
||||
while fieldtype != -1:
|
||||
if not self.valueisnull:
|
||||
_assert(self.valuefieldid == SID_IMPORTS, "Unsupported symbol table field id")
|
||||
|
||||
if fieldtype == TID_LIST:
|
||||
self.gatherimports()
|
||||
|
||||
fieldtype = self.next()
|
||||
|
||||
self.stepout()
|
||||
self.didimports = True
|
||||
|
||||
def gatherimports(self):
|
||||
self.stepin()
|
||||
|
||||
t = self.next()
|
||||
while t != -1:
|
||||
if not self.valueisnull and t == TID_STRUCT:
|
||||
self.readimport()
|
||||
|
||||
t = self.next()
|
||||
|
||||
self.stepout()
|
||||
|
||||
def readimport(self):
|
||||
version = -1
|
||||
maxid = -1
|
||||
name = ""
|
||||
|
||||
self.stepin()
|
||||
|
||||
t = self.next()
|
||||
while t != -1:
|
||||
if not self.valueisnull and self.valuefieldid != SID_UNKNOWN:
|
||||
if self.valuefieldid == SID_NAME:
|
||||
name = self.stringvalue()
|
||||
elif self.valuefieldid == SID_VERSION:
|
||||
version = self.intvalue()
|
||||
elif self.valuefieldid == SID_MAX_ID:
|
||||
maxid = self.intvalue()
|
||||
|
||||
t = self.next()
|
||||
|
||||
self.stepout()
|
||||
|
||||
if name == "" or name == SystemSymbols.ION:
|
||||
return
|
||||
|
||||
if version < 1:
|
||||
version = 1
|
||||
|
||||
table = self.findcatalogitem(name)
|
||||
if maxid < 0:
|
||||
_assert(table is not None and version == table.version, "Import %s lacks maxid" % name)
|
||||
maxid = len(table.symnames)
|
||||
|
||||
if table is not None:
|
||||
self.symbols.import_(table, min(maxid, len(table.symnames)))
|
||||
else:
|
||||
self.symbols.importunknown(name, maxid)
|
||||
|
||||
def intvalue(self):
|
||||
_assert(self.valuetid in [TID_POSINT, TID_NEGINT], "Not an int")
|
||||
|
||||
self.preparevalue()
|
||||
return self.value
|
||||
|
||||
def stringvalue(self):
|
||||
_assert(self.valuetid == TID_STRING, "Not a string")
|
||||
|
||||
if self.valueisnull:
|
||||
return ""
|
||||
|
||||
self.preparevalue()
|
||||
return self.value
|
||||
|
||||
def symbolvalue(self):
|
||||
_assert(self.valuetid == TID_SYMBOL, "Not a symbol")
|
||||
|
||||
self.preparevalue()
|
||||
result = self.symbols.findbyid(self.value)
|
||||
if result == "":
|
||||
result = "SYMBOL#%d" % self.value
|
||||
return result
|
||||
|
||||
def lobvalue(self):
|
||||
_assert(self.valuetid in [TID_CLOB, TID_BLOB], "Not a LOB type: %s" % self.getfieldname())
|
||||
|
||||
if self.valueisnull:
|
||||
return None
|
||||
|
||||
result = self.read(self.valuelen)
|
||||
self.state = ParserState.AfterValue
|
||||
return result
|
||||
|
||||
def decimalvalue(self):
|
||||
_assert(self.valuetid == TID_DECIMAL, "Not a decimal")
|
||||
|
||||
self.preparevalue()
|
||||
return self.value
|
||||
|
||||
def preparevalue(self):
|
||||
if self.value is None:
|
||||
self.loadscalarvalue()
|
||||
|
||||
def loadscalarvalue(self):
|
||||
if self.valuetid not in [TID_NULL, TID_BOOLEAN, TID_POSINT, TID_NEGINT,
|
||||
TID_FLOAT, TID_DECIMAL, TID_TIMESTAMP,
|
||||
TID_SYMBOL, TID_STRING]:
|
||||
return
|
||||
|
||||
if self.valueisnull:
|
||||
self.value = None
|
||||
return
|
||||
|
||||
if self.valuetid == TID_STRING:
|
||||
self.value = self.read(self.valuelen).decode("UTF-8")
|
||||
|
||||
elif self.valuetid in (TID_POSINT, TID_NEGINT, TID_SYMBOL):
|
||||
if self.valuelen == 0:
|
||||
self.value = 0
|
||||
else:
|
||||
_assert(self.valuelen <= 4, "int too long: %d" % self.valuelen)
|
||||
v = 0
|
||||
for i in range(self.valuelen - 1, -1, -1):
|
||||
v = (v | (bord(self.read()) << (i * 8)))
|
||||
|
||||
if self.valuetid == TID_NEGINT:
|
||||
self.value = -v
|
||||
else:
|
||||
self.value = v
|
||||
|
||||
elif self.valuetid == TID_DECIMAL:
|
||||
self.value = self.readdecimal()
|
||||
|
||||
#else:
|
||||
# _assert(False, "Unhandled scalar type %d" % self.valuetid)
|
||||
|
||||
self.state = ParserState.AfterValue
|
||||
|
||||
def clearvalue(self):
|
||||
self.valuetid = -1
|
||||
self.value = None
|
||||
self.valueisnull = False
|
||||
self.valuefieldid = SID_UNKNOWN
|
||||
self.annotations = []
|
||||
|
||||
def loadannotations(self):
|
||||
ln = self.readvaruint()
|
||||
maxpos = self.stream.tell() + ln
|
||||
while self.stream.tell() < maxpos:
|
||||
self.annotations.append(self.readvaruint())
|
||||
self.valuetid = self.readtypeid()
|
||||
|
||||
def checkversionmarker(self):
|
||||
for i in VERSION_MARKER:
|
||||
_assert(self.read() == i, "Unknown version marker")
|
||||
|
||||
self.valuelen = 0
|
||||
self.valuetid = TID_SYMBOL
|
||||
self.value = SID_ION_1_0
|
||||
self.valueisnull = False
|
||||
self.valuefieldid = SID_UNKNOWN
|
||||
self.state = ParserState.AfterValue
|
||||
|
||||
def findcatalogitem(self, name):
|
||||
for result in self.catalog:
|
||||
if result.name == name:
|
||||
return result
|
||||
|
||||
def forceimport(self, symbols):
|
||||
item = IonCatalogItem("Forced", 1, symbols)
|
||||
self.symbols.import_(item, len(symbols))
|
||||
|
||||
def getfieldname(self):
|
||||
if self.valuefieldid == SID_UNKNOWN:
|
||||
return ""
|
||||
return self.symbols.findbyid(self.valuefieldid)
|
||||
|
||||
def getfieldnamesymbol(self):
|
||||
return SymbolToken(self.getfieldname(), self.valuefieldid)
|
||||
|
||||
def gettypename(self):
|
||||
if len(self.annotations) == 0:
|
||||
return ""
|
||||
|
||||
return self.symbols.findbyid(self.annotations[0])
|
||||
|
||||
@staticmethod
|
||||
def printlob(b):
|
||||
if b is None:
|
||||
return "null"
|
||||
|
||||
result = ""
|
||||
for i in b:
|
||||
result += ("%02x " % bord(i))
|
||||
|
||||
if len(result) > 0:
|
||||
result = result[:-1]
|
||||
return result
|
||||
|
||||
def ionwalk(self, supert, indent, lst):
|
||||
while self.hasnext():
|
||||
if supert == TID_STRUCT:
|
||||
L = self.getfieldname() + ":"
|
||||
else:
|
||||
L = ""
|
||||
|
||||
t = self.next()
|
||||
if t in [TID_STRUCT, TID_LIST]:
|
||||
if L != "":
|
||||
lst.append(indent + L)
|
||||
L = self.gettypename()
|
||||
if L != "":
|
||||
lst.append(indent + L + "::")
|
||||
if t == TID_STRUCT:
|
||||
lst.append(indent + "{")
|
||||
else:
|
||||
lst.append(indent + "[")
|
||||
|
||||
self.stepin()
|
||||
self.ionwalk(t, indent + " ", lst)
|
||||
self.stepout()
|
||||
|
||||
if t == TID_STRUCT:
|
||||
lst.append(indent + "}")
|
||||
else:
|
||||
lst.append(indent + "]")
|
||||
|
||||
else:
|
||||
if t == TID_STRING:
|
||||
L += ('"%s"' % self.stringvalue())
|
||||
elif t in [TID_CLOB, TID_BLOB]:
|
||||
L += ("{%s}" % self.printlob(self.lobvalue()))
|
||||
elif t == TID_POSINT:
|
||||
L += str(self.intvalue())
|
||||
elif t == TID_SYMBOL:
|
||||
tn = self.gettypename()
|
||||
if tn != "":
|
||||
tn += "::"
|
||||
L += tn + self.symbolvalue()
|
||||
elif t == TID_DECIMAL:
|
||||
L += str(self.decimalvalue())
|
||||
else:
|
||||
L += ("TID %d" % t)
|
||||
lst.append(indent + L)
|
||||
|
||||
def print_(self, lst):
|
||||
self.reset()
|
||||
self.ionwalk(-1, "", lst)
|
||||
|
||||
|
||||
SYM_NAMES = [ 'com.amazon.drm.Envelope@1.0',
|
||||
'com.amazon.drm.EnvelopeMetadata@1.0', 'size', 'page_size',
|
||||
'encryption_key', 'encryption_transformation',
|
||||
'encryption_voucher', 'signing_key', 'signing_algorithm',
|
||||
'signing_voucher', 'com.amazon.drm.EncryptedPage@1.0',
|
||||
'cipher_text', 'cipher_iv', 'com.amazon.drm.Signature@1.0',
|
||||
'data', 'com.amazon.drm.EnvelopeIndexTable@1.0', 'length',
|
||||
'offset', 'algorithm', 'encoded', 'encryption_algorithm',
|
||||
'hashing_algorithm', 'expires', 'format', 'id',
|
||||
'lock_parameters', 'strategy', 'com.amazon.drm.Key@1.0',
|
||||
'com.amazon.drm.KeySet@1.0', 'com.amazon.drm.PIDv3@1.0',
|
||||
'com.amazon.drm.PlainTextPage@1.0',
|
||||
'com.amazon.drm.PlainText@1.0', 'com.amazon.drm.PrivateKey@1.0',
|
||||
'com.amazon.drm.PublicKey@1.0', 'com.amazon.drm.SecretKey@1.0',
|
||||
'com.amazon.drm.Voucher@1.0', 'public_key', 'private_key',
|
||||
'com.amazon.drm.KeyPair@1.0', 'com.amazon.drm.ProtectedData@1.0',
|
||||
'doctype', 'com.amazon.drm.EnvelopeIndexTableOffset@1.0',
|
||||
'enddoc', 'license_type', 'license', 'watermark', 'key', 'value',
|
||||
'com.amazon.drm.License@1.0', 'category', 'metadata',
|
||||
'categorized_metadata', 'com.amazon.drm.CategorizedMetadata@1.0',
|
||||
'com.amazon.drm.VoucherEnvelope@1.0', 'mac', 'voucher',
|
||||
'com.amazon.drm.ProtectedData@2.0',
|
||||
'com.amazon.drm.Envelope@2.0',
|
||||
'com.amazon.drm.EnvelopeMetadata@2.0',
|
||||
'com.amazon.drm.EncryptedPage@2.0',
|
||||
'com.amazon.drm.PlainText@2.0', 'compression_algorithm',
|
||||
'com.amazon.drm.Compressed@1.0', 'priority', 'refines']
|
||||
|
||||
def addprottable(ion):
|
||||
ion.addtocatalog("ProtectedData", 1, SYM_NAMES)
|
||||
|
||||
|
||||
def pkcs7pad(msg, blocklen):
|
||||
paddinglen = blocklen - len(msg) % blocklen
|
||||
padding = bchr(paddinglen) * paddinglen
|
||||
return msg + padding
|
||||
|
||||
|
||||
def pkcs7unpad(msg, blocklen):
|
||||
_assert(len(msg) % blocklen == 0)
|
||||
|
||||
paddinglen = bord(msg[-1])
|
||||
_assert(paddinglen > 0 and paddinglen <= blocklen, "Incorrect padding - Wrong key")
|
||||
_assert(msg[-paddinglen:] == bchr(paddinglen) * paddinglen, "Incorrect padding - Wrong key")
|
||||
|
||||
return msg[:-paddinglen]
|
||||
|
||||
|
||||
class DrmIonVoucher(object):
|
||||
envelope = None
|
||||
voucher = None
|
||||
drmkey = None
|
||||
license_type = "Unknown"
|
||||
|
||||
encalgorithm = ""
|
||||
enctransformation = ""
|
||||
hashalgorithm = ""
|
||||
|
||||
lockparams = None
|
||||
|
||||
ciphertext = b""
|
||||
cipheriv = b""
|
||||
secretkey = b""
|
||||
|
||||
def __init__(self, voucherenv, dsn, secret):
|
||||
self.dsn,self.secret = dsn,secret
|
||||
|
||||
self.lockparams = []
|
||||
|
||||
self.envelope = BinaryIonParser(voucherenv)
|
||||
addprottable(self.envelope)
|
||||
|
||||
def decryptvoucher(self):
|
||||
shared = "PIDv3" + self.encalgorithm + self.enctransformation + self.hashalgorithm
|
||||
|
||||
self.lockparams.sort()
|
||||
for param in self.lockparams:
|
||||
if param == "ACCOUNT_SECRET":
|
||||
shared += param + self.secret
|
||||
elif param == "CLIENT_ID":
|
||||
shared += param + self.dsn
|
||||
else:
|
||||
_assert(False, "Unknown lock parameter: %s" % param)
|
||||
|
||||
sharedsecret = shared.encode("UTF-8")
|
||||
|
||||
key = hmac.new(sharedsecret, sharedsecret[:5], digestmod=hashlib.sha256).digest()
|
||||
aes = AES.new(key[:32], AES.MODE_CBC, self.cipheriv[:16])
|
||||
b = aes.decrypt(self.ciphertext)
|
||||
b = pkcs7unpad(b, 16)
|
||||
|
||||
self.drmkey = BinaryIonParser(StringIO(b))
|
||||
addprottable(self.drmkey)
|
||||
|
||||
_assert(self.drmkey.hasnext() and self.drmkey.next() == TID_LIST and self.drmkey.gettypename() == "com.amazon.drm.KeySet@1.0",
|
||||
"Expected KeySet, got %s" % self.drmkey.gettypename())
|
||||
|
||||
self.drmkey.stepin()
|
||||
while self.drmkey.hasnext():
|
||||
self.drmkey.next()
|
||||
if self.drmkey.gettypename() != "com.amazon.drm.SecretKey@1.0":
|
||||
continue
|
||||
|
||||
self.drmkey.stepin()
|
||||
while self.drmkey.hasnext():
|
||||
self.drmkey.next()
|
||||
if self.drmkey.getfieldname() == "algorithm":
|
||||
_assert(self.drmkey.stringvalue() == "AES", "Unknown cipher algorithm: %s" % self.drmkey.stringvalue())
|
||||
elif self.drmkey.getfieldname() == "format":
|
||||
_assert(self.drmkey.stringvalue() == "RAW", "Unknown key format: %s" % self.drmkey.stringvalue())
|
||||
elif self.drmkey.getfieldname() == "encoded":
|
||||
self.secretkey = self.drmkey.lobvalue()
|
||||
|
||||
self.drmkey.stepout()
|
||||
break
|
||||
|
||||
self.drmkey.stepout()
|
||||
|
||||
def parse(self):
|
||||
self.envelope.reset()
|
||||
_assert(self.envelope.hasnext(), "Envelope is empty")
|
||||
_assert(self.envelope.next() == TID_STRUCT and self.envelope.gettypename() == "com.amazon.drm.VoucherEnvelope@1.0",
|
||||
"Unknown type encountered in envelope, expected VoucherEnvelope")
|
||||
|
||||
self.envelope.stepin()
|
||||
while self.envelope.hasnext():
|
||||
self.envelope.next()
|
||||
field = self.envelope.getfieldname()
|
||||
if field == "voucher":
|
||||
self.voucher = BinaryIonParser(StringIO(self.envelope.lobvalue()))
|
||||
addprottable(self.voucher)
|
||||
continue
|
||||
elif field != "strategy":
|
||||
continue
|
||||
|
||||
_assert(self.envelope.gettypename() == "com.amazon.drm.PIDv3@1.0", "Unknown strategy: %s" % self.envelope.gettypename())
|
||||
|
||||
self.envelope.stepin()
|
||||
while self.envelope.hasnext():
|
||||
self.envelope.next()
|
||||
field = self.envelope.getfieldname()
|
||||
if field == "encryption_algorithm":
|
||||
self.encalgorithm = self.envelope.stringvalue()
|
||||
elif field == "encryption_transformation":
|
||||
self.enctransformation = self.envelope.stringvalue()
|
||||
elif field == "hashing_algorithm":
|
||||
self.hashalgorithm = self.envelope.stringvalue()
|
||||
elif field == "lock_parameters":
|
||||
self.envelope.stepin()
|
||||
while self.envelope.hasnext():
|
||||
_assert(self.envelope.next() == TID_STRING, "Expected string list for lock_parameters")
|
||||
self.lockparams.append(self.envelope.stringvalue())
|
||||
self.envelope.stepout()
|
||||
|
||||
self.envelope.stepout()
|
||||
|
||||
self.parsevoucher()
|
||||
|
||||
def parsevoucher(self):
|
||||
_assert(self.voucher.hasnext(), "Voucher is empty")
|
||||
_assert(self.voucher.next() == TID_STRUCT and self.voucher.gettypename() == "com.amazon.drm.Voucher@1.0",
|
||||
"Unknown type, expected Voucher")
|
||||
|
||||
self.voucher.stepin()
|
||||
while self.voucher.hasnext():
|
||||
self.voucher.next()
|
||||
|
||||
if self.voucher.getfieldname() == "cipher_iv":
|
||||
self.cipheriv = self.voucher.lobvalue()
|
||||
elif self.voucher.getfieldname() == "cipher_text":
|
||||
self.ciphertext = self.voucher.lobvalue()
|
||||
elif self.voucher.getfieldname() == "license":
|
||||
_assert(self.voucher.gettypename() == "com.amazon.drm.License@1.0",
|
||||
"Unknown license: %s" % self.voucher.gettypename())
|
||||
self.voucher.stepin()
|
||||
while self.voucher.hasnext():
|
||||
self.voucher.next()
|
||||
if self.voucher.getfieldname() == "license_type":
|
||||
self.license_type = self.voucher.stringvalue()
|
||||
self.voucher.stepout()
|
||||
|
||||
def printenvelope(self, lst):
|
||||
self.envelope.print_(lst)
|
||||
|
||||
def printkey(self, lst):
|
||||
if self.voucher is None:
|
||||
self.parse()
|
||||
if self.drmkey is None:
|
||||
self.decryptvoucher()
|
||||
|
||||
self.drmkey.print_(lst)
|
||||
|
||||
def printvoucher(self, lst):
|
||||
if self.voucher is None:
|
||||
self.parse()
|
||||
|
||||
self.voucher.print_(lst)
|
||||
|
||||
def getlicensetype(self):
|
||||
return self.license_type
|
||||
|
||||
|
||||
class DrmIon(object):
|
||||
ion = None
|
||||
voucher = None
|
||||
vouchername = ""
|
||||
key = b""
|
||||
onvoucherrequired = None
|
||||
|
||||
def __init__(self, ionstream, onvoucherrequired):
|
||||
self.ion = BinaryIonParser(ionstream)
|
||||
addprottable(self.ion)
|
||||
self.onvoucherrequired = onvoucherrequired
|
||||
|
||||
def parse(self, outpages):
|
||||
self.ion.reset()
|
||||
|
||||
_assert(self.ion.hasnext(), "DRMION envelope is empty")
|
||||
_assert(self.ion.next() == TID_SYMBOL and self.ion.gettypename() == "doctype", "Expected doctype symbol")
|
||||
_assert(self.ion.next() == TID_LIST and self.ion.gettypename() in ["com.amazon.drm.Envelope@1.0", "com.amazon.drm.Envelope@2.0"],
|
||||
"Unknown type encountered in DRMION envelope, expected Envelope, got %s" % self.ion.gettypename())
|
||||
|
||||
while True:
|
||||
if self.ion.gettypename() == "enddoc":
|
||||
break
|
||||
|
||||
self.ion.stepin()
|
||||
while self.ion.hasnext():
|
||||
self.ion.next()
|
||||
|
||||
if self.ion.gettypename() in ["com.amazon.drm.EnvelopeMetadata@1.0", "com.amazon.drm.EnvelopeMetadata@2.0"]:
|
||||
self.ion.stepin()
|
||||
while self.ion.hasnext():
|
||||
self.ion.next()
|
||||
if self.ion.getfieldname() != "encryption_voucher":
|
||||
continue
|
||||
|
||||
if self.vouchername == "":
|
||||
self.vouchername = self.ion.stringvalue()
|
||||
self.voucher = self.onvoucherrequired(self.vouchername)
|
||||
self.key = self.voucher.secretkey
|
||||
_assert(self.key is not None, "Unable to obtain secret key from voucher")
|
||||
else:
|
||||
_assert(self.vouchername == self.ion.stringvalue(),
|
||||
"Unexpected: Different vouchers required for same file?")
|
||||
|
||||
self.ion.stepout()
|
||||
|
||||
elif self.ion.gettypename() in ["com.amazon.drm.EncryptedPage@1.0", "com.amazon.drm.EncryptedPage@2.0"]:
|
||||
decompress = False
|
||||
ct = None
|
||||
civ = None
|
||||
self.ion.stepin()
|
||||
while self.ion.hasnext():
|
||||
self.ion.next()
|
||||
if self.ion.gettypename() == "com.amazon.drm.Compressed@1.0":
|
||||
decompress = True
|
||||
if self.ion.getfieldname() == "cipher_text":
|
||||
ct = self.ion.lobvalue()
|
||||
elif self.ion.getfieldname() == "cipher_iv":
|
||||
civ = self.ion.lobvalue()
|
||||
|
||||
if ct is not None and civ is not None:
|
||||
self.processpage(ct, civ, outpages, decompress)
|
||||
self.ion.stepout()
|
||||
|
||||
self.ion.stepout()
|
||||
if not self.ion.hasnext():
|
||||
break
|
||||
self.ion.next()
|
||||
|
||||
def print_(self, lst):
|
||||
self.ion.print_(lst)
|
||||
|
||||
def processpage(self, ct, civ, outpages, decompress):
|
||||
aes = AES.new(self.key[:16], AES.MODE_CBC, civ[:16])
|
||||
msg = pkcs7unpad(aes.decrypt(ct), 16)
|
||||
|
||||
if not decompress:
|
||||
outpages.write(msg)
|
||||
return
|
||||
|
||||
_assert(msg[0] == b"\x00", "LZMA UseFilter not supported")
|
||||
|
||||
if calibre_lzma is not None:
|
||||
with calibre_lzma.decompress(msg[1:], bufsize=0x1000000) as f:
|
||||
f.seek(0)
|
||||
outpages.write(f.read())
|
||||
return
|
||||
|
||||
decomp = lzma.LZMADecompressor(format=lzma.FORMAT_ALONE)
|
||||
while not decomp.eof:
|
||||
segment = decomp.decompress(msg[1:])
|
||||
msg = b"" # Contents were internally buffered after the first call
|
||||
outpages.write(segment)
|
||||
@@ -3,10 +3,13 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# k4mobidedrm.py, version 5.3
|
||||
# Copyright © 2009-2015 by ApprenticeHarper et al.
|
||||
# k4mobidedrm.py
|
||||
# Copyright © 2008-2017 by Apprentice Harper et al.
|
||||
|
||||
# engine to remove drm from Kindle and Mobipocket ebooks
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '5.5'
|
||||
|
||||
# Engine to remove drm from Kindle and Mobipocket ebooks
|
||||
# for personal use for archiving and converting your ebooks
|
||||
|
||||
# PLEASE DO NOT PIRATE EBOOKS!
|
||||
@@ -17,12 +20,11 @@ from __future__ import with_statement
|
||||
# readable for a long, long time
|
||||
|
||||
# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
|
||||
# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates
|
||||
# and many many others
|
||||
# unswindle, DarkReverser, ApprenticeAlf, and many many others
|
||||
|
||||
# Special thanks to The Dark Reverser for MobiDeDrm and CMBDTC for cmbdtc_dump
|
||||
# from which this script borrows most unashamedly.
|
||||
|
||||
|
||||
# Changelog
|
||||
# 1.0 - Name change to k4mobidedrm. Adds Mac support, Adds plugin code
|
||||
# 1.1 - Adds support for additional kindle.info files
|
||||
@@ -56,9 +58,9 @@ from __future__ import with_statement
|
||||
# 5.1 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 5.2 - Fixed error in command line processing of unicode arguments
|
||||
# 5.3 - Changed Android support to allow passing of backup .ab files
|
||||
|
||||
__version__ = '5.3'
|
||||
|
||||
# 5.4 - Recognise KFX files masquerading as azw, even if we can't decrypt them yet.
|
||||
# 5.5 - Added GPL v3 licence explicitly.
|
||||
# 5.x - Invoke KFXZipBook to handle zipped KFX files
|
||||
|
||||
import sys, os, re
|
||||
import csv
|
||||
@@ -82,11 +84,13 @@ if inCalibre:
|
||||
from calibre_plugins.dedrm import topazextract
|
||||
from calibre_plugins.dedrm import kgenpids
|
||||
from calibre_plugins.dedrm import androidkindlekey
|
||||
from calibre_plugins.dedrm import kfxdedrm
|
||||
else:
|
||||
import mobidedrm
|
||||
import topazextract
|
||||
import kgenpids
|
||||
import androidkindlekey
|
||||
import kfxdedrm
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
@@ -194,11 +198,17 @@ def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime
|
||||
raise DrmException(u"Input file does not exist.")
|
||||
|
||||
mobi = True
|
||||
magic3 = open(infile,'rb').read(3)
|
||||
magic8 = open(infile,'rb').read(8)
|
||||
if magic8 == '\xeaDRMION\xee':
|
||||
raise DrmException(u"The .kfx DRMION file cannot be decrypted by itself. A .kfx-zip archive containing a DRM voucher is required.")
|
||||
|
||||
magic3 = magic8[:3]
|
||||
if magic3 == 'TPZ':
|
||||
mobi = False
|
||||
|
||||
if mobi:
|
||||
if magic8[:4] == 'PK\x03\x04':
|
||||
mb = kfxdedrm.KFXZipBook(infile)
|
||||
elif mobi:
|
||||
mb = mobidedrm.MobiBook(infile)
|
||||
else:
|
||||
mb = topazextract.TopazBook(infile)
|
||||
@@ -215,8 +225,9 @@ def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime
|
||||
md1, md2 = mb.getPIDMetaInfo()
|
||||
totalpids.extend(kgenpids.getPidList(md1, md2, serials, kDatabases))
|
||||
# remove any duplicates
|
||||
totalpid = list(set(totalpids))
|
||||
totalpids = list(set(totalpids))
|
||||
print u"Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(totalpids))
|
||||
#print totalpids
|
||||
|
||||
try:
|
||||
mb.processBook(totalpids)
|
||||
@@ -289,7 +300,7 @@ def usage(progname):
|
||||
def cli_main():
|
||||
argv=unicode_argv()
|
||||
progname = os.path.basename(argv[0])
|
||||
print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2013 The Dark Reverser et al.".format(__version__)
|
||||
print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2017 Apprentice Harper et al.".format(__version__)
|
||||
|
||||
try:
|
||||
opts, args = getopt.getopt(argv[1:], "k:p:s:a:")
|
||||
|
||||
108
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kfxdedrm.py
Normal file
108
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kfxdedrm.py
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# Engine to remove drm from Kindle KFX ebooks
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import zipfile
|
||||
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except ImportError:
|
||||
from StringIO import StringIO
|
||||
|
||||
try:
|
||||
import ion
|
||||
except:
|
||||
from calibre_plugins.dedrm import ion
|
||||
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '1.0'
|
||||
|
||||
|
||||
class KFXZipBook:
|
||||
def __init__(self, infile):
|
||||
self.infile = infile
|
||||
self.voucher = None
|
||||
self.decrypted = {}
|
||||
|
||||
def getPIDMetaInfo(self):
|
||||
return (None, None)
|
||||
|
||||
def processBook(self, totalpids):
|
||||
with zipfile.ZipFile(self.infile, 'r') as zf:
|
||||
for filename in zf.namelist():
|
||||
data = zf.read(filename)
|
||||
if data.startswith('\xeaDRMION\xee'):
|
||||
if self.voucher is None:
|
||||
self.decrypt_voucher(totalpids)
|
||||
print u'Decrypting KFX DRMION: {0}'.format(filename)
|
||||
outfile = StringIO()
|
||||
ion.DrmIon(StringIO(data[8:-8]), lambda name: self.voucher).parse(outfile)
|
||||
self.decrypted[filename] = outfile.getvalue()
|
||||
|
||||
if not self.decrypted:
|
||||
print(u'The .kfx-zip archive does not contain an encrypted DRMION file')
|
||||
|
||||
def decrypt_voucher(self, totalpids):
|
||||
with zipfile.ZipFile(self.infile, 'r') as zf:
|
||||
for info in zf.infolist():
|
||||
if info.file_size < 0x10000:
|
||||
data = zf.read(info.filename)
|
||||
if data.startswith('\xe0\x01\x00\xea') and 'ProtectedData' in data:
|
||||
break # found DRM voucher
|
||||
else:
|
||||
raise Exception(u'The .kfx-zip archive contains an encrypted DRMION file without a DRM voucher')
|
||||
|
||||
print u'Decrypting KFX DRM voucher: {0}'.format(info.filename)
|
||||
|
||||
for pid in [''] + totalpids:
|
||||
for dsn_len,secret_len in [(0,0), (16,0), (16,40), (32,40), (40,40)]:
|
||||
if len(pid) == dsn_len + secret_len:
|
||||
break # split pid into DSN and account secret
|
||||
else:
|
||||
continue
|
||||
|
||||
try:
|
||||
voucher = ion.DrmIonVoucher(StringIO(data), pid[:dsn_len], pid[dsn_len:])
|
||||
voucher.parse()
|
||||
voucher.decryptvoucher()
|
||||
break
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
raise Exception(u'Failed to decrypt KFX DRM voucher with any key')
|
||||
|
||||
print u'KFX DRM voucher successfully decrypted'
|
||||
|
||||
license_type = voucher.getlicensetype()
|
||||
if license_type != "Purchase":
|
||||
raise Exception((u'This book is licensed as {0}. '
|
||||
'These tools are intended for use on purchased books.').format(license_type))
|
||||
|
||||
self.voucher = voucher
|
||||
|
||||
def getBookTitle(self):
|
||||
return os.path.splitext(os.path.split(self.infile)[1])[0]
|
||||
|
||||
def getBookExtension(self):
|
||||
return '.kfx-zip'
|
||||
|
||||
def getBookType(self):
|
||||
return 'KFX-ZIP'
|
||||
|
||||
def cleanup(self):
|
||||
pass
|
||||
|
||||
def getFile(self, outpath):
|
||||
if not self.decrypted:
|
||||
shutil.copyfile(self.infile, outpath)
|
||||
else:
|
||||
with zipfile.ZipFile(self.infile, 'r') as zif:
|
||||
with zipfile.ZipFile(outpath, 'w') as zof:
|
||||
for info in zif.infolist():
|
||||
zof.writestr(info, self.decrypted.get(info.filename, zif.read(info.filename)))
|
||||
@@ -4,10 +4,15 @@
|
||||
from __future__ import with_statement
|
||||
|
||||
# kgenpids.py
|
||||
# Copyright © 2010-2015 by some_updates, Apprentice Alf and Apprentice Harper
|
||||
# Copyright © 2008-2017 Apprentice Harper et al.
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '2.1'
|
||||
|
||||
# Revision history:
|
||||
# 2.0 - Fix for non-ascii Windows user names
|
||||
# 2.1 - Actual fix for non-ascii WIndows user names.
|
||||
# x.x - Return information needed for KFX decryption
|
||||
|
||||
import sys
|
||||
import os, csv
|
||||
@@ -168,6 +173,9 @@ def pidFromSerial(s, l):
|
||||
|
||||
# Parse the EXTH header records and use the Kindle serial number to calculate the book pid.
|
||||
def getKindlePids(rec209, token, serialnum):
|
||||
if rec209 is None:
|
||||
return [serialnum]
|
||||
|
||||
pids=[]
|
||||
|
||||
if isinstance(serialnum,unicode):
|
||||
@@ -196,31 +204,59 @@ def getK4Pids(rec209, token, kindleDatabase):
|
||||
pids = []
|
||||
|
||||
try:
|
||||
# Get the Mazama Random number
|
||||
MazamaRandomNumber = (kindleDatabase[1])['MazamaRandomNumber'].decode('hex')
|
||||
|
||||
# Get the kindle account token
|
||||
# Get the kindle account token, if present
|
||||
kindleAccountToken = (kindleDatabase[1])['kindle.account.tokens'].decode('hex')
|
||||
|
||||
# Get the IDString used to decode the Kindle Info file
|
||||
IDString = (kindleDatabase[1])['IDString'].decode('hex')
|
||||
|
||||
# Get the UserName stored when the Kindle Info file was decoded
|
||||
UserName = (kindleDatabase[1])['UserName'].decode('hex')
|
||||
|
||||
except KeyError:
|
||||
print u"Keys not found in the database {0}.".format(kindleDatabase[0])
|
||||
kindleAccountToken=""
|
||||
pass
|
||||
|
||||
try:
|
||||
# Get the DSN token, if present
|
||||
DSN = (kindleDatabase[1])['DSN'].decode('hex')
|
||||
print u"Got DSN key from database {0}".format(kindleDatabase[0])
|
||||
except KeyError:
|
||||
# See if we have the info to generate the DSN
|
||||
try:
|
||||
# Get the Mazama Random number
|
||||
MazamaRandomNumber = (kindleDatabase[1])['MazamaRandomNumber'].decode('hex')
|
||||
#print u"Got MazamaRandomNumber from database {0}".format(kindleDatabase[0])
|
||||
|
||||
try:
|
||||
# Get the SerialNumber token, if present
|
||||
IDString = (kindleDatabase[1])['SerialNumber'].decode('hex')
|
||||
print u"Got SerialNumber from database {0}".format(kindleDatabase[0])
|
||||
except KeyError:
|
||||
# Get the IDString we added
|
||||
IDString = (kindleDatabase[1])['IDString'].decode('hex')
|
||||
|
||||
try:
|
||||
# Get the UsernameHash token, if present
|
||||
encodedUsername = (kindleDatabase[1])['UsernameHash'].decode('hex')
|
||||
print u"Got UsernameHash from database {0}".format(kindleDatabase[0])
|
||||
except KeyError:
|
||||
# Get the UserName we added
|
||||
UserName = (kindleDatabase[1])['UserName'].decode('hex')
|
||||
# encode it
|
||||
encodedUsername = encodeHash(UserName,charMap1)
|
||||
#print u"encodedUsername",encodedUsername.encode('hex')
|
||||
except KeyError:
|
||||
print u"Keys not found in the database {0}.".format(kindleDatabase[0])
|
||||
return pids
|
||||
|
||||
# Get the ID string used
|
||||
encodedIDString = encodeHash(IDString,charMap1)
|
||||
#print u"encodedIDString",encodedIDString.encode('hex')
|
||||
|
||||
# concat, hash and encode to calculate the DSN
|
||||
DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1)
|
||||
#print u"DSN",DSN.encode('hex')
|
||||
pass
|
||||
|
||||
if rec209 is None:
|
||||
pids.append(DSN+kindleAccountToken)
|
||||
return pids
|
||||
|
||||
# Get the ID string used
|
||||
encodedIDString = encodeHash(IDString,charMap1)
|
||||
|
||||
# Get the current user name
|
||||
encodedUsername = encodeHash(UserName,charMap1)
|
||||
|
||||
# concat, hash and encode to calculate the DSN
|
||||
DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1)
|
||||
|
||||
# Compute the device PID (for which I can tell, is used for nothing).
|
||||
table = generatePidEncryptionTable()
|
||||
devicePID = generateDevicePID(table,DSN,4)
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
from __future__ import with_statement
|
||||
|
||||
# kindlekey.py
|
||||
# Copyright © 2010-2015 by some_updates, Apprentice Alf and Apprentice Harper
|
||||
# Copyright © 2008-2017 Apprentice Harper et al.
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '2.5'
|
||||
|
||||
# Revision history:
|
||||
# 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc.
|
||||
@@ -19,15 +22,18 @@ from __future__ import with_statement
|
||||
# 1.8 - Fixes for Kindle for Mac, and non-ascii in Windows user names
|
||||
# 1.9 - Fixes for Unicode in Windows user names
|
||||
# 2.0 - Added comments and extra fix for non-ascii Windows user names
|
||||
# 2.1 - Fixed Kindle for PC encryption changes March 2016
|
||||
# 2.2 - Fixes for Macs with bonded ethernet ports
|
||||
# Also removed old .kinfo file support (pre-2011)
|
||||
# 2.3 - Added more field names thanks to concavegit's KFX code.
|
||||
# 2.4 - Fix for complex Mac disk setups, thanks to Tibs
|
||||
# 2.5 - Final Fix for Windows user names with non-ascii characters, thanks to oneofusoneofus
|
||||
|
||||
|
||||
"""
|
||||
Retrieve Kindle for PC/Mac user key.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '1.9'
|
||||
|
||||
import sys, os, re
|
||||
from struct import pack, unpack, unpack_from
|
||||
import json
|
||||
@@ -882,10 +888,18 @@ if iswindows:
|
||||
if errcd == 234:
|
||||
# bad wine implementation up through wine 1.3.21
|
||||
return "AlternateUserName"
|
||||
# double the buffer size
|
||||
buffer = create_unicode_buffer(len(buffer) * 2)
|
||||
size.value = len(buffer)
|
||||
# return low byte of the unicode value of each character of the username
|
||||
return buffer.value.encode('utf-16-le')[::2]
|
||||
|
||||
# replace any non-ASCII values with 0xfffd
|
||||
for i in xrange(0,len(buffer)):
|
||||
if buffer[i]>u"\u007f":
|
||||
#print u"swapping char "+str(i)+" ("+buffer[i]+")"
|
||||
buffer[i] = u"\ufffd"
|
||||
# return utf-8 encoding of modified username
|
||||
#print u"modified username:"+buffer.value
|
||||
return buffer.value.encode('utf-8')
|
||||
return GetUserName
|
||||
GetUserName = GetUserName()
|
||||
|
||||
@@ -926,7 +940,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 +1007,128 @@ 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',\
|
||||
'SerialNumber',\
|
||||
'UsernameHash',\
|
||||
'kindle.directedid.info',\
|
||||
'DSN',\
|
||||
'kindle.accounttype.info',\
|
||||
'krx.flashcardsplugin.data.encryption_key',\
|
||||
'krx.notebookexportplugin.data.encryption_key',\
|
||||
'proxy.http.password',\
|
||||
'proxy.http.username'
|
||||
]
|
||||
DB = {}
|
||||
with open(kInfoFile, 'rb') as infoReader:
|
||||
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:
|
||||
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 +1260,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 +1271,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 ')
|
||||
(devpart, mpath) = resline.split(' on ')[:2]
|
||||
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 +1297,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 +1314,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 +1344,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 +1360,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 +1384,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 +1452,122 @@ 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',\
|
||||
'SerialNumber',\
|
||||
'UsernameHash',\
|
||||
'kindle.directedid.info',\
|
||||
'DSN'
|
||||
]
|
||||
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 +1631,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 +1661,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):
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# mobidedrm.py, version 0.38
|
||||
# mobidedrm.py
|
||||
# Copyright © 2008 The Dark Reverser
|
||||
#
|
||||
# Modified 2008–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
# Portions © 2008–2017 Apprentice Harper et al.
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = u"0.42"
|
||||
|
||||
# This is a python script. You need a Python interpreter to run it.
|
||||
# For example, ActiveState Python, which exists for windows.
|
||||
@@ -69,9 +71,7 @@
|
||||
# 0.39 - Fixed problem with TEXtREAd and getBookType interface
|
||||
# 0.40 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 0.41 - Fixed potential unicode problem in command line calls
|
||||
|
||||
|
||||
__version__ = u"0.41"
|
||||
# 0.42 - Added GPL v3 licence. updated/removed some print statements
|
||||
|
||||
import sys
|
||||
import os
|
||||
@@ -244,7 +244,7 @@ class MobiBook:
|
||||
pass
|
||||
|
||||
def __init__(self, infile):
|
||||
print u"MobiDeDrm v{0:s}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__)
|
||||
print u"MobiDeDrm v{0:s}.\nCopyright © 2008-2017 The Dark Reverser, Apprentice Harper et al.".format(__version__)
|
||||
|
||||
try:
|
||||
from alfcrypto import Pukall_Cipher
|
||||
@@ -288,10 +288,10 @@ class MobiBook:
|
||||
self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18])
|
||||
self.mobi_codepage, = struct.unpack('>L',self.sect[0x1c:0x20])
|
||||
self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C])
|
||||
print u"MOBI header version {0:d}, header length {1:d}".format(self.mobi_version, self.mobi_length)
|
||||
#print u"MOBI header version {0:d}, header length {1:d}".format(self.mobi_version, self.mobi_length)
|
||||
if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
|
||||
self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4])
|
||||
print u"Extra Data Flags: {0:d}".format(self.extra_data_flags)
|
||||
#print u"Extra Data Flags: {0:d}".format(self.extra_data_flags)
|
||||
if (self.compression != 17480):
|
||||
# multibyte utf8 data is included in the encryption for PalmDoc compression
|
||||
# so clear that byte so that we leave it to be decrypted.
|
||||
@@ -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'
|
||||
@@ -514,7 +516,7 @@ def cli_main():
|
||||
argv=unicode_argv()
|
||||
progname = os.path.basename(argv[0])
|
||||
if len(argv)<3 or len(argv)>4:
|
||||
print u"MobiDeDrm v{0}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__)
|
||||
print u"MobiDeDrm v{0:s}.\nCopyright © 2008-2017 The Dark Reverser, Apprentice Harper et al.".format(__version__)
|
||||
print u"Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks"
|
||||
print u"Usage:"
|
||||
print u" {0} <infile> <outfile> [<Comma separated list of PIDs to try>]".format(progname)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -352,7 +352,7 @@ class TopazBook:
|
||||
break
|
||||
|
||||
if not bookKey:
|
||||
raise DrmException(u"No key found in {0:d} keys tried. Read the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(len(pidlst)))
|
||||
raise DrmException(u"No key found in {0:d} keys tried. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(len(pidlst)))
|
||||
|
||||
self.setBookKey(bookKey)
|
||||
self.createBookDirectory()
|
||||
|
||||
@@ -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,15 +1,15 @@
|
||||
DeDRM_App - DeDRM_App.pyw and DeDRM_Drop_Target.bat
|
||||
===================================================
|
||||
|
||||
DeDRM_App.pyw is a python drag and drop application that allows users to drag and drop ebooks or folders of ebooks onto the DeDRM_Drop_Target.bat to have the DRM removed. It repackages all the "tools" python software (except obok) in one easy to use program that remembers preferences and settings.
|
||||
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, Adobe Digital Edition epub and pdf ebooks and Barnes & Noble NOOK Study ePubs when Kindle for PC, Adobe Digital Editions and NOOK Study are installed on the same computer and user account.
|
||||
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, 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
|
||||
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.
|
||||
|
||||
@@ -20,7 +20,7 @@ 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.2.2/DeDRM_Application_Windows to your "My Documents" folder.
|
||||
1. Drag the DeDRM_App folder from 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.
|
||||
|
||||
@@ -46,7 +46,7 @@ Installing Python on Windows
|
||||
----------------------------
|
||||
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.8 for Windows (or later 2.7.x version for Windows) from http://www.activestate.com/activepython/downloads.
|
||||
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.
|
||||
|
||||
|
||||
Binary file not shown.
@@ -55,7 +55,7 @@ p {margin-top: 0}
|
||||
<li>And probably many more.</li>
|
||||
</ul>
|
||||
|
||||
<h3> For additional help read the <a href="http://apprenticealf.wordpress.com/2011/01/17/frequently-asked-questions-about-the-drm-removal-tools/">FAQs</a> at <a href="http://apprenticealf.wordpress.com">Apprentice Alf’s Blog</a> and ask questions in the comments section of the <a href="http://apprenticealf.wordpress.com/2012/09/10/drm-removal-tools-for-ebooks/">first post</a>.</h3>
|
||||
<h3> For additional help read the <a href="https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md">FAQs</a> at <a href="https://github.com/apprenticeharper/DeDRM_tools/">Apprentice Harpers’s GitHub repository</a>. You can ask questions in the comments section of the <a href="http://apprenticealf.wordpress.com/2012/09/10/drm-removal-tools-for-ebooks/">first post</a> at <a href="http://wordpress.com/apprenticealf/">Apprentice Alf's blog</a> or <a href="https://github.com/apprenticeharper/DeDRM_tools/issues">raise an issue</a>. </h3>
|
||||
|
||||
<h2>Linux Systems Only</h2>
|
||||
<h3>Generating decryption keys for Adobe Digital Editions and Kindle for PC</h3>
|
||||
|
||||
@@ -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>
|
||||
@@ -1,52 +0,0 @@
|
||||
<!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 serial numbers</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 serial numbers</h1>
|
||||
|
||||
<p>Amazon's Kindle for Android application uses an internal serial number that's 72 character long. Extracting that serial number 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 the Kindle for Android serial number</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 the ‘backup.ab’ file you obtained by using the adb command. The backup file will be processed to extract any serial numbers in it, and the numbers will be added to the list.</p>
|
||||
|
||||
<h3>Adding the Kindle for Android serial number manually</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 a new Kindle for Android serial number.</p>
|
||||
<ul>
|
||||
<li><span class="bold">Kindle for Android Serial Number:</span> this is the unique serial number of your device. You may have obtained this through using the old android.py script.</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>
|
||||
|
||||
<h3>Deleting Kindle for Android serial numbers:</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 Kindle serial number from the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.</p>
|
||||
|
||||
<p>Once done creating/deleting serial numbers, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.</p>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -2,6 +2,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# __init__.py for DeDRM_plugin
|
||||
# Copyright © 2008-2018 Apprentice Harper et al.
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
@@ -9,8 +13,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.
|
||||
#
|
||||
@@ -43,6 +45,26 @@ __docformat__ = 'restructuredtext en'
|
||||
# 6.2.2 - Added URL method for B&N/nook books
|
||||
# 6.3.0 - Added in Kindle for Android serial number solution
|
||||
# 6.3.1 - Version number bump for clarity
|
||||
# 6.3.2 - Fixed Kindle for Android help file
|
||||
# 6.3.3 - Bug fix for Kindle for PC support
|
||||
# 6.3.4 - Fixes for Kindle for Android, Linux, and Kobo 3.17
|
||||
# 6.3.5 - Fixes for Linux, and Kobo 3.19 and more logging
|
||||
# 6.3.6 - Fixes for ADE ePub and PDF introduced in 6.3.5
|
||||
# 6.4.0 - Updated for new Kindle for PC encryption
|
||||
# 6.4.1 - Fix for some new tags in Topaz ebooks.
|
||||
# 6.4.2 - Fix for more new tags in Topaz ebooks and very small Topaz ebooks
|
||||
# 6.4.3 - Fix for error that only appears when not in debug mode
|
||||
# Also includes fix for Macs with bonded ethernet ports
|
||||
# 6.5.0 - Big update to Macintosh app
|
||||
# Fix for some more 'new' tags in Topaz ebooks.
|
||||
# Fix an error in wineutils.py
|
||||
# 6.5.1 - Updated version number, added PDF check for DRM-free documents
|
||||
# 6.5.2 - Another Topaz fix
|
||||
# 6.5.3 - Warn about KFX files explicitly
|
||||
# 6.5.4 - Mac App Fix, improve PDF decryption, handle latest tcl changes in ActivePython
|
||||
# 6.5.5 - Finally a fix for the Windows non-ASCII user names.
|
||||
# 6.6.0 - Add kfx and kfx-zip as supported file types (also invoke this plugin if the original
|
||||
# imported format was azw8 since that may be converted to kfx)
|
||||
|
||||
|
||||
"""
|
||||
@@ -50,7 +72,7 @@ Decrypt DRMed ebooks.
|
||||
"""
|
||||
|
||||
PLUGIN_NAME = u"DeDRM"
|
||||
PLUGIN_VERSION_TUPLE = (6, 3, 1)
|
||||
PLUGIN_VERSION_TUPLE = (6, 6, 0)
|
||||
PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE])
|
||||
# Include an html helpfile in the plugin's zipfile with the following name.
|
||||
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
|
||||
@@ -82,8 +104,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)
|
||||
|
||||
@@ -93,8 +119,8 @@ class DeDRM(FileTypePlugin):
|
||||
supported_platforms = ['linux', 'osx', 'windows']
|
||||
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','pobi','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','azw8','tpz','kfx','kfx-zip'])
|
||||
on_import = True
|
||||
priority = 600
|
||||
|
||||
@@ -146,7 +172,7 @@ class DeDRM(FileTypePlugin):
|
||||
try:
|
||||
open(file_path,'wb').write(data)
|
||||
except:
|
||||
print u"{0} v{1}: Exception when copying needed library files after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
|
||||
print u"{0} v{1}: Exception when copying needed library files".format(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
traceback.print_exc()
|
||||
pass
|
||||
|
||||
@@ -270,8 +296,8 @@ class DeDRM(FileTypePlugin):
|
||||
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 after {2:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
|
||||
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
|
||||
# import the Adobe Adept ePub handler
|
||||
import calibre_plugins.dedrm.ineptepub as ineptepub
|
||||
@@ -293,11 +319,15 @@ class DeDRM(FileTypePlugin):
|
||||
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)
|
||||
@@ -357,16 +387,19 @@ class DeDRM(FileTypePlugin):
|
||||
except:
|
||||
print u"{0} v{1}: Exception when saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
|
||||
traceback.print_exc()
|
||||
print u"{0} v{1}: Decrypted with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
|
||||
# Return the modified PersistentTemporary file to calibre.
|
||||
return of.name
|
||||
|
||||
print u"{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
|
||||
except Exception, e:
|
||||
print u"{0} v{1}: Unexpected Exception trying a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
|
||||
traceback.print_exc()
|
||||
pass
|
||||
|
||||
# Something went wrong with decryption.
|
||||
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\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 after {2:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
|
||||
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
|
||||
# Not a Barnes & Noble nor an Adobe Adept
|
||||
# Import the fixed epub.
|
||||
@@ -465,8 +498,8 @@ class DeDRM(FileTypePlugin):
|
||||
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 after {2:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
|
||||
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
|
||||
|
||||
def KindleMobiDecrypt(self,path_to_ebook):
|
||||
@@ -522,7 +555,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")
|
||||
@@ -533,8 +566,8 @@ class DeDRM(FileTypePlugin):
|
||||
pass
|
||||
if not decoded:
|
||||
#if you reached here then no luck raise and exception
|
||||
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\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 after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
|
||||
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
|
||||
of = self.temporary_file(book.getBookExtension())
|
||||
book.getFile(of.name)
|
||||
@@ -568,8 +601,8 @@ 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)
|
||||
|
||||
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 after {2:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
|
||||
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
|
||||
|
||||
def run(self, path_to_ebook):
|
||||
@@ -582,7 +615,7 @@ class DeDRM(FileTypePlugin):
|
||||
self.starttime = time.time()
|
||||
|
||||
booktype = os.path.splitext(path_to_ebook)[1].lower()[1:]
|
||||
if booktype in ['prc','mobi','pobi','azw','azw1','azw3','azw4','tpz']:
|
||||
if booktype in ['prc','mobi','pobi','azw','azw1','azw3','azw4','tpz','kfx-zip']:
|
||||
# Kindle/Mobipocket
|
||||
decrypted_ebook = self.KindleMobiDecrypt(path_to_ebook)
|
||||
elif booktype == 'pdb':
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# adobekey.pyw, version 5.7
|
||||
# adobekey.pyw, version 6.0
|
||||
# Copyright © 2009-2010 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 2010–2016 by several people
|
||||
|
||||
# Windows users: Before running this program, you must first install Python.
|
||||
# We recommend ActiveState Python 2.7.X for Windows (x86) from
|
||||
|
||||
@@ -16,16 +16,18 @@ from __future__ import with_statement
|
||||
# - 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.4'
|
||||
__version__ = '1.5'
|
||||
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
import getopt
|
||||
import tempfile
|
||||
import zlib
|
||||
@@ -220,20 +222,30 @@ def get_serials2(path=STORAGE2):
|
||||
userdata_keys = cursor.fetchall()
|
||||
dsns = []
|
||||
for userdata_row in userdata_keys:
|
||||
if userdata_row:
|
||||
userdata_utf8 = userdata_row[0].encode('utf8')
|
||||
if len(userdata_utf8) > 0:
|
||||
dsns.append(userdata_utf8)
|
||||
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:
|
||||
if userdata_row:
|
||||
userdata_utf8 = userdata_row[0].encode('utf8')
|
||||
if len(userdata_utf8) > 0:
|
||||
tokens.append(userdata_utf8)
|
||||
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 = []
|
||||
@@ -377,7 +389,6 @@ def gui_main():
|
||||
import Tkconstants
|
||||
import tkMessageBox
|
||||
import tkFileDialog
|
||||
import traceback
|
||||
except:
|
||||
print "Tkinter not installed"
|
||||
return cli_main()
|
||||
|
||||
@@ -124,7 +124,7 @@ class ConfigWidget(QWidget):
|
||||
d.exec_()
|
||||
|
||||
def kindle_android(self):
|
||||
d = ManageKeysDialog(self,u"Kindle for Android Keys File",self.tempdedrmprefs['androidkeys'], AddAndroidDialog, 'k4a')
|
||||
d = ManageKeysDialog(self,u"Kindle for Android Key",self.tempdedrmprefs['androidkeys'], AddAndroidDialog, 'k4a')
|
||||
d.exec_()
|
||||
|
||||
def kindle_keys(self):
|
||||
@@ -566,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)
|
||||
@@ -579,8 +592,7 @@ class AddBandNKeyDialog(QDialog):
|
||||
|
||||
@property
|
||||
def key_value(self):
|
||||
from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key
|
||||
return fetch_bandn_key(self.user_name,self.cc_number)
|
||||
return unicode(self.key_display.text()).strip()
|
||||
|
||||
@property
|
||||
def user_name(self):
|
||||
@@ -590,6 +602,14 @@ class AddBandNKeyDialog(QDialog):
|
||||
def cc_number(self):
|
||||
return unicode(self.cc_ledit.text()).strip()
|
||||
|
||||
def retrieve_key(self):
|
||||
from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key
|
||||
fetched_key = fetch_bandn_key(self.user_name,self.cc_number)
|
||||
if fetched_key == "":
|
||||
errmsg = u"Could not retrieve key. Check username, password and intenet connectivity and try again."
|
||||
error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
else:
|
||||
self.key_display.setText(fetched_key)
|
||||
|
||||
def accept(self):
|
||||
if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace():
|
||||
@@ -598,6 +618,10 @@ class AddBandNKeyDialog(QDialog):
|
||||
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):
|
||||
|
||||
@@ -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,19 @@ 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),
|
||||
'src' : (1, 'text', 0, 0),
|
||||
|
||||
}
|
||||
|
||||
@@ -506,8 +528,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)
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ignobleepub.pyw, version 3.8
|
||||
# ignobleepub.pyw, version 4.1
|
||||
# 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–2017 by Apprentice Harper
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# from <http://www.python.org/download/> and PyCrypto from
|
||||
@@ -35,13 +36,14 @@ from __future__ import with_statement
|
||||
# 3.8 - Fixed to retain zip file metadata (e.g. file modification date)
|
||||
# 3.9 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 4.0 - Work if TkInter is missing
|
||||
# 4.1 - Import tkFileDialog, don't assume something else will import it.
|
||||
|
||||
"""
|
||||
Decrypt Barnes & Noble encrypted ePub books.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "4.0"
|
||||
__version__ = "4.1"
|
||||
|
||||
import sys
|
||||
import os
|
||||
@@ -337,6 +339,7 @@ def gui_main():
|
||||
try:
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
import traceback
|
||||
except:
|
||||
|
||||
@@ -3,18 +3,19 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ineptepub.pyw, version 6.1
|
||||
# ineptepub.pyw, version 6.6
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2013 by some_updates, DiapDealer and Apprentice Alf
|
||||
# Modified 2015–2017 by Apprentice Harper
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# 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
|
||||
@@ -38,13 +39,17 @@ from __future__ import with_statement
|
||||
# 6.0 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 6.1 - Work if TkInter is missing
|
||||
# 6.2 - Handle UTF-8 file names inside an ePub, fix by Jose Luis
|
||||
# 6.3 - Add additional check on DER file sanity
|
||||
# 6.4 - Remove erroneous check on DER file sanity
|
||||
# 6.5 - Completely remove erroneous check on DER file sanity
|
||||
# 6.6 - Import tkFileDialog, don't assume something else will import it.
|
||||
|
||||
"""
|
||||
Decrypt Adobe Digital Editions encrypted ePub books.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "6.2"
|
||||
__version__ = "6.6"
|
||||
|
||||
import sys
|
||||
import os
|
||||
@@ -480,6 +485,7 @@ def gui_main():
|
||||
try:
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
import traceback
|
||||
except:
|
||||
|
||||
@@ -3,18 +3,19 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ineptpdf.pyw, version 7.11
|
||||
# ineptpdf.pyw, version 8.0.6
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
# Modified 2015-2017 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,19 @@ 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
|
||||
# 8.0.5 - Do not process DRM-free documents
|
||||
# 8.0.6 - Replace use of float by Decimal for greater precision, and import tkFileDialog
|
||||
|
||||
|
||||
"""
|
||||
Decrypts Adobe ADEPT-encrypted PDF files.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "8.0.1"
|
||||
__version__ = "8.0.6"
|
||||
|
||||
import sys
|
||||
import os
|
||||
@@ -67,6 +74,7 @@ import re
|
||||
import zlib
|
||||
import struct
|
||||
import hashlib
|
||||
from decimal import *
|
||||
from itertools import chain, islice
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
@@ -647,7 +655,7 @@ class PSBaseParser(object):
|
||||
return (self.parse_number, j+1)
|
||||
if c == '.':
|
||||
self.token = c
|
||||
return (self.parse_float, j+1)
|
||||
return (self.parse_decimal, j+1)
|
||||
if c.isalpha():
|
||||
self.token = c
|
||||
return (self.parse_keyword, j+1)
|
||||
@@ -712,20 +720,21 @@ class PSBaseParser(object):
|
||||
c = s[j]
|
||||
if c == '.':
|
||||
self.token += c
|
||||
return (self.parse_float, j+1)
|
||||
return (self.parse_decimal, j+1)
|
||||
try:
|
||||
self.add_token(int(self.token))
|
||||
except ValueError:
|
||||
pass
|
||||
return (self.parse_main, j)
|
||||
def parse_float(self, s, i):
|
||||
|
||||
def parse_decimal(self, s, i):
|
||||
m = END_NUMBER.search(s, i)
|
||||
if not m:
|
||||
self.token += s[i:]
|
||||
return (self.parse_float, len(s))
|
||||
return (self.parse_decimal, len(s))
|
||||
j = m.start(0)
|
||||
self.token += s[i:j]
|
||||
self.add_token(float(self.token))
|
||||
self.add_token(Decimal(self.token))
|
||||
return (self.parse_main, j)
|
||||
|
||||
def parse_keyword(self, s, i):
|
||||
@@ -927,7 +936,7 @@ class PSStackParser(PSBaseParser):
|
||||
(pos, token) = self.nexttoken()
|
||||
##print (pos,token), (self.curtype, self.curstack)
|
||||
if (isinstance(token, int) or
|
||||
isinstance(token, float) or
|
||||
isinstance(token, Decimal) or
|
||||
isinstance(token, bool) or
|
||||
isinstance(token, str) or
|
||||
isinstance(token, PSLiteral)):
|
||||
@@ -1056,17 +1065,17 @@ def int_value(x):
|
||||
return 0
|
||||
return x
|
||||
|
||||
def float_value(x):
|
||||
def decimal_value(x):
|
||||
x = resolve1(x)
|
||||
if not isinstance(x, float):
|
||||
if not isinstance(x, Decimal):
|
||||
if STRICT:
|
||||
raise PDFTypeError('Float required: %r' % x)
|
||||
raise PDFTypeError('Decimal required: %r' % x)
|
||||
return 0.0
|
||||
return x
|
||||
|
||||
def num_value(x):
|
||||
x = resolve1(x)
|
||||
if not (isinstance(x, int) or isinstance(x, float)):
|
||||
if not (isinstance(x, int) or isinstance(x, Decimal)):
|
||||
if STRICT:
|
||||
raise PDFTypeError('Int or Float required: %r' % x)
|
||||
return 0
|
||||
@@ -1463,6 +1472,7 @@ class PDFDocument(object):
|
||||
if not self.encryption:
|
||||
self.is_printable = self.is_modifiable = self.is_extractable = True
|
||||
self.ready = True
|
||||
raise PDFEncryptionError('Document is not encrypted.')
|
||||
return
|
||||
(docid, param) = self.encryption
|
||||
type = literal_name(param['Filter'])
|
||||
@@ -2135,7 +2145,11 @@ class PDFSerializer(object):
|
||||
if self.last.isalnum():
|
||||
self.write(' ')
|
||||
self.write(str(obj).lower())
|
||||
elif isinstance(obj, (int, long, float)):
|
||||
elif isinstance(obj, (int, long)):
|
||||
if self.last.isalnum():
|
||||
self.write(' ')
|
||||
self.write(str(obj))
|
||||
elif isinstance(obj, Decimal):
|
||||
if self.last.isalnum():
|
||||
self.write(' ')
|
||||
self.write(str(obj))
|
||||
@@ -2175,11 +2189,11 @@ def decryptBook(userkey, inpath, outpath):
|
||||
if RSA is None:
|
||||
raise ADEPTError(u"PyCrypto or OpenSSL must be installed.")
|
||||
with open(inpath, 'rb') as inf:
|
||||
try:
|
||||
serializer = PDFSerializer(inf, userkey)
|
||||
except:
|
||||
print u"Error serializing pdf {0}. Probably wrong key.".format(os.path.basename(inpath))
|
||||
return 2
|
||||
#try:
|
||||
serializer = PDFSerializer(inf, userkey)
|
||||
#except:
|
||||
# print u"Error serializing pdf {0}. Probably wrong key.".format(os.path.basename(inpath))
|
||||
# return 2
|
||||
# hope this will fix the 'bad file descriptor' problem
|
||||
with open(outpath, 'wb') as outf:
|
||||
# help construct to make sure the method runs to the end
|
||||
@@ -2211,6 +2225,7 @@ def gui_main():
|
||||
try:
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
import traceback
|
||||
except:
|
||||
|
||||
981
DeDRM_calibre_plugin/DeDRM_plugin/ion.py
Normal file
981
DeDRM_calibre_plugin/DeDRM_plugin/ion.py
Normal file
@@ -0,0 +1,981 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Pascal implementation by lulzkabulz. Python translation by apprenticenaomi. DeDRM integration by anon.
|
||||
# BinaryIon.pas + DrmIon.pas + IonSymbols.pas
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import collections
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
import os.path
|
||||
import struct
|
||||
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except ImportError:
|
||||
from StringIO import StringIO
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.py3compat import bchr, bord
|
||||
|
||||
try:
|
||||
# lzma library from calibre 2.35.0 or later
|
||||
import lzma.lzma1 as calibre_lzma
|
||||
except:
|
||||
calibre_lzma = None
|
||||
try:
|
||||
import lzma
|
||||
except:
|
||||
# Need pip backports.lzma on Python <3.3
|
||||
from backports import lzma
|
||||
|
||||
|
||||
TID_NULL = 0
|
||||
TID_BOOLEAN = 1
|
||||
TID_POSINT = 2
|
||||
TID_NEGINT = 3
|
||||
TID_FLOAT = 4
|
||||
TID_DECIMAL = 5
|
||||
TID_TIMESTAMP = 6
|
||||
TID_SYMBOL = 7
|
||||
TID_STRING = 8
|
||||
TID_CLOB = 9
|
||||
TID_BLOB = 0xA
|
||||
TID_LIST = 0xB
|
||||
TID_SEXP = 0xC
|
||||
TID_STRUCT = 0xD
|
||||
TID_TYPEDECL = 0xE
|
||||
TID_UNUSED = 0xF
|
||||
|
||||
|
||||
SID_UNKNOWN = -1
|
||||
SID_ION = 1
|
||||
SID_ION_1_0 = 2
|
||||
SID_ION_SYMBOL_TABLE = 3
|
||||
SID_NAME = 4
|
||||
SID_VERSION = 5
|
||||
SID_IMPORTS = 6
|
||||
SID_SYMBOLS = 7
|
||||
SID_MAX_ID = 8
|
||||
SID_ION_SHARED_SYMBOL_TABLE = 9
|
||||
SID_ION_1_0_MAX = 10
|
||||
|
||||
|
||||
LEN_IS_VAR_LEN = 0xE
|
||||
LEN_IS_NULL = 0xF
|
||||
|
||||
|
||||
VERSION_MARKER = b"\x01\x00\xEA"
|
||||
|
||||
|
||||
# asserts must always raise exceptions for proper functioning
|
||||
def _assert(test, msg="Exception"):
|
||||
if not test:
|
||||
raise Exception(msg)
|
||||
|
||||
|
||||
class SystemSymbols(object):
|
||||
ION = '$ion'
|
||||
ION_1_0 = '$ion_1_0'
|
||||
ION_SYMBOL_TABLE = '$ion_symbol_table'
|
||||
NAME = 'name'
|
||||
VERSION = 'version'
|
||||
IMPORTS = 'imports'
|
||||
SYMBOLS = 'symbols'
|
||||
MAX_ID = 'max_id'
|
||||
ION_SHARED_SYMBOL_TABLE = '$ion_shared_symbol_table'
|
||||
|
||||
|
||||
class IonCatalogItem(object):
|
||||
name = ""
|
||||
version = 0
|
||||
symnames = []
|
||||
|
||||
def __init__(self, name, version, symnames):
|
||||
self.name = name
|
||||
self.version = version
|
||||
self.symnames = symnames
|
||||
|
||||
|
||||
class SymbolToken(object):
|
||||
text = ""
|
||||
sid = 0
|
||||
|
||||
def __init__(self, text, sid):
|
||||
if text == "" and sid == 0:
|
||||
raise ValueError("Symbol token must have Text or SID")
|
||||
|
||||
self.text = text
|
||||
self.sid = sid
|
||||
|
||||
|
||||
class SymbolTable(object):
|
||||
table = None
|
||||
|
||||
def __init__(self):
|
||||
self.table = [None] * SID_ION_1_0_MAX
|
||||
self.table[SID_ION] = SystemSymbols.ION
|
||||
self.table[SID_ION_1_0] = SystemSymbols.ION_1_0
|
||||
self.table[SID_ION_SYMBOL_TABLE] = SystemSymbols.ION_SYMBOL_TABLE
|
||||
self.table[SID_NAME] = SystemSymbols.NAME
|
||||
self.table[SID_VERSION] = SystemSymbols.VERSION
|
||||
self.table[SID_IMPORTS] = SystemSymbols.IMPORTS
|
||||
self.table[SID_SYMBOLS] = SystemSymbols.SYMBOLS
|
||||
self.table[SID_MAX_ID] = SystemSymbols.MAX_ID
|
||||
self.table[SID_ION_SHARED_SYMBOL_TABLE] = SystemSymbols.ION_SHARED_SYMBOL_TABLE
|
||||
|
||||
def findbyid(self, sid):
|
||||
if sid < 1:
|
||||
raise ValueError("Invalid symbol id")
|
||||
|
||||
if sid < len(self.table):
|
||||
return self.table[sid]
|
||||
else:
|
||||
return ""
|
||||
|
||||
def import_(self, table, maxid):
|
||||
for i in range(maxid):
|
||||
self.table.append(table.symnames[i])
|
||||
|
||||
def importunknown(self, name, maxid):
|
||||
for i in range(maxid):
|
||||
self.table.append("%s#%d" % (name, i + 1))
|
||||
|
||||
|
||||
class ParserState:
|
||||
Invalid,BeforeField,BeforeTID,BeforeValue,AfterValue,EOF = 1,2,3,4,5,6
|
||||
|
||||
ContainerRec = collections.namedtuple("ContainerRec", "nextpos, tid, remaining")
|
||||
|
||||
|
||||
class BinaryIonParser(object):
|
||||
eof = False
|
||||
state = None
|
||||
localremaining = 0
|
||||
needhasnext = False
|
||||
isinstruct = False
|
||||
valuetid = 0
|
||||
valuefieldid = 0
|
||||
parenttid = 0
|
||||
valuelen = 0
|
||||
valueisnull = False
|
||||
valueistrue = False
|
||||
value = None
|
||||
didimports = False
|
||||
|
||||
def __init__(self, stream):
|
||||
self.annotations = []
|
||||
self.catalog = []
|
||||
|
||||
self.stream = stream
|
||||
self.initpos = stream.tell()
|
||||
self.reset()
|
||||
self.symbols = SymbolTable()
|
||||
|
||||
def reset(self):
|
||||
self.state = ParserState.BeforeTID
|
||||
self.needhasnext = True
|
||||
self.localremaining = -1
|
||||
self.eof = False
|
||||
self.isinstruct = False
|
||||
self.containerstack = []
|
||||
self.stream.seek(self.initpos)
|
||||
|
||||
def addtocatalog(self, name, version, symbols):
|
||||
self.catalog.append(IonCatalogItem(name, version, symbols))
|
||||
|
||||
def hasnext(self):
|
||||
while self.needhasnext and not self.eof:
|
||||
self.hasnextraw()
|
||||
if len(self.containerstack) == 0 and not self.valueisnull:
|
||||
if self.valuetid == TID_SYMBOL:
|
||||
if self.value == SID_ION_1_0:
|
||||
self.needhasnext = True
|
||||
elif self.valuetid == TID_STRUCT:
|
||||
for a in self.annotations:
|
||||
if a == SID_ION_SYMBOL_TABLE:
|
||||
self.parsesymboltable()
|
||||
self.needhasnext = True
|
||||
break
|
||||
return not self.eof
|
||||
|
||||
def hasnextraw(self):
|
||||
self.clearvalue()
|
||||
while self.valuetid == -1 and not self.eof:
|
||||
self.needhasnext = False
|
||||
if self.state == ParserState.BeforeField:
|
||||
_assert(self.valuefieldid == SID_UNKNOWN)
|
||||
|
||||
self.valuefieldid = self.readfieldid()
|
||||
if self.valuefieldid != SID_UNKNOWN:
|
||||
self.state = ParserState.BeforeTID
|
||||
else:
|
||||
self.eof = True
|
||||
|
||||
elif self.state == ParserState.BeforeTID:
|
||||
self.state = ParserState.BeforeValue
|
||||
self.valuetid = self.readtypeid()
|
||||
if self.valuetid == -1:
|
||||
self.state = ParserState.EOF
|
||||
self.eof = True
|
||||
break
|
||||
|
||||
if self.valuetid == TID_TYPEDECL:
|
||||
if self.valuelen == 0:
|
||||
self.checkversionmarker()
|
||||
else:
|
||||
self.loadannotations()
|
||||
|
||||
elif self.state == ParserState.BeforeValue:
|
||||
self.skip(self.valuelen)
|
||||
self.state = ParserState.AfterValue
|
||||
|
||||
elif self.state == ParserState.AfterValue:
|
||||
if self.isinstruct:
|
||||
self.state = ParserState.BeforeField
|
||||
else:
|
||||
self.state = ParserState.BeforeTID
|
||||
|
||||
else:
|
||||
_assert(self.state == ParserState.EOF)
|
||||
|
||||
def next(self):
|
||||
if self.hasnext():
|
||||
self.needhasnext = True
|
||||
return self.valuetid
|
||||
else:
|
||||
return -1
|
||||
|
||||
def push(self, typeid, nextposition, nextremaining):
|
||||
self.containerstack.append(ContainerRec(nextpos=nextposition, tid=typeid, remaining=nextremaining))
|
||||
|
||||
def stepin(self):
|
||||
_assert(self.valuetid in [TID_STRUCT, TID_LIST, TID_SEXP] and not self.eof,
|
||||
"valuetid=%s eof=%s" % (self.valuetid, self.eof))
|
||||
_assert((not self.valueisnull or self.state == ParserState.AfterValue) and
|
||||
(self.valueisnull or self.state == ParserState.BeforeValue))
|
||||
|
||||
nextrem = self.localremaining
|
||||
if nextrem != -1:
|
||||
nextrem -= self.valuelen
|
||||
if nextrem < 0:
|
||||
nextrem = 0
|
||||
self.push(self.parenttid, self.stream.tell() + self.valuelen, nextrem)
|
||||
|
||||
self.isinstruct = (self.valuetid == TID_STRUCT)
|
||||
if self.isinstruct:
|
||||
self.state = ParserState.BeforeField
|
||||
else:
|
||||
self.state = ParserState.BeforeTID
|
||||
|
||||
self.localremaining = self.valuelen
|
||||
self.parenttid = self.valuetid
|
||||
self.clearvalue()
|
||||
self.needhasnext = True
|
||||
|
||||
def stepout(self):
|
||||
rec = self.containerstack.pop()
|
||||
|
||||
self.eof = False
|
||||
self.parenttid = rec.tid
|
||||
if self.parenttid == TID_STRUCT:
|
||||
self.isinstruct = True
|
||||
self.state = ParserState.BeforeField
|
||||
else:
|
||||
self.isinstruct = False
|
||||
self.state = ParserState.BeforeTID
|
||||
self.needhasnext = True
|
||||
|
||||
self.clearvalue()
|
||||
curpos = self.stream.tell()
|
||||
if rec.nextpos > curpos:
|
||||
self.skip(rec.nextpos - curpos)
|
||||
else:
|
||||
_assert(rec.nextpos == curpos)
|
||||
|
||||
self.localremaining = rec.remaining
|
||||
|
||||
def read(self, count=1):
|
||||
if self.localremaining != -1:
|
||||
self.localremaining -= count
|
||||
_assert(self.localremaining >= 0)
|
||||
|
||||
result = self.stream.read(count)
|
||||
if len(result) == 0:
|
||||
raise EOFError()
|
||||
return result
|
||||
|
||||
def readfieldid(self):
|
||||
if self.localremaining != -1 and self.localremaining < 1:
|
||||
return -1
|
||||
|
||||
try:
|
||||
return self.readvaruint()
|
||||
except EOFError:
|
||||
return -1
|
||||
|
||||
def readtypeid(self):
|
||||
if self.localremaining != -1:
|
||||
if self.localremaining < 1:
|
||||
return -1
|
||||
self.localremaining -= 1
|
||||
|
||||
b = self.stream.read(1)
|
||||
if len(b) < 1:
|
||||
return -1
|
||||
b = bord(b)
|
||||
result = b >> 4
|
||||
ln = b & 0xF
|
||||
|
||||
if ln == LEN_IS_VAR_LEN:
|
||||
ln = self.readvaruint()
|
||||
elif ln == LEN_IS_NULL:
|
||||
ln = 0
|
||||
self.state = ParserState.AfterValue
|
||||
elif result == TID_NULL:
|
||||
# Must have LEN_IS_NULL
|
||||
_assert(False)
|
||||
elif result == TID_BOOLEAN:
|
||||
_assert(ln <= 1)
|
||||
self.valueistrue = (ln == 1)
|
||||
ln = 0
|
||||
self.state = ParserState.AfterValue
|
||||
elif result == TID_STRUCT:
|
||||
if ln == 1:
|
||||
ln = self.readvaruint()
|
||||
|
||||
self.valuelen = ln
|
||||
return result
|
||||
|
||||
def readvarint(self):
|
||||
b = bord(self.read())
|
||||
negative = ((b & 0x40) != 0)
|
||||
result = (b & 0x3F)
|
||||
|
||||
i = 0
|
||||
while (b & 0x80) == 0 and i < 4:
|
||||
b = bord(self.read())
|
||||
result = (result << 7) | (b & 0x7F)
|
||||
i += 1
|
||||
|
||||
_assert(i < 4 or (b & 0x80) != 0, "int overflow")
|
||||
|
||||
if negative:
|
||||
return -result
|
||||
return result
|
||||
|
||||
def readvaruint(self):
|
||||
b = bord(self.read())
|
||||
result = (b & 0x7F)
|
||||
|
||||
i = 0
|
||||
while (b & 0x80) == 0 and i < 4:
|
||||
b = bord(self.read())
|
||||
result = (result << 7) | (b & 0x7F)
|
||||
i += 1
|
||||
|
||||
_assert(i < 4 or (b & 0x80) != 0, "int overflow")
|
||||
|
||||
return result
|
||||
|
||||
def readdecimal(self):
|
||||
if self.valuelen == 0:
|
||||
return 0.
|
||||
|
||||
rem = self.localremaining - self.valuelen
|
||||
self.localremaining = self.valuelen
|
||||
exponent = self.readvarint()
|
||||
|
||||
_assert(self.localremaining > 0, "Only exponent in ReadDecimal")
|
||||
_assert(self.localremaining <= 8, "Decimal overflow")
|
||||
|
||||
signed = False
|
||||
b = [bord(x) for x in self.read(self.localremaining)]
|
||||
if (b[0] & 0x80) != 0:
|
||||
b[0] = b[0] & 0x7F
|
||||
signed = True
|
||||
|
||||
# Convert variably sized network order integer into 64-bit little endian
|
||||
j = 0
|
||||
vb = [0] * 8
|
||||
for i in range(len(b), -1, -1):
|
||||
vb[i] = b[j]
|
||||
j += 1
|
||||
|
||||
v = struct.unpack("<Q", b"".join(bchr(x) for x in vb))[0]
|
||||
|
||||
result = v * (10 ** exponent)
|
||||
if signed:
|
||||
result = -result
|
||||
|
||||
self.localremaining = rem
|
||||
return result
|
||||
|
||||
def skip(self, count):
|
||||
if self.localremaining != -1:
|
||||
self.localremaining -= count
|
||||
if self.localremaining < 0:
|
||||
raise EOFError()
|
||||
|
||||
self.stream.seek(count, os.SEEK_CUR)
|
||||
|
||||
def parsesymboltable(self):
|
||||
self.next() # shouldn't do anything?
|
||||
|
||||
_assert(self.valuetid == TID_STRUCT)
|
||||
|
||||
if self.didimports:
|
||||
return
|
||||
|
||||
self.stepin()
|
||||
|
||||
fieldtype = self.next()
|
||||
while fieldtype != -1:
|
||||
if not self.valueisnull:
|
||||
_assert(self.valuefieldid == SID_IMPORTS, "Unsupported symbol table field id")
|
||||
|
||||
if fieldtype == TID_LIST:
|
||||
self.gatherimports()
|
||||
|
||||
fieldtype = self.next()
|
||||
|
||||
self.stepout()
|
||||
self.didimports = True
|
||||
|
||||
def gatherimports(self):
|
||||
self.stepin()
|
||||
|
||||
t = self.next()
|
||||
while t != -1:
|
||||
if not self.valueisnull and t == TID_STRUCT:
|
||||
self.readimport()
|
||||
|
||||
t = self.next()
|
||||
|
||||
self.stepout()
|
||||
|
||||
def readimport(self):
|
||||
version = -1
|
||||
maxid = -1
|
||||
name = ""
|
||||
|
||||
self.stepin()
|
||||
|
||||
t = self.next()
|
||||
while t != -1:
|
||||
if not self.valueisnull and self.valuefieldid != SID_UNKNOWN:
|
||||
if self.valuefieldid == SID_NAME:
|
||||
name = self.stringvalue()
|
||||
elif self.valuefieldid == SID_VERSION:
|
||||
version = self.intvalue()
|
||||
elif self.valuefieldid == SID_MAX_ID:
|
||||
maxid = self.intvalue()
|
||||
|
||||
t = self.next()
|
||||
|
||||
self.stepout()
|
||||
|
||||
if name == "" or name == SystemSymbols.ION:
|
||||
return
|
||||
|
||||
if version < 1:
|
||||
version = 1
|
||||
|
||||
table = self.findcatalogitem(name)
|
||||
if maxid < 0:
|
||||
_assert(table is not None and version == table.version, "Import %s lacks maxid" % name)
|
||||
maxid = len(table.symnames)
|
||||
|
||||
if table is not None:
|
||||
self.symbols.import_(table, min(maxid, len(table.symnames)))
|
||||
else:
|
||||
self.symbols.importunknown(name, maxid)
|
||||
|
||||
def intvalue(self):
|
||||
_assert(self.valuetid in [TID_POSINT, TID_NEGINT], "Not an int")
|
||||
|
||||
self.preparevalue()
|
||||
return self.value
|
||||
|
||||
def stringvalue(self):
|
||||
_assert(self.valuetid == TID_STRING, "Not a string")
|
||||
|
||||
if self.valueisnull:
|
||||
return ""
|
||||
|
||||
self.preparevalue()
|
||||
return self.value
|
||||
|
||||
def symbolvalue(self):
|
||||
_assert(self.valuetid == TID_SYMBOL, "Not a symbol")
|
||||
|
||||
self.preparevalue()
|
||||
result = self.symbols.findbyid(self.value)
|
||||
if result == "":
|
||||
result = "SYMBOL#%d" % self.value
|
||||
return result
|
||||
|
||||
def lobvalue(self):
|
||||
_assert(self.valuetid in [TID_CLOB, TID_BLOB], "Not a LOB type: %s" % self.getfieldname())
|
||||
|
||||
if self.valueisnull:
|
||||
return None
|
||||
|
||||
result = self.read(self.valuelen)
|
||||
self.state = ParserState.AfterValue
|
||||
return result
|
||||
|
||||
def decimalvalue(self):
|
||||
_assert(self.valuetid == TID_DECIMAL, "Not a decimal")
|
||||
|
||||
self.preparevalue()
|
||||
return self.value
|
||||
|
||||
def preparevalue(self):
|
||||
if self.value is None:
|
||||
self.loadscalarvalue()
|
||||
|
||||
def loadscalarvalue(self):
|
||||
if self.valuetid not in [TID_NULL, TID_BOOLEAN, TID_POSINT, TID_NEGINT,
|
||||
TID_FLOAT, TID_DECIMAL, TID_TIMESTAMP,
|
||||
TID_SYMBOL, TID_STRING]:
|
||||
return
|
||||
|
||||
if self.valueisnull:
|
||||
self.value = None
|
||||
return
|
||||
|
||||
if self.valuetid == TID_STRING:
|
||||
self.value = self.read(self.valuelen).decode("UTF-8")
|
||||
|
||||
elif self.valuetid in (TID_POSINT, TID_NEGINT, TID_SYMBOL):
|
||||
if self.valuelen == 0:
|
||||
self.value = 0
|
||||
else:
|
||||
_assert(self.valuelen <= 4, "int too long: %d" % self.valuelen)
|
||||
v = 0
|
||||
for i in range(self.valuelen - 1, -1, -1):
|
||||
v = (v | (bord(self.read()) << (i * 8)))
|
||||
|
||||
if self.valuetid == TID_NEGINT:
|
||||
self.value = -v
|
||||
else:
|
||||
self.value = v
|
||||
|
||||
elif self.valuetid == TID_DECIMAL:
|
||||
self.value = self.readdecimal()
|
||||
|
||||
#else:
|
||||
# _assert(False, "Unhandled scalar type %d" % self.valuetid)
|
||||
|
||||
self.state = ParserState.AfterValue
|
||||
|
||||
def clearvalue(self):
|
||||
self.valuetid = -1
|
||||
self.value = None
|
||||
self.valueisnull = False
|
||||
self.valuefieldid = SID_UNKNOWN
|
||||
self.annotations = []
|
||||
|
||||
def loadannotations(self):
|
||||
ln = self.readvaruint()
|
||||
maxpos = self.stream.tell() + ln
|
||||
while self.stream.tell() < maxpos:
|
||||
self.annotations.append(self.readvaruint())
|
||||
self.valuetid = self.readtypeid()
|
||||
|
||||
def checkversionmarker(self):
|
||||
for i in VERSION_MARKER:
|
||||
_assert(self.read() == i, "Unknown version marker")
|
||||
|
||||
self.valuelen = 0
|
||||
self.valuetid = TID_SYMBOL
|
||||
self.value = SID_ION_1_0
|
||||
self.valueisnull = False
|
||||
self.valuefieldid = SID_UNKNOWN
|
||||
self.state = ParserState.AfterValue
|
||||
|
||||
def findcatalogitem(self, name):
|
||||
for result in self.catalog:
|
||||
if result.name == name:
|
||||
return result
|
||||
|
||||
def forceimport(self, symbols):
|
||||
item = IonCatalogItem("Forced", 1, symbols)
|
||||
self.symbols.import_(item, len(symbols))
|
||||
|
||||
def getfieldname(self):
|
||||
if self.valuefieldid == SID_UNKNOWN:
|
||||
return ""
|
||||
return self.symbols.findbyid(self.valuefieldid)
|
||||
|
||||
def getfieldnamesymbol(self):
|
||||
return SymbolToken(self.getfieldname(), self.valuefieldid)
|
||||
|
||||
def gettypename(self):
|
||||
if len(self.annotations) == 0:
|
||||
return ""
|
||||
|
||||
return self.symbols.findbyid(self.annotations[0])
|
||||
|
||||
@staticmethod
|
||||
def printlob(b):
|
||||
if b is None:
|
||||
return "null"
|
||||
|
||||
result = ""
|
||||
for i in b:
|
||||
result += ("%02x " % bord(i))
|
||||
|
||||
if len(result) > 0:
|
||||
result = result[:-1]
|
||||
return result
|
||||
|
||||
def ionwalk(self, supert, indent, lst):
|
||||
while self.hasnext():
|
||||
if supert == TID_STRUCT:
|
||||
L = self.getfieldname() + ":"
|
||||
else:
|
||||
L = ""
|
||||
|
||||
t = self.next()
|
||||
if t in [TID_STRUCT, TID_LIST]:
|
||||
if L != "":
|
||||
lst.append(indent + L)
|
||||
L = self.gettypename()
|
||||
if L != "":
|
||||
lst.append(indent + L + "::")
|
||||
if t == TID_STRUCT:
|
||||
lst.append(indent + "{")
|
||||
else:
|
||||
lst.append(indent + "[")
|
||||
|
||||
self.stepin()
|
||||
self.ionwalk(t, indent + " ", lst)
|
||||
self.stepout()
|
||||
|
||||
if t == TID_STRUCT:
|
||||
lst.append(indent + "}")
|
||||
else:
|
||||
lst.append(indent + "]")
|
||||
|
||||
else:
|
||||
if t == TID_STRING:
|
||||
L += ('"%s"' % self.stringvalue())
|
||||
elif t in [TID_CLOB, TID_BLOB]:
|
||||
L += ("{%s}" % self.printlob(self.lobvalue()))
|
||||
elif t == TID_POSINT:
|
||||
L += str(self.intvalue())
|
||||
elif t == TID_SYMBOL:
|
||||
tn = self.gettypename()
|
||||
if tn != "":
|
||||
tn += "::"
|
||||
L += tn + self.symbolvalue()
|
||||
elif t == TID_DECIMAL:
|
||||
L += str(self.decimalvalue())
|
||||
else:
|
||||
L += ("TID %d" % t)
|
||||
lst.append(indent + L)
|
||||
|
||||
def print_(self, lst):
|
||||
self.reset()
|
||||
self.ionwalk(-1, "", lst)
|
||||
|
||||
|
||||
SYM_NAMES = [ 'com.amazon.drm.Envelope@1.0',
|
||||
'com.amazon.drm.EnvelopeMetadata@1.0', 'size', 'page_size',
|
||||
'encryption_key', 'encryption_transformation',
|
||||
'encryption_voucher', 'signing_key', 'signing_algorithm',
|
||||
'signing_voucher', 'com.amazon.drm.EncryptedPage@1.0',
|
||||
'cipher_text', 'cipher_iv', 'com.amazon.drm.Signature@1.0',
|
||||
'data', 'com.amazon.drm.EnvelopeIndexTable@1.0', 'length',
|
||||
'offset', 'algorithm', 'encoded', 'encryption_algorithm',
|
||||
'hashing_algorithm', 'expires', 'format', 'id',
|
||||
'lock_parameters', 'strategy', 'com.amazon.drm.Key@1.0',
|
||||
'com.amazon.drm.KeySet@1.0', 'com.amazon.drm.PIDv3@1.0',
|
||||
'com.amazon.drm.PlainTextPage@1.0',
|
||||
'com.amazon.drm.PlainText@1.0', 'com.amazon.drm.PrivateKey@1.0',
|
||||
'com.amazon.drm.PublicKey@1.0', 'com.amazon.drm.SecretKey@1.0',
|
||||
'com.amazon.drm.Voucher@1.0', 'public_key', 'private_key',
|
||||
'com.amazon.drm.KeyPair@1.0', 'com.amazon.drm.ProtectedData@1.0',
|
||||
'doctype', 'com.amazon.drm.EnvelopeIndexTableOffset@1.0',
|
||||
'enddoc', 'license_type', 'license', 'watermark', 'key', 'value',
|
||||
'com.amazon.drm.License@1.0', 'category', 'metadata',
|
||||
'categorized_metadata', 'com.amazon.drm.CategorizedMetadata@1.0',
|
||||
'com.amazon.drm.VoucherEnvelope@1.0', 'mac', 'voucher',
|
||||
'com.amazon.drm.ProtectedData@2.0',
|
||||
'com.amazon.drm.Envelope@2.0',
|
||||
'com.amazon.drm.EnvelopeMetadata@2.0',
|
||||
'com.amazon.drm.EncryptedPage@2.0',
|
||||
'com.amazon.drm.PlainText@2.0', 'compression_algorithm',
|
||||
'com.amazon.drm.Compressed@1.0', 'priority', 'refines']
|
||||
|
||||
def addprottable(ion):
|
||||
ion.addtocatalog("ProtectedData", 1, SYM_NAMES)
|
||||
|
||||
|
||||
def pkcs7pad(msg, blocklen):
|
||||
paddinglen = blocklen - len(msg) % blocklen
|
||||
padding = bchr(paddinglen) * paddinglen
|
||||
return msg + padding
|
||||
|
||||
|
||||
def pkcs7unpad(msg, blocklen):
|
||||
_assert(len(msg) % blocklen == 0)
|
||||
|
||||
paddinglen = bord(msg[-1])
|
||||
_assert(paddinglen > 0 and paddinglen <= blocklen, "Incorrect padding - Wrong key")
|
||||
_assert(msg[-paddinglen:] == bchr(paddinglen) * paddinglen, "Incorrect padding - Wrong key")
|
||||
|
||||
return msg[:-paddinglen]
|
||||
|
||||
|
||||
class DrmIonVoucher(object):
|
||||
envelope = None
|
||||
voucher = None
|
||||
drmkey = None
|
||||
license_type = "Unknown"
|
||||
|
||||
encalgorithm = ""
|
||||
enctransformation = ""
|
||||
hashalgorithm = ""
|
||||
|
||||
lockparams = None
|
||||
|
||||
ciphertext = b""
|
||||
cipheriv = b""
|
||||
secretkey = b""
|
||||
|
||||
def __init__(self, voucherenv, dsn, secret):
|
||||
self.dsn,self.secret = dsn,secret
|
||||
|
||||
self.lockparams = []
|
||||
|
||||
self.envelope = BinaryIonParser(voucherenv)
|
||||
addprottable(self.envelope)
|
||||
|
||||
def decryptvoucher(self):
|
||||
shared = "PIDv3" + self.encalgorithm + self.enctransformation + self.hashalgorithm
|
||||
|
||||
self.lockparams.sort()
|
||||
for param in self.lockparams:
|
||||
if param == "ACCOUNT_SECRET":
|
||||
shared += param + self.secret
|
||||
elif param == "CLIENT_ID":
|
||||
shared += param + self.dsn
|
||||
else:
|
||||
_assert(False, "Unknown lock parameter: %s" % param)
|
||||
|
||||
sharedsecret = shared.encode("UTF-8")
|
||||
|
||||
key = hmac.new(sharedsecret, sharedsecret[:5], digestmod=hashlib.sha256).digest()
|
||||
aes = AES.new(key[:32], AES.MODE_CBC, self.cipheriv[:16])
|
||||
b = aes.decrypt(self.ciphertext)
|
||||
b = pkcs7unpad(b, 16)
|
||||
|
||||
self.drmkey = BinaryIonParser(StringIO(b))
|
||||
addprottable(self.drmkey)
|
||||
|
||||
_assert(self.drmkey.hasnext() and self.drmkey.next() == TID_LIST and self.drmkey.gettypename() == "com.amazon.drm.KeySet@1.0",
|
||||
"Expected KeySet, got %s" % self.drmkey.gettypename())
|
||||
|
||||
self.drmkey.stepin()
|
||||
while self.drmkey.hasnext():
|
||||
self.drmkey.next()
|
||||
if self.drmkey.gettypename() != "com.amazon.drm.SecretKey@1.0":
|
||||
continue
|
||||
|
||||
self.drmkey.stepin()
|
||||
while self.drmkey.hasnext():
|
||||
self.drmkey.next()
|
||||
if self.drmkey.getfieldname() == "algorithm":
|
||||
_assert(self.drmkey.stringvalue() == "AES", "Unknown cipher algorithm: %s" % self.drmkey.stringvalue())
|
||||
elif self.drmkey.getfieldname() == "format":
|
||||
_assert(self.drmkey.stringvalue() == "RAW", "Unknown key format: %s" % self.drmkey.stringvalue())
|
||||
elif self.drmkey.getfieldname() == "encoded":
|
||||
self.secretkey = self.drmkey.lobvalue()
|
||||
|
||||
self.drmkey.stepout()
|
||||
break
|
||||
|
||||
self.drmkey.stepout()
|
||||
|
||||
def parse(self):
|
||||
self.envelope.reset()
|
||||
_assert(self.envelope.hasnext(), "Envelope is empty")
|
||||
_assert(self.envelope.next() == TID_STRUCT and self.envelope.gettypename() == "com.amazon.drm.VoucherEnvelope@1.0",
|
||||
"Unknown type encountered in envelope, expected VoucherEnvelope")
|
||||
|
||||
self.envelope.stepin()
|
||||
while self.envelope.hasnext():
|
||||
self.envelope.next()
|
||||
field = self.envelope.getfieldname()
|
||||
if field == "voucher":
|
||||
self.voucher = BinaryIonParser(StringIO(self.envelope.lobvalue()))
|
||||
addprottable(self.voucher)
|
||||
continue
|
||||
elif field != "strategy":
|
||||
continue
|
||||
|
||||
_assert(self.envelope.gettypename() == "com.amazon.drm.PIDv3@1.0", "Unknown strategy: %s" % self.envelope.gettypename())
|
||||
|
||||
self.envelope.stepin()
|
||||
while self.envelope.hasnext():
|
||||
self.envelope.next()
|
||||
field = self.envelope.getfieldname()
|
||||
if field == "encryption_algorithm":
|
||||
self.encalgorithm = self.envelope.stringvalue()
|
||||
elif field == "encryption_transformation":
|
||||
self.enctransformation = self.envelope.stringvalue()
|
||||
elif field == "hashing_algorithm":
|
||||
self.hashalgorithm = self.envelope.stringvalue()
|
||||
elif field == "lock_parameters":
|
||||
self.envelope.stepin()
|
||||
while self.envelope.hasnext():
|
||||
_assert(self.envelope.next() == TID_STRING, "Expected string list for lock_parameters")
|
||||
self.lockparams.append(self.envelope.stringvalue())
|
||||
self.envelope.stepout()
|
||||
|
||||
self.envelope.stepout()
|
||||
|
||||
self.parsevoucher()
|
||||
|
||||
def parsevoucher(self):
|
||||
_assert(self.voucher.hasnext(), "Voucher is empty")
|
||||
_assert(self.voucher.next() == TID_STRUCT and self.voucher.gettypename() == "com.amazon.drm.Voucher@1.0",
|
||||
"Unknown type, expected Voucher")
|
||||
|
||||
self.voucher.stepin()
|
||||
while self.voucher.hasnext():
|
||||
self.voucher.next()
|
||||
|
||||
if self.voucher.getfieldname() == "cipher_iv":
|
||||
self.cipheriv = self.voucher.lobvalue()
|
||||
elif self.voucher.getfieldname() == "cipher_text":
|
||||
self.ciphertext = self.voucher.lobvalue()
|
||||
elif self.voucher.getfieldname() == "license":
|
||||
_assert(self.voucher.gettypename() == "com.amazon.drm.License@1.0",
|
||||
"Unknown license: %s" % self.voucher.gettypename())
|
||||
self.voucher.stepin()
|
||||
while self.voucher.hasnext():
|
||||
self.voucher.next()
|
||||
if self.voucher.getfieldname() == "license_type":
|
||||
self.license_type = self.voucher.stringvalue()
|
||||
self.voucher.stepout()
|
||||
|
||||
def printenvelope(self, lst):
|
||||
self.envelope.print_(lst)
|
||||
|
||||
def printkey(self, lst):
|
||||
if self.voucher is None:
|
||||
self.parse()
|
||||
if self.drmkey is None:
|
||||
self.decryptvoucher()
|
||||
|
||||
self.drmkey.print_(lst)
|
||||
|
||||
def printvoucher(self, lst):
|
||||
if self.voucher is None:
|
||||
self.parse()
|
||||
|
||||
self.voucher.print_(lst)
|
||||
|
||||
def getlicensetype(self):
|
||||
return self.license_type
|
||||
|
||||
|
||||
class DrmIon(object):
|
||||
ion = None
|
||||
voucher = None
|
||||
vouchername = ""
|
||||
key = b""
|
||||
onvoucherrequired = None
|
||||
|
||||
def __init__(self, ionstream, onvoucherrequired):
|
||||
self.ion = BinaryIonParser(ionstream)
|
||||
addprottable(self.ion)
|
||||
self.onvoucherrequired = onvoucherrequired
|
||||
|
||||
def parse(self, outpages):
|
||||
self.ion.reset()
|
||||
|
||||
_assert(self.ion.hasnext(), "DRMION envelope is empty")
|
||||
_assert(self.ion.next() == TID_SYMBOL and self.ion.gettypename() == "doctype", "Expected doctype symbol")
|
||||
_assert(self.ion.next() == TID_LIST and self.ion.gettypename() in ["com.amazon.drm.Envelope@1.0", "com.amazon.drm.Envelope@2.0"],
|
||||
"Unknown type encountered in DRMION envelope, expected Envelope, got %s" % self.ion.gettypename())
|
||||
|
||||
while True:
|
||||
if self.ion.gettypename() == "enddoc":
|
||||
break
|
||||
|
||||
self.ion.stepin()
|
||||
while self.ion.hasnext():
|
||||
self.ion.next()
|
||||
|
||||
if self.ion.gettypename() in ["com.amazon.drm.EnvelopeMetadata@1.0", "com.amazon.drm.EnvelopeMetadata@2.0"]:
|
||||
self.ion.stepin()
|
||||
while self.ion.hasnext():
|
||||
self.ion.next()
|
||||
if self.ion.getfieldname() != "encryption_voucher":
|
||||
continue
|
||||
|
||||
if self.vouchername == "":
|
||||
self.vouchername = self.ion.stringvalue()
|
||||
self.voucher = self.onvoucherrequired(self.vouchername)
|
||||
self.key = self.voucher.secretkey
|
||||
_assert(self.key is not None, "Unable to obtain secret key from voucher")
|
||||
else:
|
||||
_assert(self.vouchername == self.ion.stringvalue(),
|
||||
"Unexpected: Different vouchers required for same file?")
|
||||
|
||||
self.ion.stepout()
|
||||
|
||||
elif self.ion.gettypename() in ["com.amazon.drm.EncryptedPage@1.0", "com.amazon.drm.EncryptedPage@2.0"]:
|
||||
decompress = False
|
||||
ct = None
|
||||
civ = None
|
||||
self.ion.stepin()
|
||||
while self.ion.hasnext():
|
||||
self.ion.next()
|
||||
if self.ion.gettypename() == "com.amazon.drm.Compressed@1.0":
|
||||
decompress = True
|
||||
if self.ion.getfieldname() == "cipher_text":
|
||||
ct = self.ion.lobvalue()
|
||||
elif self.ion.getfieldname() == "cipher_iv":
|
||||
civ = self.ion.lobvalue()
|
||||
|
||||
if ct is not None and civ is not None:
|
||||
self.processpage(ct, civ, outpages, decompress)
|
||||
self.ion.stepout()
|
||||
|
||||
self.ion.stepout()
|
||||
if not self.ion.hasnext():
|
||||
break
|
||||
self.ion.next()
|
||||
|
||||
def print_(self, lst):
|
||||
self.ion.print_(lst)
|
||||
|
||||
def processpage(self, ct, civ, outpages, decompress):
|
||||
aes = AES.new(self.key[:16], AES.MODE_CBC, civ[:16])
|
||||
msg = pkcs7unpad(aes.decrypt(ct), 16)
|
||||
|
||||
if not decompress:
|
||||
outpages.write(msg)
|
||||
return
|
||||
|
||||
_assert(msg[0] == b"\x00", "LZMA UseFilter not supported")
|
||||
|
||||
if calibre_lzma is not None:
|
||||
with calibre_lzma.decompress(msg[1:], bufsize=0x1000000) as f:
|
||||
f.seek(0)
|
||||
outpages.write(f.read())
|
||||
return
|
||||
|
||||
decomp = lzma.LZMADecompressor(format=lzma.FORMAT_ALONE)
|
||||
while not decomp.eof:
|
||||
segment = decomp.decompress(msg[1:])
|
||||
msg = b"" # Contents were internally buffered after the first call
|
||||
outpages.write(segment)
|
||||
@@ -3,10 +3,13 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# k4mobidedrm.py, version 5.3
|
||||
# Copyright © 2009-2015 by ApprenticeHarper et al.
|
||||
# k4mobidedrm.py
|
||||
# Copyright © 2008-2017 by Apprentice Harper et al.
|
||||
|
||||
# engine to remove drm from Kindle and Mobipocket ebooks
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '5.5'
|
||||
|
||||
# Engine to remove drm from Kindle and Mobipocket ebooks
|
||||
# for personal use for archiving and converting your ebooks
|
||||
|
||||
# PLEASE DO NOT PIRATE EBOOKS!
|
||||
@@ -17,12 +20,11 @@ from __future__ import with_statement
|
||||
# readable for a long, long time
|
||||
|
||||
# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
|
||||
# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates
|
||||
# and many many others
|
||||
# unswindle, DarkReverser, ApprenticeAlf, and many many others
|
||||
|
||||
# Special thanks to The Dark Reverser for MobiDeDrm and CMBDTC for cmbdtc_dump
|
||||
# from which this script borrows most unashamedly.
|
||||
|
||||
|
||||
# Changelog
|
||||
# 1.0 - Name change to k4mobidedrm. Adds Mac support, Adds plugin code
|
||||
# 1.1 - Adds support for additional kindle.info files
|
||||
@@ -56,9 +58,9 @@ from __future__ import with_statement
|
||||
# 5.1 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 5.2 - Fixed error in command line processing of unicode arguments
|
||||
# 5.3 - Changed Android support to allow passing of backup .ab files
|
||||
|
||||
__version__ = '5.3'
|
||||
|
||||
# 5.4 - Recognise KFX files masquerading as azw, even if we can't decrypt them yet.
|
||||
# 5.5 - Added GPL v3 licence explicitly.
|
||||
# 5.x - Invoke KFXZipBook to handle zipped KFX files
|
||||
|
||||
import sys, os, re
|
||||
import csv
|
||||
@@ -82,11 +84,13 @@ if inCalibre:
|
||||
from calibre_plugins.dedrm import topazextract
|
||||
from calibre_plugins.dedrm import kgenpids
|
||||
from calibre_plugins.dedrm import androidkindlekey
|
||||
from calibre_plugins.dedrm import kfxdedrm
|
||||
else:
|
||||
import mobidedrm
|
||||
import topazextract
|
||||
import kgenpids
|
||||
import androidkindlekey
|
||||
import kfxdedrm
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
@@ -194,11 +198,17 @@ def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime
|
||||
raise DrmException(u"Input file does not exist.")
|
||||
|
||||
mobi = True
|
||||
magic3 = open(infile,'rb').read(3)
|
||||
magic8 = open(infile,'rb').read(8)
|
||||
if magic8 == '\xeaDRMION\xee':
|
||||
raise DrmException(u"The .kfx DRMION file cannot be decrypted by itself. A .kfx-zip archive containing a DRM voucher is required.")
|
||||
|
||||
magic3 = magic8[:3]
|
||||
if magic3 == 'TPZ':
|
||||
mobi = False
|
||||
|
||||
if mobi:
|
||||
if magic8[:4] == 'PK\x03\x04':
|
||||
mb = kfxdedrm.KFXZipBook(infile)
|
||||
elif mobi:
|
||||
mb = mobidedrm.MobiBook(infile)
|
||||
else:
|
||||
mb = topazextract.TopazBook(infile)
|
||||
@@ -215,8 +225,9 @@ def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime
|
||||
md1, md2 = mb.getPIDMetaInfo()
|
||||
totalpids.extend(kgenpids.getPidList(md1, md2, serials, kDatabases))
|
||||
# remove any duplicates
|
||||
totalpid = list(set(totalpids))
|
||||
totalpids = list(set(totalpids))
|
||||
print u"Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(totalpids))
|
||||
#print totalpids
|
||||
|
||||
try:
|
||||
mb.processBook(totalpids)
|
||||
@@ -289,7 +300,7 @@ def usage(progname):
|
||||
def cli_main():
|
||||
argv=unicode_argv()
|
||||
progname = os.path.basename(argv[0])
|
||||
print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2013 The Dark Reverser et al.".format(__version__)
|
||||
print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2017 Apprentice Harper et al.".format(__version__)
|
||||
|
||||
try:
|
||||
opts, args = getopt.getopt(argv[1:], "k:p:s:a:")
|
||||
|
||||
108
DeDRM_calibre_plugin/DeDRM_plugin/kfxdedrm.py
Normal file
108
DeDRM_calibre_plugin/DeDRM_plugin/kfxdedrm.py
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# Engine to remove drm from Kindle KFX ebooks
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import zipfile
|
||||
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except ImportError:
|
||||
from StringIO import StringIO
|
||||
|
||||
try:
|
||||
import ion
|
||||
except:
|
||||
from calibre_plugins.dedrm import ion
|
||||
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '1.0'
|
||||
|
||||
|
||||
class KFXZipBook:
|
||||
def __init__(self, infile):
|
||||
self.infile = infile
|
||||
self.voucher = None
|
||||
self.decrypted = {}
|
||||
|
||||
def getPIDMetaInfo(self):
|
||||
return (None, None)
|
||||
|
||||
def processBook(self, totalpids):
|
||||
with zipfile.ZipFile(self.infile, 'r') as zf:
|
||||
for filename in zf.namelist():
|
||||
data = zf.read(filename)
|
||||
if data.startswith('\xeaDRMION\xee'):
|
||||
if self.voucher is None:
|
||||
self.decrypt_voucher(totalpids)
|
||||
print u'Decrypting KFX DRMION: {0}'.format(filename)
|
||||
outfile = StringIO()
|
||||
ion.DrmIon(StringIO(data[8:-8]), lambda name: self.voucher).parse(outfile)
|
||||
self.decrypted[filename] = outfile.getvalue()
|
||||
|
||||
if not self.decrypted:
|
||||
print(u'The .kfx-zip archive does not contain an encrypted DRMION file')
|
||||
|
||||
def decrypt_voucher(self, totalpids):
|
||||
with zipfile.ZipFile(self.infile, 'r') as zf:
|
||||
for info in zf.infolist():
|
||||
if info.file_size < 0x10000:
|
||||
data = zf.read(info.filename)
|
||||
if data.startswith('\xe0\x01\x00\xea') and 'ProtectedData' in data:
|
||||
break # found DRM voucher
|
||||
else:
|
||||
raise Exception(u'The .kfx-zip archive contains an encrypted DRMION file without a DRM voucher')
|
||||
|
||||
print u'Decrypting KFX DRM voucher: {0}'.format(info.filename)
|
||||
|
||||
for pid in [''] + totalpids:
|
||||
for dsn_len,secret_len in [(0,0), (16,0), (16,40), (32,40), (40,40)]:
|
||||
if len(pid) == dsn_len + secret_len:
|
||||
break # split pid into DSN and account secret
|
||||
else:
|
||||
continue
|
||||
|
||||
try:
|
||||
voucher = ion.DrmIonVoucher(StringIO(data), pid[:dsn_len], pid[dsn_len:])
|
||||
voucher.parse()
|
||||
voucher.decryptvoucher()
|
||||
break
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
raise Exception(u'Failed to decrypt KFX DRM voucher with any key')
|
||||
|
||||
print u'KFX DRM voucher successfully decrypted'
|
||||
|
||||
license_type = voucher.getlicensetype()
|
||||
if license_type != "Purchase":
|
||||
raise Exception((u'This book is licensed as {0}. '
|
||||
'These tools are intended for use on purchased books.').format(license_type))
|
||||
|
||||
self.voucher = voucher
|
||||
|
||||
def getBookTitle(self):
|
||||
return os.path.splitext(os.path.split(self.infile)[1])[0]
|
||||
|
||||
def getBookExtension(self):
|
||||
return '.kfx-zip'
|
||||
|
||||
def getBookType(self):
|
||||
return 'KFX-ZIP'
|
||||
|
||||
def cleanup(self):
|
||||
pass
|
||||
|
||||
def getFile(self, outpath):
|
||||
if not self.decrypted:
|
||||
shutil.copyfile(self.infile, outpath)
|
||||
else:
|
||||
with zipfile.ZipFile(self.infile, 'r') as zif:
|
||||
with zipfile.ZipFile(outpath, 'w') as zof:
|
||||
for info in zif.infolist():
|
||||
zof.writestr(info, self.decrypted.get(info.filename, zif.read(info.filename)))
|
||||
@@ -4,10 +4,15 @@
|
||||
from __future__ import with_statement
|
||||
|
||||
# kgenpids.py
|
||||
# Copyright © 2010-2015 by some_updates, Apprentice Alf and Apprentice Harper
|
||||
# Copyright © 2008-2017 Apprentice Harper et al.
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '2.1'
|
||||
|
||||
# Revision history:
|
||||
# 2.0 - Fix for non-ascii Windows user names
|
||||
# 2.1 - Actual fix for non-ascii WIndows user names.
|
||||
# x.x - Return information needed for KFX decryption
|
||||
|
||||
import sys
|
||||
import os, csv
|
||||
@@ -168,6 +173,9 @@ def pidFromSerial(s, l):
|
||||
|
||||
# Parse the EXTH header records and use the Kindle serial number to calculate the book pid.
|
||||
def getKindlePids(rec209, token, serialnum):
|
||||
if rec209 is None:
|
||||
return [serialnum]
|
||||
|
||||
pids=[]
|
||||
|
||||
if isinstance(serialnum,unicode):
|
||||
@@ -196,31 +204,59 @@ def getK4Pids(rec209, token, kindleDatabase):
|
||||
pids = []
|
||||
|
||||
try:
|
||||
# Get the Mazama Random number
|
||||
MazamaRandomNumber = (kindleDatabase[1])['MazamaRandomNumber'].decode('hex')
|
||||
|
||||
# Get the kindle account token
|
||||
# Get the kindle account token, if present
|
||||
kindleAccountToken = (kindleDatabase[1])['kindle.account.tokens'].decode('hex')
|
||||
|
||||
# Get the IDString used to decode the Kindle Info file
|
||||
IDString = (kindleDatabase[1])['IDString'].decode('hex')
|
||||
|
||||
# Get the UserName stored when the Kindle Info file was decoded
|
||||
UserName = (kindleDatabase[1])['UserName'].decode('hex')
|
||||
|
||||
except KeyError:
|
||||
print u"Keys not found in the database {0}.".format(kindleDatabase[0])
|
||||
kindleAccountToken=""
|
||||
pass
|
||||
|
||||
try:
|
||||
# Get the DSN token, if present
|
||||
DSN = (kindleDatabase[1])['DSN'].decode('hex')
|
||||
print u"Got DSN key from database {0}".format(kindleDatabase[0])
|
||||
except KeyError:
|
||||
# See if we have the info to generate the DSN
|
||||
try:
|
||||
# Get the Mazama Random number
|
||||
MazamaRandomNumber = (kindleDatabase[1])['MazamaRandomNumber'].decode('hex')
|
||||
#print u"Got MazamaRandomNumber from database {0}".format(kindleDatabase[0])
|
||||
|
||||
try:
|
||||
# Get the SerialNumber token, if present
|
||||
IDString = (kindleDatabase[1])['SerialNumber'].decode('hex')
|
||||
print u"Got SerialNumber from database {0}".format(kindleDatabase[0])
|
||||
except KeyError:
|
||||
# Get the IDString we added
|
||||
IDString = (kindleDatabase[1])['IDString'].decode('hex')
|
||||
|
||||
try:
|
||||
# Get the UsernameHash token, if present
|
||||
encodedUsername = (kindleDatabase[1])['UsernameHash'].decode('hex')
|
||||
print u"Got UsernameHash from database {0}".format(kindleDatabase[0])
|
||||
except KeyError:
|
||||
# Get the UserName we added
|
||||
UserName = (kindleDatabase[1])['UserName'].decode('hex')
|
||||
# encode it
|
||||
encodedUsername = encodeHash(UserName,charMap1)
|
||||
#print u"encodedUsername",encodedUsername.encode('hex')
|
||||
except KeyError:
|
||||
print u"Keys not found in the database {0}.".format(kindleDatabase[0])
|
||||
return pids
|
||||
|
||||
# Get the ID string used
|
||||
encodedIDString = encodeHash(IDString,charMap1)
|
||||
#print u"encodedIDString",encodedIDString.encode('hex')
|
||||
|
||||
# concat, hash and encode to calculate the DSN
|
||||
DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1)
|
||||
#print u"DSN",DSN.encode('hex')
|
||||
pass
|
||||
|
||||
if rec209 is None:
|
||||
pids.append(DSN+kindleAccountToken)
|
||||
return pids
|
||||
|
||||
# Get the ID string used
|
||||
encodedIDString = encodeHash(IDString,charMap1)
|
||||
|
||||
# Get the current user name
|
||||
encodedUsername = encodeHash(UserName,charMap1)
|
||||
|
||||
# concat, hash and encode to calculate the DSN
|
||||
DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1)
|
||||
|
||||
# Compute the device PID (for which I can tell, is used for nothing).
|
||||
table = generatePidEncryptionTable()
|
||||
devicePID = generateDevicePID(table,DSN,4)
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
from __future__ import with_statement
|
||||
|
||||
# kindlekey.py
|
||||
# Copyright © 2010-2015 by some_updates, Apprentice Alf and Apprentice Harper
|
||||
# Copyright © 2008-2017 Apprentice Harper et al.
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '2.5'
|
||||
|
||||
# Revision history:
|
||||
# 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc.
|
||||
@@ -19,15 +22,18 @@ from __future__ import with_statement
|
||||
# 1.8 - Fixes for Kindle for Mac, and non-ascii in Windows user names
|
||||
# 1.9 - Fixes for Unicode in Windows user names
|
||||
# 2.0 - Added comments and extra fix for non-ascii Windows user names
|
||||
# 2.1 - Fixed Kindle for PC encryption changes March 2016
|
||||
# 2.2 - Fixes for Macs with bonded ethernet ports
|
||||
# Also removed old .kinfo file support (pre-2011)
|
||||
# 2.3 - Added more field names thanks to concavegit's KFX code.
|
||||
# 2.4 - Fix for complex Mac disk setups, thanks to Tibs
|
||||
# 2.5 - Final Fix for Windows user names with non-ascii characters, thanks to oneofusoneofus
|
||||
|
||||
|
||||
"""
|
||||
Retrieve Kindle for PC/Mac user key.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '1.9'
|
||||
|
||||
import sys, os, re
|
||||
from struct import pack, unpack, unpack_from
|
||||
import json
|
||||
@@ -882,10 +888,18 @@ if iswindows:
|
||||
if errcd == 234:
|
||||
# bad wine implementation up through wine 1.3.21
|
||||
return "AlternateUserName"
|
||||
# double the buffer size
|
||||
buffer = create_unicode_buffer(len(buffer) * 2)
|
||||
size.value = len(buffer)
|
||||
# return low byte of the unicode value of each character of the username
|
||||
return buffer.value.encode('utf-16-le')[::2]
|
||||
|
||||
# replace any non-ASCII values with 0xfffd
|
||||
for i in xrange(0,len(buffer)):
|
||||
if buffer[i]>u"\u007f":
|
||||
#print u"swapping char "+str(i)+" ("+buffer[i]+")"
|
||||
buffer[i] = u"\ufffd"
|
||||
# return utf-8 encoding of modified username
|
||||
#print u"modified username:"+buffer.value
|
||||
return buffer.value.encode('utf-8')
|
||||
return GetUserName
|
||||
GetUserName = GetUserName()
|
||||
|
||||
@@ -926,7 +940,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 +1007,128 @@ 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',\
|
||||
'SerialNumber',\
|
||||
'UsernameHash',\
|
||||
'kindle.directedid.info',\
|
||||
'DSN',\
|
||||
'kindle.accounttype.info',\
|
||||
'krx.flashcardsplugin.data.encryption_key',\
|
||||
'krx.notebookexportplugin.data.encryption_key',\
|
||||
'proxy.http.password',\
|
||||
'proxy.http.username'
|
||||
]
|
||||
DB = {}
|
||||
with open(kInfoFile, 'rb') as infoReader:
|
||||
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:
|
||||
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 +1260,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 +1271,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 ')
|
||||
(devpart, mpath) = resline.split(' on ')[:2]
|
||||
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 +1297,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 +1314,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 +1344,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 +1360,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 +1384,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 +1452,122 @@ 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',\
|
||||
'SerialNumber',\
|
||||
'UsernameHash',\
|
||||
'kindle.directedid.info',\
|
||||
'DSN'
|
||||
]
|
||||
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 +1631,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 +1661,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):
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# mobidedrm.py, version 0.38
|
||||
# mobidedrm.py
|
||||
# Copyright © 2008 The Dark Reverser
|
||||
#
|
||||
# Modified 2008–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
# Portions © 2008–2017 Apprentice Harper et al.
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = u"0.42"
|
||||
|
||||
# This is a python script. You need a Python interpreter to run it.
|
||||
# For example, ActiveState Python, which exists for windows.
|
||||
@@ -69,9 +71,7 @@
|
||||
# 0.39 - Fixed problem with TEXtREAd and getBookType interface
|
||||
# 0.40 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 0.41 - Fixed potential unicode problem in command line calls
|
||||
|
||||
|
||||
__version__ = u"0.41"
|
||||
# 0.42 - Added GPL v3 licence. updated/removed some print statements
|
||||
|
||||
import sys
|
||||
import os
|
||||
@@ -244,7 +244,7 @@ class MobiBook:
|
||||
pass
|
||||
|
||||
def __init__(self, infile):
|
||||
print u"MobiDeDrm v{0:s}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__)
|
||||
print u"MobiDeDrm v{0:s}.\nCopyright © 2008-2017 The Dark Reverser, Apprentice Harper et al.".format(__version__)
|
||||
|
||||
try:
|
||||
from alfcrypto import Pukall_Cipher
|
||||
@@ -288,10 +288,10 @@ class MobiBook:
|
||||
self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18])
|
||||
self.mobi_codepage, = struct.unpack('>L',self.sect[0x1c:0x20])
|
||||
self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C])
|
||||
print u"MOBI header version {0:d}, header length {1:d}".format(self.mobi_version, self.mobi_length)
|
||||
#print u"MOBI header version {0:d}, header length {1:d}".format(self.mobi_version, self.mobi_length)
|
||||
if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
|
||||
self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4])
|
||||
print u"Extra Data Flags: {0:d}".format(self.extra_data_flags)
|
||||
#print u"Extra Data Flags: {0:d}".format(self.extra_data_flags)
|
||||
if (self.compression != 17480):
|
||||
# multibyte utf8 data is included in the encryption for PalmDoc compression
|
||||
# so clear that byte so that we leave it to be decrypted.
|
||||
@@ -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'
|
||||
@@ -514,7 +516,7 @@ def cli_main():
|
||||
argv=unicode_argv()
|
||||
progname = os.path.basename(argv[0])
|
||||
if len(argv)<3 or len(argv)>4:
|
||||
print u"MobiDeDrm v{0}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__)
|
||||
print u"MobiDeDrm v{0:s}.\nCopyright © 2008-2017 The Dark Reverser, Apprentice Harper et al.".format(__version__)
|
||||
print u"Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks"
|
||||
print u"Usage:"
|
||||
print u" {0} <infile> <outfile> [<Comma separated list of PIDs to try>]".format(progname)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -352,7 +352,7 @@ class TopazBook:
|
||||
break
|
||||
|
||||
if not bookKey:
|
||||
raise DrmException(u"No key found in {0:d} keys tried. Read the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(len(pidlst)))
|
||||
raise DrmException(u"No key found in {0:d} keys tried. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(len(pidlst)))
|
||||
|
||||
self.setBookKey(bookKey)
|
||||
self.createBookDirectory()
|
||||
|
||||
@@ -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,47 @@
|
||||
DeDRM_plugin.zip
|
||||
DeDRM_plugin.zip
|
||||
================
|
||||
|
||||
This calibre plugin replaces all previous DRM removal plugins. Before you install this plugin, you should uninstall any older individual DRM removal plugins, e.g. K4MobiDeDRM.
|
||||
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 (nook) ePubs, Adobe Digital Edition PDFs, and Fictionwise eReader ebooks.
|
||||
This plugin will remove the DRM from
|
||||
- Kindle ebooks (files 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 Kindle for PC/Mac 1.19 and later, Amazon included support for their new KFX format. While the tools now include a first attempt at supporting drm removal for KFX format, we recommend using Kindle for PC/Mac 1.17 or earlier which prevents downloads of the new format, as conversions from the olde KF8 format are likely to be more successful.
|
||||
|
||||
** Some later Kindles support Amazon's new KFX format. And some books download in a split azw3/azw6 format. For best results, instead of using files downloaded directly to your Kindle, download from Amazon's web site 'for transfer via USB'. This will give you an single file to import. See also the FAQ entry about this.
|
||||
|
||||
*** 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 October 2017.
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
From the Preferences menu, 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, Adobe Digital Editions and NOOK Study are automatically generated and saved when needed. If all your DRMed ebooks can be downloaded and read in Kindle for Mac/PC, Adobe Digital Editions or NOOK Study on the same computer and user account on which you are running calibre, you do not need to do add any customisation data to this plugin. (Linux users should see the Linux section at the end of this ReadMe.)
|
||||
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,25 +50,14 @@ 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 - that is, they won't convert or open in the calibre ebook viewer), you should make a log of 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:
|
||||
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 Windows, open a terminal/command window. (Start/Run… and then type 'cmd.exe' (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.
|
||||
- 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.
|
||||
|
||||
You should now have a text-based command-line window open.
|
||||
|
||||
Type in "calibre-debug -g" (without the "s but with the space before the -g) 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.)
|
||||
|
||||
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/ or an issue at Apprentice Harper's repository, https://github.com/apprenticeharper/DeDRM_tools/issues . You should also give details of your computer, and how you obtained the ebook file.
|
||||
|
||||
|
||||
Credits
|
||||
@@ -54,6 +67,7 @@ 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 original KFX format decryption was by lulzkabulz, converted to python by Apprentice Naomi and integrated into the tools by tomthumb1997
|
||||
|
||||
The alfcrypto library is by some_updates
|
||||
The ePub encryption detection script is by Apprentice Alf, adapted from a script by Paul Durrant
|
||||
@@ -69,23 +83,24 @@ Linux Systems Only
|
||||
Instructions for installing Wine, Kindle for PC, Adobe Digital Editions, Python and PyCrypto
|
||||
--------------------------------------------------------------------------------------------
|
||||
|
||||
These instructions have been tested with Wine 1.4 on Ubuntu.
|
||||
These instructions have been tested with Wine 1.4 on Ubuntu but are now very out of date.
|
||||
|
||||
1. First download the software you're going to to have to install.
|
||||
a. Kindle for PC from http://www.amazon.co.uk/gp/kindle/pc/
|
||||
b. Adobe Digital Editions 1.7.x from http://helpx.adobe.com/digital-editions/kb/cant-install-digital-editions.html
|
||||
a. Adobe Digital Editions 1.7.x from http://helpx.adobe.com/digital-editions/kb/cant-install-digital-editions.html
|
||||
(Adobe Digital Editions 2.x doesn't work with Wine.)
|
||||
c. ActivePython 2.7.X for Windows (x86) from http://www.activestate.com/activepython/downloads
|
||||
d. PyCrypto 2.1 for 32bit Windows and Python 2.7 from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
|
||||
b. Python 2.7.X for Windows (x86) from https://www.python.org/ftp/python/2.7.13/python-2.7.13.msi
|
||||
c. PyCrypto 2.1 for 32bit Windows and Python 2.7 from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
|
||||
(PyCrypto downloads as a zip file. You will need to unzip it.)
|
||||
2. Install Wine for 32-bit x86. (e.g. on Ubuntu, Open the Ubuntu Software Center, search for Wine, and install "Wine Windows Program Loader".)
|
||||
2a. [update] Kindle for PC now requires Windows 7, so in the following setups, choose any option for Windows 7, not Windows XP.
|
||||
3. Run "Configure Wine", which will set up the default 'wineprefix'
|
||||
4. Run winetricks, select the default wineprefix and install component vcrun2008
|
||||
5. Run the mis-named "Uninstall Wine Software", which also allows installation of software.
|
||||
6. Install Kindle for PC. Accept all defaults and register with your Amazon Account.
|
||||
7. Install Adobe Digital Editions. Accept all defaults and register with your Adobe ID.
|
||||
8. Install ActiveState Python 2.7.x. Accept all defaults.
|
||||
9. Install PyCrypto 2.1. Accept all defaults.
|
||||
4. Navigate to "Install an application" and install Kindle. Alternatively, run `winetricks kindle`
|
||||
5. Install Adobe Digital Editions. Accept all defaults and register with your Adobe ID.
|
||||
6. Install Python 2.7.x using `msiexec /i python-2.7.8.msi`. Accept all defaults.
|
||||
7. Install PyCrypto 2.1. Accept all defaults.
|
||||
8. Unzip DeDRM_plugin.zip and move kindlekey.py to somewhere in drive_c, such as ~/.wine/drive_c/DeDRM/libraryfiles/kindlekey.py.
|
||||
9. Run `wine 'C:\Python27/python.exe' 'C:\DeDRM/libraryfiles/kindlekey.py'`, or wherever you copied kindlekey.py to.
|
||||
10. Import the resulting key file to the Calibre plugin through the Kindle for Mac/PC ebooks option.
|
||||
|
||||
|
||||
Instructions for getting Kindle for PC and Adobe Digital Editions default decryption keys
|
||||
|
||||
229
FAQs.md
Normal file
229
FAQs.md
Normal file
@@ -0,0 +1,229 @@
|
||||
# Overview
|
||||
## What's this repository all about?
|
||||
Providing free open source tools to remove DRM from your 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 might 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 remove the DRM that's been applied to your ebooks.
|
||||
|
||||
### A Recent Change to Kindle for PC/Kindle for Mac
|
||||
Starting with version 1.19, Kindle for PC/Mac uses Amazon's new KFX format which these tools can't handle. There are two options to get the older formats that the tools can decrypt. Either stick with version 1.17 or earlier, or modify the executable by changing a file name.
|
||||
|
||||
Version 1.17 of Kindle is are no longer available directly from Amazon, so you will need to search for the proper file name and find it on a third party site. The name is "KindleForPC-installer-1.17.44170.exe" for PC and "KindleForMac-44182.dmg" for Mac.
|
||||
Verify the one of the following cryptographic hash values, using software of your choice, before installing the downloaded file in order to avoid viruses. If the hash does not match, delete the downloaded file and try again from another site.
|
||||
Kindle for PC:
|
||||
MD-5: 53F793B562F4823721AA47D7DE099869
|
||||
SHA-1: 73C404D719F0DD8D4AE1C2C96612B095D6C86255
|
||||
SHA-256: 14E0F0053F1276C0C7C446892DC170344F707FBFE99B695176 2C120144163200
|
||||
Kindle for Mac:
|
||||
MD-5: E7E36D5369E1F3CF1D28E5D9115DF15F
|
||||
SHA-1: 7AB9A86B954CB23D622BD79E3257F8E2182D791C
|
||||
SHA-256: 28DC21246A9C7CDEDD2D6F0F4082E6BF7EF9DB9CE9D485548E 8A9E1D19EAE2AC.
|
||||
|
||||
You will need to go to the preferences and uncheck the auto update checkbox. Then download and install 1.17 over the top of the 1.19 installation. You'll also need to delete the KFX folders from your My Kindle Content folder.
|
||||
|
||||
A second possible solution is to use 1.19 or later, but disable KFX by renaming or disabling a necessary component of the application. This may or may not work on versions after 1.20. In a command window, enter the following commands when Kindle for PC/Mac is not running:
|
||||
|
||||
#### Windows
|
||||
ren %localappdata%\Amazon\Kindle\application\renderer-test.exe renderer-test.xxx
|
||||
|
||||
PC Note: The renderer-test program may be in a different location in some Kindle for PC installations. If the rename command fails look in other folders, such as C:\Program Files\Amazon\Kindle.
|
||||
|
||||
#### Macintosh
|
||||
chmod -x /Applications/Kindle.app/Contents/MacOS/renderer-test
|
||||
|
||||
Mac Note: If the chmod command fails with a permission error try again using sudo before chmod - sudo chmod [...]
|
||||
|
||||
After restarting the Kindle program any books previously downloaded in KFX format will no longer open. You will need to remove them from your device and re-download them. All future downloads will use the older Kindle formats instead of KFX although they will continue to be placed in one individual subdirectory per book.
|
||||
|
||||
#### Another Note on KFX
|
||||
It now possible, but not easy, to convert books from KFX to other formats in calibre by installing the optional KFX Input plugin. The lack of automatic DRM removal makes this process difficult so it is not recommended unless there is no other alternative, such as for Indic language books only available in KFX. There is a windows-only KFX DRM rmeoval program in the repository, but not yet integrated into the tools.
|
||||
|
||||
#### Thanks
|
||||
Thanks to jhowell for his investigations into KFX format and workarounds. Some of these instructions are from [his thread on the subject](https://www.mobileread.com/forums/showthread.php?t=283371) at MobileRead.
|
||||
|
||||
## 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.
|
||||
|
||||
# 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.5.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.5.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 some older ActiveState Python Windows installers 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 to the correct place manually 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.
|
||||
|
||||
### I can't open the Macintosh Application at all. I get 'The aplication "DeDRM" can't be opened'
|
||||
Some unzip applications do not respect the execution bit setting. Try unzipping the main tools archive using the built-in Mac unzip utility.
|
||||
|
||||
### I can't open the Macintosh Application at all. I get 'spawn_via_launchd() failed, errno=111'
|
||||
There seems to be a bug in Apple's launch services. Try using the free [Maintenance utility](https://www.titanium-software.fr/en/maintenance.html) from Titanium Software to clear the launch cache and database.
|
||||
|
||||
# Using the Tools
|
||||
## I can’t get the tools to work on my rented or library ebooks.
|
||||
The tools are not designed to remove DRM from rented or library ebooks.
|
||||
|
||||
## 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 then the ReadMe files included in the tools folder(s) you're interested in. 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.
|
||||
DRM only gets removed when an ebook is imported into calibre. Also, 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 "Documents" folder ("My Documents" folder, pre-Windows 7)
|
||||
|
||||
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 this book is from an eInk Kindle (e.g. Paperwhite), 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 and user account as your copy of calibre.
|
||||
|
||||
If this book is from Kindle for Mac or Kindle for PC, you must be using version 1.17 or below, see note at top of this file.
|
||||
|
||||
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) version 1.17 or earlier (see above for download links).
|
||||
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.
|
||||
* 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 create a [new issue at Apprentice Harper's github repository](https://github.com/apprenticeharper/DeDRM_tools/issues/), attaching the book and the log, 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 3.0 or later 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 or later, 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 calibre plugin only imports the HTMLZ versions of the Topaz 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.
|
||||
|
||||
There are some optional shared libraries (`*.dll`, `*.dylib`, and `*.so`) included for performance. The source for any compiled pieces are provided within `alfcrypto_src.zip`. If this is a concern either delete the binary files or manually rebuild them.
|
||||
|
||||
## 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 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.
|
||||
|
||||
## 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 or create a new issue at Apprentice Harper's github repository, 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.
|
||||
Binary file not shown.
@@ -19,7 +19,7 @@ except NameError:
|
||||
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 = (3, 1, 2)
|
||||
PLUGIN_VERSION_TUPLE = (6, 5, 4)
|
||||
PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE])
|
||||
HELPFILE_NAME = PLUGIN_SAFE_NAME + '_Help.htm'
|
||||
PLUGIN_AUTHORS = 'Anon'
|
||||
@@ -29,7 +29,7 @@ class ObokDeDRMAction(InterfaceActionBase):
|
||||
|
||||
name = PLUGIN_NAME
|
||||
description = PLUGIN_DESCRIPTION
|
||||
supported_platforms = ['windows', 'osx']
|
||||
supported_platforms = ['windows', 'osx', 'linux' ]
|
||||
author = PLUGIN_AUTHORS
|
||||
version = PLUGIN_VERSION_TUPLE
|
||||
minimum_calibre_version = (1, 0, 0)
|
||||
|
||||
@@ -6,7 +6,7 @@ __license__ = 'GPL v3'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
|
||||
import os, zipfile
|
||||
import os, traceback, zipfile
|
||||
|
||||
try:
|
||||
from PyQt5.Qt import QToolButton, QUrl
|
||||
@@ -78,8 +78,30 @@ class InterfacePluginAction(InterfaceAction):
|
||||
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()
|
||||
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()
|
||||
@@ -98,6 +120,7 @@ class InterfacePluginAction(InterfaceAction):
|
||||
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)
|
||||
|
||||
@@ -3,15 +3,23 @@ from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
try:
|
||||
from PyQt5.Qt import (QWidget, QLabel, QVBoxLayout, QHBoxLayout, QComboBox)
|
||||
from PyQt5.Qt import (Qt, QGroupBox, QListWidget, QLineEdit, QDialogButtonBox, QWidget, QLabel, QDialog, QVBoxLayout, QAbstractItemView, QIcon, QHBoxLayout, QComboBox, QListWidgetItem)
|
||||
except ImportError:
|
||||
from PyQt4.Qt import (QWidget, QLabel, QVBoxLayout, QHBoxLayout, QComboBox)
|
||||
|
||||
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")
|
||||
@@ -26,7 +34,10 @@ class ConfigWidget(QWidget):
|
||||
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()
|
||||
@@ -35,6 +46,178 @@ class ConfigWidget(QWidget):
|
||||
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)
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.1 KiB |
@@ -1,6 +1,48 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Version 3.2.5 December 2016
|
||||
# Improve detection of good text decryption.
|
||||
#
|
||||
# Version 3.2.4 December 2016
|
||||
# Remove incorrect support for Kobo Desktop under Wine
|
||||
#
|
||||
# Version 3.2.3 October 2016
|
||||
# Fix for windows network user and more xml fixes
|
||||
#
|
||||
# Version 3.2.2 October 2016
|
||||
# Change to the way the new database version is handled.
|
||||
#
|
||||
# Version 3.2.1 September 2016
|
||||
# Update for v4.0 of Windows Desktop app.
|
||||
#
|
||||
# Version 3.2.0 January 2016
|
||||
# Update for latest version of Windows Desktop app.
|
||||
# Support Kobo devices in the command line version.
|
||||
#
|
||||
# Version 3.1.9 November 2015
|
||||
# Handle Kobo Desktop under wine on Linux
|
||||
#
|
||||
# Version 3.1.8 November 2015
|
||||
# Handle the case of Kobo Arc or Vox device (i.e. don't crash).
|
||||
#
|
||||
# Version 3.1.7 October 2015
|
||||
# Handle the case of no device or database more gracefully.
|
||||
#
|
||||
# Version 3.1.6 September 2015
|
||||
# Enable support for Kobo devices
|
||||
# More character encoding fixes (unicode strings)
|
||||
#
|
||||
# Version 3.1.5 September 2015
|
||||
# Removed requirement that a purchase has been made.
|
||||
# Also add in character encoding fixes
|
||||
#
|
||||
# Version 3.1.4 September 2015
|
||||
# Updated for version 3.17 of the Windows Desktop app.
|
||||
#
|
||||
# Version 3.1.3 August 2015
|
||||
# Add translations for Portuguese and Arabic
|
||||
#
|
||||
# Version 3.1.2 January 2015
|
||||
# Add coding, version number and version announcement
|
||||
#
|
||||
@@ -109,7 +151,8 @@
|
||||
#
|
||||
"""Manage all Kobo books, either encrypted or DRM-free."""
|
||||
|
||||
__version__ = '3.1.1'
|
||||
__version__ = '3.2.4'
|
||||
__about__ = u"Obok v{0}\nCopyright © 2012-2016 Physisticated et al.".format(__version__)
|
||||
|
||||
import sys
|
||||
import os
|
||||
@@ -123,6 +166,19 @@ import hashlib
|
||||
import xml.etree.ElementTree as ET
|
||||
import string
|
||||
import shutil
|
||||
import argparse
|
||||
import tempfile
|
||||
|
||||
can_parse_xml = True
|
||||
try:
|
||||
from xml.etree import ElementTree as ET
|
||||
# print u"using xml.etree for xml parsing"
|
||||
except ImportError:
|
||||
can_parse_xml = False
|
||||
# print u"Cannot find xml.etree, disabling extraction of serial numbers"
|
||||
|
||||
# List of all known hash keys
|
||||
KOBO_HASH_KEYS = ['88b3a2e13', 'XzUhGYdFp', 'NoCanLook','QJhwzAtXL']
|
||||
|
||||
class ENCRYPTIONError(Exception):
|
||||
pass
|
||||
@@ -209,6 +265,24 @@ def _load_crypto():
|
||||
|
||||
AES = _load_crypto()
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
|
||||
class KoboLibrary(object):
|
||||
"""The Kobo library.
|
||||
|
||||
@@ -216,28 +290,111 @@ class KoboLibrary(object):
|
||||
written by the Kobo Desktop Edition application, including the list
|
||||
of books, their titles, and the user's encryption key(s)."""
|
||||
|
||||
def __init__ (self):
|
||||
print u"Obok v{0}\nCopyright © 2012-2014 Physisticated et al.".format(__version__)
|
||||
if sys.platform.startswith('win'):
|
||||
if sys.getwindowsversion().major > 5:
|
||||
self.kobodir = os.environ['LOCALAPPDATA']
|
||||
else:
|
||||
self.kobodir = os.path.join(os.environ['USERPROFILE'], 'Local Settings', 'Application Data')
|
||||
self.kobodir = os.path.join(self.kobodir, 'Kobo', 'Kobo Desktop Edition')
|
||||
elif sys.platform.startswith('darwin'):
|
||||
self.kobodir = os.path.join(os.environ['HOME'], 'Library', 'Application Support', 'Kobo', 'Kobo Desktop Edition')
|
||||
self.bookdir = os.path.join(self.kobodir, 'kepub')
|
||||
kobodb = os.path.join(self.kobodir, 'Kobo.sqlite')
|
||||
self.__sqlite = sqlite3.connect(kobodb)
|
||||
self.__cursor = self.__sqlite.cursor()
|
||||
self._userkeys = []
|
||||
self._books = []
|
||||
self._volumeID = []
|
||||
def __init__ (self, serials = [], device_path = None):
|
||||
print __about__
|
||||
self.kobodir = u""
|
||||
kobodb = u""
|
||||
|
||||
# Order of checks
|
||||
# 1. first check if a device_path has been passed in, and whether
|
||||
# we can find the sqlite db in the respective place
|
||||
# 2. if 1., and we got some serials passed in (from saved
|
||||
# settings in calibre), just use it
|
||||
# 3. if 1. worked, but we didn't get serials, try to parse them
|
||||
# from the device, if this didn't work, unset everything
|
||||
# 4. if by now we don't have kobodir set, give up on device and
|
||||
# try to use the Desktop app.
|
||||
|
||||
# step 1. check whether this looks like a real device
|
||||
if (device_path):
|
||||
# we got a device path
|
||||
self.kobodir = os.path.join(device_path, u".kobo")
|
||||
# devices use KoboReader.sqlite
|
||||
kobodb = os.path.join(self.kobodir, u"KoboReader.sqlite")
|
||||
if (not(os.path.isfile(kobodb))):
|
||||
# device path seems to be wrong, unset it
|
||||
device_path = u""
|
||||
self.kobodir = u""
|
||||
kobodb = u""
|
||||
|
||||
if (self.kobodir):
|
||||
# step 3. we found a device but didn't get serials, try to get them
|
||||
if (len(serials) == 0):
|
||||
# we got a device path but no saved serial
|
||||
# try to get the serial from the device
|
||||
# print u"get_device_settings - device_path = {0}".format(device_path)
|
||||
# get serial from device_path/.adobe-digital-editions/device.xml
|
||||
if can_parse_xml:
|
||||
devicexml = os.path.join(device_path, '.adobe-digital-editions', 'device.xml')
|
||||
# print u"trying to load {0}".format(devicexml)
|
||||
if (os.path.exists(devicexml)):
|
||||
# print u"trying to parse {0}".format(devicexml)
|
||||
xmltree = ET.parse(devicexml)
|
||||
for node in xmltree.iter():
|
||||
if "deviceSerial" in node.tag:
|
||||
serial = node.text
|
||||
# print u"found serial {0}".format(serial)
|
||||
serials.append(serial)
|
||||
break
|
||||
else:
|
||||
# print u"cannot get serials from device."
|
||||
device_path = u""
|
||||
self.kobodir = u""
|
||||
kobodb = u""
|
||||
|
||||
if (self.kobodir == u""):
|
||||
# step 4. we haven't found a device with serials, so try desktop apps
|
||||
if sys.platform.startswith('win'):
|
||||
import _winreg as winreg
|
||||
if sys.getwindowsversion().major > 5:
|
||||
if 'LOCALAPPDATA' in os.environ.keys():
|
||||
# Python 2.x does not return unicode env. Use Python 3.x
|
||||
self.kobodir = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
|
||||
if (self.kobodir == u""):
|
||||
if 'USERPROFILE' in os.environ.keys():
|
||||
# Python 2.x does not return unicode env. Use Python 3.x
|
||||
self.kobodir = os.path.join(winreg.ExpandEnvironmentStrings(u"%USERPROFILE%"), u"Local Settings", u"Application Data")
|
||||
self.kobodir = os.path.join(self.kobodir, u"Kobo", u"Kobo Desktop Edition")
|
||||
elif sys.platform.startswith('darwin'):
|
||||
self.kobodir = os.path.join(os.environ['HOME'], u"Library", u"Application Support", u"Kobo", u"Kobo Desktop Edition")
|
||||
#elif linux_path != None:
|
||||
# Probably Linux, let's get the wine prefix and path to Kobo.
|
||||
# self.kobodir = os.path.join(linux_path, u"Local Settings", u"Application Data", u"Kobo", u"Kobo Desktop Edition")
|
||||
# desktop versions use Kobo.sqlite
|
||||
kobodb = os.path.join(self.kobodir, u"Kobo.sqlite")
|
||||
# check for existence of file
|
||||
if (not(os.path.isfile(kobodb))):
|
||||
# give up here, we haven't found anything useful
|
||||
self.kobodir = u""
|
||||
kobodb = u""
|
||||
|
||||
|
||||
if (self.kobodir != u""):
|
||||
self.bookdir = os.path.join(self.kobodir, u"kepub")
|
||||
# make a copy of the database in a temporary file
|
||||
# so we can ensure it's not using WAL logging which sqlite3 can't do.
|
||||
self.newdb = tempfile.NamedTemporaryFile(mode='wb', delete=False)
|
||||
print self.newdb.name
|
||||
olddb = open(kobodb, 'rb')
|
||||
self.newdb.write(olddb.read(18))
|
||||
self.newdb.write('\x01\x01')
|
||||
olddb.read(2)
|
||||
self.newdb.write(olddb.read())
|
||||
olddb.close()
|
||||
self.newdb.close()
|
||||
self.__sqlite = sqlite3.connect(self.newdb.name)
|
||||
self.__cursor = self.__sqlite.cursor()
|
||||
self._userkeys = []
|
||||
self._books = []
|
||||
self._volumeID = []
|
||||
self._serials = serials
|
||||
|
||||
def close (self):
|
||||
"""Closes the database used by the library."""
|
||||
self.__cursor.close()
|
||||
self.__sqlite.close()
|
||||
# delete the temporary copy of the database
|
||||
os.remove(self.newdb.name)
|
||||
|
||||
@property
|
||||
def userkeys (self):
|
||||
@@ -246,9 +403,8 @@ class KoboLibrary(object):
|
||||
"""
|
||||
if len(self._userkeys) != 0:
|
||||
return self._userkeys
|
||||
userid = self.__getuserid()
|
||||
for macaddr in self.__getmacaddrs():
|
||||
self._userkeys.append(self.__getuserkey(macaddr, userid))
|
||||
self._userkeys.extend(self.__getuserkeys(macaddr))
|
||||
return self._userkeys
|
||||
|
||||
@property
|
||||
@@ -274,14 +430,15 @@ class KoboLibrary(object):
|
||||
|
||||
def __bookfile (self, volumeid):
|
||||
"""The filename needed to open a given book."""
|
||||
return os.path.join(self.kobodir, 'kepub', volumeid)
|
||||
return os.path.join(self.kobodir, u"kepub", volumeid)
|
||||
|
||||
def __getmacaddrs (self):
|
||||
"""The list of all MAC addresses on this machine."""
|
||||
macaddrs = []
|
||||
if sys.platform.startswith('win'):
|
||||
c = re.compile('\s(' + '[0-9a-f]{2}-' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
|
||||
for line in os.popen('ipconfig /all'):
|
||||
(p_in, p_out, p_err) = os.popen3('ipconfig /all')
|
||||
for line in p_out:
|
||||
m = c.search(line)
|
||||
if m:
|
||||
macaddrs.append(re.sub("-", ":", m.group(1)).upper())
|
||||
@@ -290,17 +447,44 @@ class KoboLibrary(object):
|
||||
output = subprocess.check_output('/sbin/ifconfig -a', shell=True)
|
||||
matches = c.findall(output)
|
||||
for m in matches:
|
||||
# print "m:",m[0]
|
||||
# print u"m:{0}".format(m[0])
|
||||
macaddrs.append(m[0].upper())
|
||||
else:
|
||||
# probably linux, let's try ipconfig under wine
|
||||
c = re.compile('\s(' + '[0-9a-f]{2}-' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
|
||||
for line in os.popen('ipconfig /all'):
|
||||
m = c.search(line)
|
||||
if m:
|
||||
macaddrs.append(re.sub("-", ":", m.group(1)).upper())
|
||||
|
||||
# extend the list of macaddrs in any case with the serials
|
||||
# cannot hurt ;-)
|
||||
macaddrs.extend(self._serials)
|
||||
|
||||
return macaddrs
|
||||
|
||||
def __getuserid (self):
|
||||
return self.__cursor.execute('SELECT UserID FROM user WHERE HasMadePurchase = "true"').fetchone()[0]
|
||||
|
||||
def __getuserkey (self, macaddr, userid):
|
||||
deviceid = hashlib.sha256('NoCanLook' + macaddr).hexdigest()
|
||||
userkey = hashlib.sha256(deviceid + userid).hexdigest()
|
||||
return binascii.a2b_hex(userkey[32:])
|
||||
def __getuserids (self):
|
||||
userids = []
|
||||
cursor = self.__cursor.execute('SELECT UserID FROM user')
|
||||
row = cursor.fetchone()
|
||||
while row is not None:
|
||||
try:
|
||||
userid = row[0]
|
||||
userids.append(userid)
|
||||
except:
|
||||
pass
|
||||
row = cursor.fetchone()
|
||||
return userids
|
||||
|
||||
def __getuserkeys (self, macaddr):
|
||||
userids = self.__getuserids()
|
||||
userkeys = []
|
||||
for hash in KOBO_HASH_KEYS:
|
||||
deviceid = hashlib.sha256(hash + macaddr).hexdigest()
|
||||
for userid in userids:
|
||||
userkey = hashlib.sha256(deviceid + userid).hexdigest()
|
||||
userkeys.append(binascii.a2b_hex(userkey[32:]))
|
||||
return userkeys
|
||||
|
||||
class KoboBook(object):
|
||||
"""A Kobo book.
|
||||
@@ -404,16 +588,62 @@ class KoboFile(object):
|
||||
Returns True if the content was checked, False if it was not
|
||||
checked."""
|
||||
if self.mimetype == 'application/xhtml+xml':
|
||||
if contents[:5]=="<?xml":
|
||||
# assume utf-8 with no BOM
|
||||
textoffset = 0
|
||||
stride = 1
|
||||
print u"Checking text:{0}:".format(contents[:10])
|
||||
# check for byte order mark
|
||||
if contents[:3]=="\xef\xbb\xbf":
|
||||
# seems to be utf-8 with BOM
|
||||
print u"Could be utf-8 with BOM"
|
||||
textoffset = 3
|
||||
elif contents[:2]=="\xfe\xff":
|
||||
# seems to be utf-16BE
|
||||
print u"Could be utf-16BE"
|
||||
textoffset = 3
|
||||
stride = 2
|
||||
elif contents[:2]=="\xff\xfe":
|
||||
# seems to be utf-16LE
|
||||
print u"Could be utf-16LE"
|
||||
textoffset = 2
|
||||
stride = 2
|
||||
else:
|
||||
print u"Perhaps utf-8 without BOM"
|
||||
|
||||
# now check that the first few characters are in the ASCII range
|
||||
for i in xrange(textoffset,textoffset+5*stride,stride):
|
||||
if ord(contents[i])<32 or ord(contents[i])>127:
|
||||
# Non-ascii, so decryption probably failed
|
||||
print u"Bad character at {0}, value {1}".format(i,ord(contents[i]))
|
||||
raise ValueError
|
||||
print u"Seems to be good text"
|
||||
return True
|
||||
if contents[:5]=="<?xml" or contents[:8]=="\xef\xbb\xbf<?xml":
|
||||
# utf-8
|
||||
return True
|
||||
elif contents[:14]=="\xfe\xff\x00<\x00?\x00x\x00m\x00l":
|
||||
# utf-16BE
|
||||
return True
|
||||
elif contents[:14]=="\xff\xfe<\x00?\x00x\x00m\x00l\x00":
|
||||
# utf-16LE
|
||||
return True
|
||||
elif contents[:9]=="<!DOCTYPE" or contents[:12]=="\xef\xbb\xbf<!DOCTYPE":
|
||||
# utf-8 of weird <!DOCTYPE start
|
||||
return True
|
||||
elif contents[:22]=="\xfe\xff\x00<\x00!\x00D\x00O\x00C\x00T\x00Y\x00P\x00E":
|
||||
# utf-16BE of weird <!DOCTYPE start
|
||||
return True
|
||||
elif contents[:22]=="\xff\xfe<\x00!\x00D\x00O\x00C\x00T\x00Y\x00P\x00E\x00":
|
||||
# utf-16LE of weird <!DOCTYPE start
|
||||
return True
|
||||
else:
|
||||
print "Bad XML: ",contents[:5]
|
||||
print u"Bad XML: {0}".format(contents[:8])
|
||||
raise ValueError
|
||||
if self.mimetype == 'image/jpeg':
|
||||
elif self.mimetype == 'image/jpeg':
|
||||
if contents[:3] == '\xff\xd8\xff':
|
||||
return True
|
||||
else:
|
||||
print "Bad JPEG: ", contents[:3].encode('hex')
|
||||
print u"Bad JPEG: {0}".format(contents[:3].encode('hex'))
|
||||
raise ValueError()
|
||||
return False
|
||||
|
||||
@@ -435,36 +665,19 @@ class KoboFile(object):
|
||||
contents = contents[:-padding]
|
||||
return contents
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
lib = KoboLibrary()
|
||||
|
||||
for i, book in enumerate(lib.books):
|
||||
print ('%d: %s' % (i + 1, book.title)).encode('ascii', 'ignore')
|
||||
|
||||
num_string = raw_input("Convert book number... ")
|
||||
try:
|
||||
num = int(num_string)
|
||||
book = lib.books[num - 1]
|
||||
except (ValueError, IndexError):
|
||||
exit()
|
||||
|
||||
print "Converting", book.title
|
||||
|
||||
def decrypt_book(book, lib):
|
||||
print u"Converting {0}".format(book.title)
|
||||
zin = zipfile.ZipFile(book.filename, "r")
|
||||
# make filename out of Unicode alphanumeric and whitespace equivalents from title
|
||||
outname = "%s.epub" % (re.sub('[^\s\w]', '', book.title, 0, re.UNICODE))
|
||||
|
||||
outname = u"{0}.epub".format(re.sub('[^\s\w]', '_', book.title, 0, re.UNICODE))
|
||||
if (book.type == 'drm-free'):
|
||||
print "DRM-free book, conversion is not needed"
|
||||
print u"DRM-free book, conversion is not needed"
|
||||
shutil.copyfile(book.filename, outname)
|
||||
print "Book saved as", os.path.join(os.getcwd(), outname)
|
||||
exit(0)
|
||||
|
||||
print u"Book saved as {0}".format(os.path.join(os.getcwd(), outname))
|
||||
return 0
|
||||
result = 1
|
||||
for userkey in lib.userkeys:
|
||||
# print "Trying key: ",userkey.encode('hex_codec')
|
||||
confirmedGood = False
|
||||
print u"Trying key: {0}".format(userkey.encode('hex_codec'))
|
||||
try:
|
||||
zout = zipfile.ZipFile(outname, "w", zipfile.ZIP_DEFLATED)
|
||||
for filename in zin.namelist():
|
||||
@@ -473,18 +686,62 @@ if __name__ == '__main__':
|
||||
file = book.encryptedfiles[filename]
|
||||
contents = file.decrypt(userkey, contents)
|
||||
# Parse failures mean the key is probably wrong.
|
||||
if not confirmedGood:
|
||||
confirmedGood = file.check(contents)
|
||||
file.check(contents)
|
||||
zout.writestr(filename, contents)
|
||||
zout.close()
|
||||
print "Book saved as", os.path.join(os.getcwd(), outname)
|
||||
print u"Decryption succeeded."
|
||||
print u"Book saved as {0}".format(os.path.join(os.getcwd(), outname))
|
||||
result = 0
|
||||
break
|
||||
except ValueError:
|
||||
print "Decryption failed, trying next key"
|
||||
print u"Decryption failed."
|
||||
zout.close()
|
||||
os.remove(outname)
|
||||
|
||||
zin.close()
|
||||
return result
|
||||
|
||||
|
||||
def cli_main():
|
||||
description = __about__
|
||||
epilog = u"Parsing of arguments failed."
|
||||
parser = argparse.ArgumentParser(prog=sys.argv[0], description=description, epilog=epilog)
|
||||
parser.add_argument('--devicedir', default='/media/KOBOeReader', help="directory of connected Kobo device")
|
||||
parser.add_argument('--all', action='store_true', help="flag for converting all books on device")
|
||||
args = vars(parser.parse_args())
|
||||
serials = []
|
||||
devicedir = u""
|
||||
if args['devicedir']:
|
||||
devicedir = args['devicedir']
|
||||
|
||||
lib = KoboLibrary(serials, devicedir)
|
||||
|
||||
if args['all']:
|
||||
books = lib.books
|
||||
else:
|
||||
for i, book in enumerate(lib.books):
|
||||
print u"{0}: {1}".format(i + 1, book.title)
|
||||
print u"Or 'all'"
|
||||
|
||||
choice = raw_input(u"Convert book number... ")
|
||||
if choice == u'all':
|
||||
books = list(lib.books)
|
||||
else:
|
||||
try:
|
||||
num = int(choice)
|
||||
books = [lib.books[num - 1]]
|
||||
except (ValueError, IndexError):
|
||||
print u"Invalid choice. Exiting..."
|
||||
exit()
|
||||
|
||||
results = [decrypt_book(book, lib) for book in books]
|
||||
lib.close()
|
||||
exit(result)
|
||||
overall_result = all(result != 0 for result in results)
|
||||
if overall_result != 0:
|
||||
print u"Could not decrypt book with any of the keys found."
|
||||
return overall_result
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<body>
|
||||
|
||||
<h1>Obok DeDRM Plugin</h1>
|
||||
<h3>(version 3.1.2)</h3>
|
||||
<h3>(version 3.1.3)</h3>
|
||||
|
||||
<h3>Installation:</h3>
|
||||
|
||||
|
||||
BIN
Obok_calibre_plugin/obok_plugin/translations/ar.mo
Normal file
BIN
Obok_calibre_plugin/obok_plugin/translations/ar.mo
Normal file
Binary file not shown.
@@ -3,33 +3,37 @@
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2014-11-17 12:51+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"PO-Revision-Date: 2015-05-31 22:44+1000\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"X-Generator: Poedit 1.8.1\n"
|
||||
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
|
||||
"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
|
||||
"Language: ar\n"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:80
|
||||
msgid ""
|
||||
"<p>No books found in Kobo Library\n"
|
||||
"Are you sure it's installed\\configured\\synchronized?"
|
||||
msgstr ""
|
||||
"<p>لا يوجد كتب بمكتبة كوبو الخاصة بكم\n"
|
||||
"هل أنت متأكد أنها موجودة\\معدة بشكل سليم\\محملة بشكل كامل؟"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:87
|
||||
msgid "Legacy key found: "
|
||||
msgstr ""
|
||||
msgstr "تم العثور على مفتاح شفرة قديم"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:93
|
||||
msgid "Trouble retrieving keys with newer obok method."
|
||||
msgstr ""
|
||||
msgstr "هناك مشكلة فى تحميل مفاتيح الشفرة بطريقة أوبوك الجديدة."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:97
|
||||
msgid "Found {0} possible keys to try."
|
||||
@@ -150,13 +154,14 @@ msgstr ""
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:399
|
||||
msgid "<p>{0} successfully added."
|
||||
msgstr ""
|
||||
msgstr "<p>{0} تم إضافتهم بنجاح."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:403
|
||||
msgid ""
|
||||
"<p>Not all selected Kobo books made it into calibre.<br /><br />View report "
|
||||
"for details."
|
||||
msgstr ""
|
||||
"<p>لم يتم نقل كل كتب كوبو إلى كاليبر.<br/><br/>شاهد التقرير لمزيد من التفاصيل"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:404
|
||||
msgid "<p><b>Total attempted:</b> {}</p>\n"
|
||||
@@ -200,7 +205,7 @@ msgstr ""
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:458
|
||||
msgid "Unknown Book Title"
|
||||
msgstr ""
|
||||
msgstr "عنوان غير معروف"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:460
|
||||
msgid "it couldn't be decrypted."
|
||||
@@ -214,15 +219,15 @@ msgstr ""
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:464
|
||||
msgid "of unknown reasons. Gosh I'm embarrassed!"
|
||||
msgstr ""
|
||||
msgstr "معذرة، خطأ غير معروف! أشعر بالخجل من هذا!"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:465
|
||||
msgid "<p>{0} not added because {1}"
|
||||
msgstr ""
|
||||
msgstr "<p>لم يتم إضافة {0} بسبب {1}"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:226
|
||||
msgid "Help"
|
||||
msgstr ""
|
||||
msgstr "مساعدة"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:235
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:214
|
||||
@@ -237,7 +242,7 @@ msgstr ""
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:322
|
||||
msgid "Undefined"
|
||||
msgstr ""
|
||||
msgstr "غير معرف"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:30
|
||||
msgid "When should Obok try to insert EPUBs into existing calibre entries?"
|
||||
@@ -251,32 +256,23 @@ msgstr ""
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
|
||||
msgid "Ask"
|
||||
msgstr ""
|
||||
msgstr "اسأل"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
msgstr "دائما"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:60
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:150
|
||||
msgid " v"
|
||||
msgstr ""
|
||||
msgstr "أبدا"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:65
|
||||
msgid "Obok DeDRM"
|
||||
msgstr ""
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:68
|
||||
msgid "<a href=\"http://www.foo.com/\">Help</a>"
|
||||
msgstr ""
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:89
|
||||
msgid "Select All"
|
||||
msgstr ""
|
||||
msgstr "اختر الكل"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:90
|
||||
msgid "Select all books to add them to the calibre library."
|
||||
@@ -300,23 +296,23 @@ msgstr ""
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
msgstr "العنوان"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
|
||||
msgid "Author"
|
||||
msgstr ""
|
||||
msgstr "مؤلِّف"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
|
||||
msgid "Series"
|
||||
msgstr ""
|
||||
msgstr "السلسلة"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:369
|
||||
msgid "Copy to clipboard"
|
||||
msgstr ""
|
||||
msgstr "نسخ إلى الحافظة"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:397
|
||||
msgid "View Report"
|
||||
msgstr ""
|
||||
msgstr "مشاهدة التقرير"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\__init__.py:21
|
||||
msgid "Removes DRM from Kobo kepubs and adds them to the library."
|
||||
@@ -328,8 +324,8 @@ msgstr ""
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:167
|
||||
msgid "Failed to initialize AES key"
|
||||
msgstr ""
|
||||
msgstr "خطأ فى تحميل مفتاح AES"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:175
|
||||
msgid "AES decryption failed"
|
||||
msgstr ""
|
||||
msgstr "خطأ فى فك الحماية بطريقة AES"
|
||||
BIN
Obok_calibre_plugin/obok_plugin/translations/de.mo
Normal file
BIN
Obok_calibre_plugin/obok_plugin/translations/de.mo
Normal file
Binary file not shown.
BIN
Obok_calibre_plugin/obok_plugin/translations/es.mo
Normal file
BIN
Obok_calibre_plugin/obok_plugin/translations/es.mo
Normal file
Binary file not shown.
BIN
Obok_calibre_plugin/obok_plugin/translations/nl.mo
Normal file
BIN
Obok_calibre_plugin/obok_plugin/translations/nl.mo
Normal file
Binary file not shown.
BIN
Obok_calibre_plugin/obok_plugin/translations/pt.mo
Normal file
BIN
Obok_calibre_plugin/obok_plugin/translations/pt.mo
Normal file
Binary file not shown.
361
Obok_calibre_plugin/obok_plugin/translations/pt.po
Normal file
361
Obok_calibre_plugin/obok_plugin/translations/pt.po
Normal file
@@ -0,0 +1,361 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2014-11-17 12:51+0100\n"
|
||||
"PO-Revision-Date: 2015-05-31 22:44+1000\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"X-Generator: Poedit 1.8.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"Language: pt\n"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:80
|
||||
msgid ""
|
||||
"<p>No books found in Kobo Library\n"
|
||||
"Are you sure it's installed\\configured\\synchronized?"
|
||||
msgstr ""
|
||||
"<p>Não foram encontrados livros na livraria Kobo\n"
|
||||
"Tem a certeza de que está instalado\\configured\\synchronized?"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:87
|
||||
msgid "Legacy key found: "
|
||||
msgstr "Chave de legado encontrada"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:93
|
||||
msgid "Trouble retrieving keys with newer obok method."
|
||||
msgstr "Problema na obtenção das chaves com o novo método obok."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:97
|
||||
msgid "Found {0} possible keys to try."
|
||||
msgstr "Encontradas {0} chaves possíveis para experimentar."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:99
|
||||
msgid "<p>No userkeys found to decrypt books with. No point in proceeding."
|
||||
msgstr ""
|
||||
"Não foi encontrada nenhuma chave de usuário com a qual desencriptar os "
|
||||
"livros. Não vale a pena continuar."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:115
|
||||
msgid "{} - Decryption canceled by user."
|
||||
msgstr "Desencriptação cancelada pelo utilizador."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:135
|
||||
msgid "{} - \"Add books\" canceled by user."
|
||||
msgstr "{} - \"Adição de livros\" cancelada pelo utilizador."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:137
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:156
|
||||
msgid "{} - wrapping up results."
|
||||
msgstr "{} - finalizando os resultados."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:153
|
||||
msgid "{} - User opted not to try to insert EPUB formats"
|
||||
msgstr "O utilizador optou por não tentar inserir formatos EPUB"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:188
|
||||
msgid "{0} - Decrypting {1}"
|
||||
msgstr "Desencriptando"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:197
|
||||
msgid "{0} - Couldn't decrypt {1}"
|
||||
msgstr "{0} - Não foi possível desencriptar {1}"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:198
|
||||
msgid "decryption errors"
|
||||
msgstr "erros na desencriptação"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:213
|
||||
msgid "{0} - Added {1}"
|
||||
msgstr "{0} - Adicionado {1}"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:218
|
||||
msgid "{0} - {1} already exists. Will try to add format later."
|
||||
msgstr "{0} - {1} já existe. a adição do formato irá ser tentada mais tarde."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:219
|
||||
msgid "duplicate detected"
|
||||
msgstr "detectados duplicados"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:233
|
||||
msgid "{0} - Successfully added EPUB format to existing {1}"
|
||||
msgstr "{0} - formato EPUB adicionado com sucesso ao existente {1}"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:236
|
||||
msgid ""
|
||||
"{0} - Error adding EPUB format to existing {1}. This really shouldn't happen."
|
||||
msgstr ""
|
||||
"{0} - Erro ao adicionar o formato EPUB ao existente {1}. Isto realmente não "
|
||||
"deveria acontecer."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:259
|
||||
msgid "{} - \"Insert formats\" canceled by user."
|
||||
msgstr "{} - \"Inserção de formatos\" cancelada pelo utilizador."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:291
|
||||
msgid ""
|
||||
"<p><b>{0}</b> EPUB{2} successfully added to library.<br /><br /><b>{1}</b> "
|
||||
msgstr ""
|
||||
"<p><b>{0}</b> EPUB{2} adicionado com sucesso à biblioteca.<br /><br /><b>{1}"
|
||||
"</b> "
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:292
|
||||
msgid ""
|
||||
"not added because books with the same title/author were detected.<br /><br /"
|
||||
">Would you like to try and add the EPUB format{0}"
|
||||
msgstr ""
|
||||
"não adicionados porque foram detectados com o mesmo título/autor.<br /><br /"
|
||||
">Gostaria de tentar e adicionar o formato EPUB{0}"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:293
|
||||
msgid ""
|
||||
" to those existing entries?<br /><br />NOTE: no pre-existing EPUBs will be "
|
||||
"overwritten."
|
||||
msgstr ""
|
||||
" às entradas existentes?<br /><br />NOTA: EPUBs pré existentes não serão "
|
||||
"reescritos."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:295
|
||||
msgid ""
|
||||
"{0} -- not added because of {1} in your library.\n"
|
||||
"\n"
|
||||
msgstr ""
|
||||
"{0} -- não adicionado porque {1} na biblioteca.\n"
|
||||
"\n"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:297
|
||||
msgid "<p><b>{0}</b> -- not added because of {1} in your library.<br /><br />"
|
||||
msgstr "<p><b>{0}</b> -- não adicionado porque {1} na biblioteca.<br /><br />"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:298
|
||||
msgid ""
|
||||
"Would you like to try and add the EPUB format to an available calibre "
|
||||
"duplicate?<br /><br />"
|
||||
msgstr ""
|
||||
"Gostaria de tentar adicionar o formato EPUB a um duplicado já existente no "
|
||||
"calibre?<br /><br />"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:299
|
||||
msgid "NOTE: no pre-existing EPUB will be overwritten."
|
||||
msgstr "NOTA: EPUBs pré existentes não serão reescritos."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:346
|
||||
msgid "Trying key: "
|
||||
msgstr "Experimentando a chave:"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:378
|
||||
msgid "Decryption failed, trying next key."
|
||||
msgstr "A desencriptação falhou, tentado a próxima chave."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:382
|
||||
msgid "Unknown Error decrypting, trying next key.."
|
||||
msgstr "Erro desconhecido na desencriptação, tentado a próxima chave."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:395
|
||||
msgid ""
|
||||
"<p>All selected Kobo books added as new calibre books or inserted into "
|
||||
"existing calibre ebooks.<br /><br />No issues."
|
||||
msgstr ""
|
||||
"<p>Todos os livros Kobo selecionados foram adicionados como livros novos no "
|
||||
"calibre ou inseridos em livros já existentes no calibre.<br /><br />Sem "
|
||||
"problemas."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:399
|
||||
msgid "<p>{0} successfully added."
|
||||
msgstr "<p>{0} adicionados com sucesso."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:403
|
||||
msgid ""
|
||||
"<p>Not all selected Kobo books made it into calibre.<br /><br />View report "
|
||||
"for details."
|
||||
msgstr ""
|
||||
"<p>Nem todos os livros Kobo selecionados seguiram para o calibre.<br /><br /"
|
||||
">Veja o relatório para mais detalhes."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:404
|
||||
msgid "<p><b>Total attempted:</b> {}</p>\n"
|
||||
msgstr "<p><b>tentativas totais:</b> {}</p>\n"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:405
|
||||
msgid "<p><b>Decryption errors:</b> {}</p>\n"
|
||||
msgstr "<p><b>Erros de desencriptação:</b> {}</p>\n"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:411
|
||||
msgid "<p><b>New Books created:</b> {}</p>\n"
|
||||
msgstr "<p><b>Novos livros criados:</b> {}</p>\n"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:418
|
||||
msgid "<p><b>Duplicates that weren't added:</b> {}</p>\n"
|
||||
msgstr "<p><b>Duplicados não adicionados:</b> {}</p>\n"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:426
|
||||
msgid "<p><b>Book imports cancelled by user:</b> {}</p>\n"
|
||||
msgstr "<p><b>Importação de livros cancelada pelo utilizador:</b> {}</p>\n"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:428
|
||||
msgid ""
|
||||
"<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n"
|
||||
msgstr ""
|
||||
"<p><b>Novos formatos EPUB inseridos em livros existentes no calibre:</b> {0}"
|
||||
"</p>\n"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:434
|
||||
msgid ""
|
||||
"<p><b>EPUB formats NOT inserted into existing calibre books:</b> {}<br />\n"
|
||||
msgstr ""
|
||||
"<p><b>formatos EPUB NÃO inseridos em livros existentes no calibre:</b> {}"
|
||||
"<br />\n"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:435
|
||||
msgid ""
|
||||
"(Either because the user <i>chose</i> not to insert them, or because all "
|
||||
"duplicates already had an EPUB format)"
|
||||
msgstr ""
|
||||
"(Porque o utilizador <i>escolheu</i> não os inserir, ou porque todos os "
|
||||
"duplicados já tinham um formato EPUB)"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:444
|
||||
msgid "<p><b>Format imports cancelled by user:</b> {}</p>\n"
|
||||
msgstr "<p><b>Importação do formato cancelada pelo utilizador:</b> {}</p>\n"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:458
|
||||
msgid "Unknown Book Title"
|
||||
msgstr "Título do livro desconhecido"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:460
|
||||
msgid "it couldn't be decrypted."
|
||||
msgstr "não pode ser desencriptado."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:462
|
||||
msgid ""
|
||||
"user CHOSE not to insert the new EPUB format, or all existing calibre "
|
||||
"entries HAD an EPUB format already."
|
||||
msgstr ""
|
||||
"o utilizador ESCOLHEU não inserir o novo formato EPUB, ou todas as entradas "
|
||||
"existentes no calibre já tinham um formato EPUB."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:464
|
||||
msgid "of unknown reasons. Gosh I'm embarrassed!"
|
||||
msgstr "de razões desconhecidas. Estou envergonhado!"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:465
|
||||
msgid "<p>{0} not added because {1}"
|
||||
msgstr "<p>{0} não adicionado porque {1}"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:226
|
||||
msgid "Help"
|
||||
msgstr "Help"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:235
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:214
|
||||
msgid "Restart required"
|
||||
msgstr "Reinicio requerido"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:236
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:215
|
||||
msgid ""
|
||||
"Title image not found - you must restart Calibre before using this plugin!"
|
||||
msgstr ""
|
||||
"Imagem do título não encontrada - tem que reiniciar o Calibre antes de "
|
||||
"utilizar este plugin!"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:322
|
||||
msgid "Undefined"
|
||||
msgstr "Não definido"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:30
|
||||
msgid "When should Obok try to insert EPUBs into existing calibre entries?"
|
||||
msgstr ""
|
||||
"Quando deve o Obok tentar inserir EPUBs em entradas já existentes no calibre?"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:33
|
||||
msgid ""
|
||||
"<p>Default behavior when duplicates are detected. None of the choices will "
|
||||
"cause calibre ebooks to be overwritten"
|
||||
msgstr ""
|
||||
"<p>Comportamento por defeito quando são detetados duplicados. Nenhuma das "
|
||||
"escolhas fará com que os livros existentes no calibre sejam reescritos"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
|
||||
msgid "Ask"
|
||||
msgstr "Pergunta"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
|
||||
msgid "Always"
|
||||
msgstr "Sempre"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
|
||||
msgid "Never"
|
||||
msgstr "Nunca"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:65
|
||||
msgid "Obok DeDRM"
|
||||
msgstr ""
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:89
|
||||
msgid "Select All"
|
||||
msgstr "Selecionar todos"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:90
|
||||
msgid "Select all books to add them to the calibre library."
|
||||
msgstr "Selecionar todos os livros para adicioná-los à biblioteca do calibre."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:92
|
||||
msgid "All with DRM"
|
||||
msgstr "Todos com DRM"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:93
|
||||
msgid "Select all books with DRM."
|
||||
msgstr "Selecionar todos os livros com DRM"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:95
|
||||
msgid "All DRM free"
|
||||
msgstr "Todos sem DRM"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:96
|
||||
msgid "Select all books without DRM."
|
||||
msgstr "Selecionar todos os livros sem DRM."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
|
||||
msgid "Title"
|
||||
msgstr "Título"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
|
||||
msgid "Author"
|
||||
msgstr "Autor"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
|
||||
msgid "Series"
|
||||
msgstr "Série"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:369
|
||||
msgid "Copy to clipboard"
|
||||
msgstr "Copiar para a área de transferência"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:397
|
||||
msgid "View Report"
|
||||
msgstr "Ver relatório"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\__init__.py:21
|
||||
msgid "Removes DRM from Kobo kepubs and adds them to the library."
|
||||
msgstr "Remove o DRM dos kepubs Kobo e adiciona-os à biblioteca."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:162
|
||||
msgid "AES improper key used"
|
||||
msgstr "AES chave imprópria usada"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:167
|
||||
msgid "Failed to initialize AES key"
|
||||
msgstr "Falha na inicialização da chave AES"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:175
|
||||
msgid "AES decryption failed"
|
||||
msgstr "A desencriptação da chave AES falhou"
|
||||
@@ -1,17 +1,17 @@
|
||||
obok_plugin.zip
|
||||
================
|
||||
|
||||
This plugin will remove the DRM from Kobo ebooks download on Mac or Windows using the Kobo desktop application.
|
||||
This plugin will remove the DRM from Kobo ebooks download on Mac or Windows using the Kobo desktop application, or from Kobo ebooks on an attached E-Ink Kobo reader (but not a Kobo Arc or Kobo Vox). If both are available, ebooks will be read from the attached E-Ink Kobo reader. To import from the desktop application, unplug the Kobo reader.
|
||||
|
||||
|
||||
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 (obok_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 "obok_calibre_plugin", find the file "obok_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
|
||||
-------------
|
||||
No customization is required, except choosing which menus will show the plugin.
|
||||
No customization is required, except choosing which menus will show the plugin. Altough the ability to enter a device serial number is given, this should not need to be filled in, as the serial number should be picked up automatically from the attached Kobo reader.
|
||||
|
||||
|
||||
Using the plugin
|
||||
@@ -47,6 +47,7 @@ Credits
|
||||
-------
|
||||
The original obok script was by Physisticated
|
||||
The plugin conversion was done anonymously.
|
||||
The Kobo reader support was added by norbusan
|
||||
|
||||
Improvements to the script and the plugin adaption have been by numerous people since.
|
||||
Additional improvements to the script and the plugin adaption by numerous anonymous people.
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# adobekey.pyw, version 5.7
|
||||
# adobekey.pyw, version 6.0
|
||||
# Copyright © 2009-2010 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 2010–2016 by several people
|
||||
|
||||
# Windows users: Before running this program, you must first install Python.
|
||||
# We recommend ActiveState Python 2.7.X for Windows (x86) from
|
||||
|
||||
@@ -16,16 +16,18 @@ from __future__ import with_statement
|
||||
# - 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.4'
|
||||
__version__ = '1.5'
|
||||
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
import getopt
|
||||
import tempfile
|
||||
import zlib
|
||||
@@ -220,20 +222,30 @@ def get_serials2(path=STORAGE2):
|
||||
userdata_keys = cursor.fetchall()
|
||||
dsns = []
|
||||
for userdata_row in userdata_keys:
|
||||
if userdata_row:
|
||||
userdata_utf8 = userdata_row[0].encode('utf8')
|
||||
if len(userdata_utf8) > 0:
|
||||
dsns.append(userdata_utf8)
|
||||
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:
|
||||
if userdata_row:
|
||||
userdata_utf8 = userdata_row[0].encode('utf8')
|
||||
if len(userdata_utf8) > 0:
|
||||
tokens.append(userdata_utf8)
|
||||
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 = []
|
||||
@@ -377,7 +389,6 @@ def gui_main():
|
||||
import Tkconstants
|
||||
import tkMessageBox
|
||||
import tkFileDialog
|
||||
import traceback
|
||||
except:
|
||||
print "Tkinter not installed"
|
||||
return cli_main()
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
from __future__ import with_statement
|
||||
|
||||
# kindlekey.py
|
||||
# Copyright © 2010-2015 by some_updates, Apprentice Alf and Apprentice Harper
|
||||
# Copyright © 2008-2017 Apprentice Harper et al.
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '2.5'
|
||||
|
||||
# Revision history:
|
||||
# 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc.
|
||||
@@ -19,15 +22,18 @@ from __future__ import with_statement
|
||||
# 1.8 - Fixes for Kindle for Mac, and non-ascii in Windows user names
|
||||
# 1.9 - Fixes for Unicode in Windows user names
|
||||
# 2.0 - Added comments and extra fix for non-ascii Windows user names
|
||||
# 2.1 - Fixed Kindle for PC encryption changes March 2016
|
||||
# 2.2 - Fixes for Macs with bonded ethernet ports
|
||||
# Also removed old .kinfo file support (pre-2011)
|
||||
# 2.3 - Added more field names thanks to concavegit's KFX code.
|
||||
# 2.4 - Fix for complex Mac disk setups, thanks to Tibs
|
||||
# 2.5 - Final Fix for Windows user names with non-ascii characters, thanks to oneofusoneofus
|
||||
|
||||
|
||||
"""
|
||||
Retrieve Kindle for PC/Mac user key.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '1.9'
|
||||
|
||||
import sys, os, re
|
||||
from struct import pack, unpack, unpack_from
|
||||
import json
|
||||
@@ -882,10 +888,18 @@ if iswindows:
|
||||
if errcd == 234:
|
||||
# bad wine implementation up through wine 1.3.21
|
||||
return "AlternateUserName"
|
||||
# double the buffer size
|
||||
buffer = create_unicode_buffer(len(buffer) * 2)
|
||||
size.value = len(buffer)
|
||||
# return low byte of the unicode value of each character of the username
|
||||
return buffer.value.encode('utf-16-le')[::2]
|
||||
|
||||
# replace any non-ASCII values with 0xfffd
|
||||
for i in xrange(0,len(buffer)):
|
||||
if buffer[i]>u"\u007f":
|
||||
#print u"swapping char "+str(i)+" ("+buffer[i]+")"
|
||||
buffer[i] = u"\ufffd"
|
||||
# return utf-8 encoding of modified username
|
||||
#print u"modified username:"+buffer.value
|
||||
return buffer.value.encode('utf-8')
|
||||
return GetUserName
|
||||
GetUserName = GetUserName()
|
||||
|
||||
@@ -926,7 +940,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 +1007,128 @@ 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',\
|
||||
'SerialNumber',\
|
||||
'UsernameHash',\
|
||||
'kindle.directedid.info',\
|
||||
'DSN',\
|
||||
'kindle.accounttype.info',\
|
||||
'krx.flashcardsplugin.data.encryption_key',\
|
||||
'krx.notebookexportplugin.data.encryption_key',\
|
||||
'proxy.http.password',\
|
||||
'proxy.http.username'
|
||||
]
|
||||
DB = {}
|
||||
with open(kInfoFile, 'rb') as infoReader:
|
||||
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:
|
||||
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 +1260,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 +1271,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 ')
|
||||
(devpart, mpath) = resline.split(' on ')[:2]
|
||||
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 +1297,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 +1314,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 +1344,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 +1360,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 +1384,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 +1452,122 @@ 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',\
|
||||
'SerialNumber',\
|
||||
'UsernameHash',\
|
||||
'kindle.directedid.info',\
|
||||
'DSN'
|
||||
]
|
||||
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 +1631,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 +1661,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):
|
||||
|
||||
385
Other_Tools/Kobo/obok.py
Normal file → Executable file
385
Other_Tools/Kobo/obok.py
Normal file → Executable file
@@ -1,6 +1,48 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Version 3.2.5 December 2016
|
||||
# Improve detection of good text decryption.
|
||||
#
|
||||
# Version 3.2.4 December 2016
|
||||
# Remove incorrect support for Kobo Desktop under Wine
|
||||
#
|
||||
# Version 3.2.3 October 2016
|
||||
# Fix for windows network user and more xml fixes
|
||||
#
|
||||
# Version 3.2.2 October 2016
|
||||
# Change to the way the new database version is handled.
|
||||
#
|
||||
# Version 3.2.1 September 2016
|
||||
# Update for v4.0 of Windows Desktop app.
|
||||
#
|
||||
# Version 3.2.0 January 2016
|
||||
# Update for latest version of Windows Desktop app.
|
||||
# Support Kobo devices in the command line version.
|
||||
#
|
||||
# Version 3.1.9 November 2015
|
||||
# Handle Kobo Desktop under wine on Linux
|
||||
#
|
||||
# Version 3.1.8 November 2015
|
||||
# Handle the case of Kobo Arc or Vox device (i.e. don't crash).
|
||||
#
|
||||
# Version 3.1.7 October 2015
|
||||
# Handle the case of no device or database more gracefully.
|
||||
#
|
||||
# Version 3.1.6 September 2015
|
||||
# Enable support for Kobo devices
|
||||
# More character encoding fixes (unicode strings)
|
||||
#
|
||||
# Version 3.1.5 September 2015
|
||||
# Removed requirement that a purchase has been made.
|
||||
# Also add in character encoding fixes
|
||||
#
|
||||
# Version 3.1.4 September 2015
|
||||
# Updated for version 3.17 of the Windows Desktop app.
|
||||
#
|
||||
# Version 3.1.3 August 2015
|
||||
# Add translations for Portuguese and Arabic
|
||||
#
|
||||
# Version 3.1.2 January 2015
|
||||
# Add coding, version number and version announcement
|
||||
#
|
||||
@@ -109,7 +151,8 @@
|
||||
#
|
||||
"""Manage all Kobo books, either encrypted or DRM-free."""
|
||||
|
||||
__version__ = '3.1.1'
|
||||
__version__ = '3.2.4'
|
||||
__about__ = u"Obok v{0}\nCopyright © 2012-2016 Physisticated et al.".format(__version__)
|
||||
|
||||
import sys
|
||||
import os
|
||||
@@ -123,6 +166,19 @@ import hashlib
|
||||
import xml.etree.ElementTree as ET
|
||||
import string
|
||||
import shutil
|
||||
import argparse
|
||||
import tempfile
|
||||
|
||||
can_parse_xml = True
|
||||
try:
|
||||
from xml.etree import ElementTree as ET
|
||||
# print u"using xml.etree for xml parsing"
|
||||
except ImportError:
|
||||
can_parse_xml = False
|
||||
# print u"Cannot find xml.etree, disabling extraction of serial numbers"
|
||||
|
||||
# List of all known hash keys
|
||||
KOBO_HASH_KEYS = ['88b3a2e13', 'XzUhGYdFp', 'NoCanLook','QJhwzAtXL']
|
||||
|
||||
class ENCRYPTIONError(Exception):
|
||||
pass
|
||||
@@ -209,6 +265,24 @@ def _load_crypto():
|
||||
|
||||
AES = _load_crypto()
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
|
||||
class KoboLibrary(object):
|
||||
"""The Kobo library.
|
||||
|
||||
@@ -216,28 +290,111 @@ class KoboLibrary(object):
|
||||
written by the Kobo Desktop Edition application, including the list
|
||||
of books, their titles, and the user's encryption key(s)."""
|
||||
|
||||
def __init__ (self):
|
||||
print u"Obok v{0}\nCopyright © 2012-2014 Physisticated et al.".format(__version__)
|
||||
if sys.platform.startswith('win'):
|
||||
if sys.getwindowsversion().major > 5:
|
||||
self.kobodir = os.environ['LOCALAPPDATA']
|
||||
else:
|
||||
self.kobodir = os.path.join(os.environ['USERPROFILE'], 'Local Settings', 'Application Data')
|
||||
self.kobodir = os.path.join(self.kobodir, 'Kobo', 'Kobo Desktop Edition')
|
||||
elif sys.platform.startswith('darwin'):
|
||||
self.kobodir = os.path.join(os.environ['HOME'], 'Library', 'Application Support', 'Kobo', 'Kobo Desktop Edition')
|
||||
self.bookdir = os.path.join(self.kobodir, 'kepub')
|
||||
kobodb = os.path.join(self.kobodir, 'Kobo.sqlite')
|
||||
self.__sqlite = sqlite3.connect(kobodb)
|
||||
self.__cursor = self.__sqlite.cursor()
|
||||
self._userkeys = []
|
||||
self._books = []
|
||||
self._volumeID = []
|
||||
def __init__ (self, serials = [], device_path = None):
|
||||
print __about__
|
||||
self.kobodir = u""
|
||||
kobodb = u""
|
||||
|
||||
# Order of checks
|
||||
# 1. first check if a device_path has been passed in, and whether
|
||||
# we can find the sqlite db in the respective place
|
||||
# 2. if 1., and we got some serials passed in (from saved
|
||||
# settings in calibre), just use it
|
||||
# 3. if 1. worked, but we didn't get serials, try to parse them
|
||||
# from the device, if this didn't work, unset everything
|
||||
# 4. if by now we don't have kobodir set, give up on device and
|
||||
# try to use the Desktop app.
|
||||
|
||||
# step 1. check whether this looks like a real device
|
||||
if (device_path):
|
||||
# we got a device path
|
||||
self.kobodir = os.path.join(device_path, u".kobo")
|
||||
# devices use KoboReader.sqlite
|
||||
kobodb = os.path.join(self.kobodir, u"KoboReader.sqlite")
|
||||
if (not(os.path.isfile(kobodb))):
|
||||
# device path seems to be wrong, unset it
|
||||
device_path = u""
|
||||
self.kobodir = u""
|
||||
kobodb = u""
|
||||
|
||||
if (self.kobodir):
|
||||
# step 3. we found a device but didn't get serials, try to get them
|
||||
if (len(serials) == 0):
|
||||
# we got a device path but no saved serial
|
||||
# try to get the serial from the device
|
||||
# print u"get_device_settings - device_path = {0}".format(device_path)
|
||||
# get serial from device_path/.adobe-digital-editions/device.xml
|
||||
if can_parse_xml:
|
||||
devicexml = os.path.join(device_path, '.adobe-digital-editions', 'device.xml')
|
||||
# print u"trying to load {0}".format(devicexml)
|
||||
if (os.path.exists(devicexml)):
|
||||
# print u"trying to parse {0}".format(devicexml)
|
||||
xmltree = ET.parse(devicexml)
|
||||
for node in xmltree.iter():
|
||||
if "deviceSerial" in node.tag:
|
||||
serial = node.text
|
||||
# print u"found serial {0}".format(serial)
|
||||
serials.append(serial)
|
||||
break
|
||||
else:
|
||||
# print u"cannot get serials from device."
|
||||
device_path = u""
|
||||
self.kobodir = u""
|
||||
kobodb = u""
|
||||
|
||||
if (self.kobodir == u""):
|
||||
# step 4. we haven't found a device with serials, so try desktop apps
|
||||
if sys.platform.startswith('win'):
|
||||
import _winreg as winreg
|
||||
if sys.getwindowsversion().major > 5:
|
||||
if 'LOCALAPPDATA' in os.environ.keys():
|
||||
# Python 2.x does not return unicode env. Use Python 3.x
|
||||
self.kobodir = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
|
||||
if (self.kobodir == u""):
|
||||
if 'USERPROFILE' in os.environ.keys():
|
||||
# Python 2.x does not return unicode env. Use Python 3.x
|
||||
self.kobodir = os.path.join(winreg.ExpandEnvironmentStrings(u"%USERPROFILE%"), u"Local Settings", u"Application Data")
|
||||
self.kobodir = os.path.join(self.kobodir, u"Kobo", u"Kobo Desktop Edition")
|
||||
elif sys.platform.startswith('darwin'):
|
||||
self.kobodir = os.path.join(os.environ['HOME'], u"Library", u"Application Support", u"Kobo", u"Kobo Desktop Edition")
|
||||
#elif linux_path != None:
|
||||
# Probably Linux, let's get the wine prefix and path to Kobo.
|
||||
# self.kobodir = os.path.join(linux_path, u"Local Settings", u"Application Data", u"Kobo", u"Kobo Desktop Edition")
|
||||
# desktop versions use Kobo.sqlite
|
||||
kobodb = os.path.join(self.kobodir, u"Kobo.sqlite")
|
||||
# check for existence of file
|
||||
if (not(os.path.isfile(kobodb))):
|
||||
# give up here, we haven't found anything useful
|
||||
self.kobodir = u""
|
||||
kobodb = u""
|
||||
|
||||
|
||||
if (self.kobodir != u""):
|
||||
self.bookdir = os.path.join(self.kobodir, u"kepub")
|
||||
# make a copy of the database in a temporary file
|
||||
# so we can ensure it's not using WAL logging which sqlite3 can't do.
|
||||
self.newdb = tempfile.NamedTemporaryFile(mode='wb', delete=False)
|
||||
print self.newdb.name
|
||||
olddb = open(kobodb, 'rb')
|
||||
self.newdb.write(olddb.read(18))
|
||||
self.newdb.write('\x01\x01')
|
||||
olddb.read(2)
|
||||
self.newdb.write(olddb.read())
|
||||
olddb.close()
|
||||
self.newdb.close()
|
||||
self.__sqlite = sqlite3.connect(self.newdb.name)
|
||||
self.__cursor = self.__sqlite.cursor()
|
||||
self._userkeys = []
|
||||
self._books = []
|
||||
self._volumeID = []
|
||||
self._serials = serials
|
||||
|
||||
def close (self):
|
||||
"""Closes the database used by the library."""
|
||||
self.__cursor.close()
|
||||
self.__sqlite.close()
|
||||
# delete the temporary copy of the database
|
||||
os.remove(self.newdb.name)
|
||||
|
||||
@property
|
||||
def userkeys (self):
|
||||
@@ -246,9 +403,8 @@ class KoboLibrary(object):
|
||||
"""
|
||||
if len(self._userkeys) != 0:
|
||||
return self._userkeys
|
||||
userid = self.__getuserid()
|
||||
for macaddr in self.__getmacaddrs():
|
||||
self._userkeys.append(self.__getuserkey(macaddr, userid))
|
||||
self._userkeys.extend(self.__getuserkeys(macaddr))
|
||||
return self._userkeys
|
||||
|
||||
@property
|
||||
@@ -274,14 +430,15 @@ class KoboLibrary(object):
|
||||
|
||||
def __bookfile (self, volumeid):
|
||||
"""The filename needed to open a given book."""
|
||||
return os.path.join(self.kobodir, 'kepub', volumeid)
|
||||
return os.path.join(self.kobodir, u"kepub", volumeid)
|
||||
|
||||
def __getmacaddrs (self):
|
||||
"""The list of all MAC addresses on this machine."""
|
||||
macaddrs = []
|
||||
if sys.platform.startswith('win'):
|
||||
c = re.compile('\s(' + '[0-9a-f]{2}-' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
|
||||
for line in os.popen('ipconfig /all'):
|
||||
(p_in, p_out, p_err) = os.popen3('ipconfig /all')
|
||||
for line in p_out:
|
||||
m = c.search(line)
|
||||
if m:
|
||||
macaddrs.append(re.sub("-", ":", m.group(1)).upper())
|
||||
@@ -290,17 +447,44 @@ class KoboLibrary(object):
|
||||
output = subprocess.check_output('/sbin/ifconfig -a', shell=True)
|
||||
matches = c.findall(output)
|
||||
for m in matches:
|
||||
# print "m:",m[0]
|
||||
# print u"m:{0}".format(m[0])
|
||||
macaddrs.append(m[0].upper())
|
||||
else:
|
||||
# probably linux, let's try ipconfig under wine
|
||||
c = re.compile('\s(' + '[0-9a-f]{2}-' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
|
||||
for line in os.popen('ipconfig /all'):
|
||||
m = c.search(line)
|
||||
if m:
|
||||
macaddrs.append(re.sub("-", ":", m.group(1)).upper())
|
||||
|
||||
# extend the list of macaddrs in any case with the serials
|
||||
# cannot hurt ;-)
|
||||
macaddrs.extend(self._serials)
|
||||
|
||||
return macaddrs
|
||||
|
||||
def __getuserid (self):
|
||||
return self.__cursor.execute('SELECT UserID FROM user WHERE HasMadePurchase = "true"').fetchone()[0]
|
||||
|
||||
def __getuserkey (self, macaddr, userid):
|
||||
deviceid = hashlib.sha256('NoCanLook' + macaddr).hexdigest()
|
||||
userkey = hashlib.sha256(deviceid + userid).hexdigest()
|
||||
return binascii.a2b_hex(userkey[32:])
|
||||
def __getuserids (self):
|
||||
userids = []
|
||||
cursor = self.__cursor.execute('SELECT UserID FROM user')
|
||||
row = cursor.fetchone()
|
||||
while row is not None:
|
||||
try:
|
||||
userid = row[0]
|
||||
userids.append(userid)
|
||||
except:
|
||||
pass
|
||||
row = cursor.fetchone()
|
||||
return userids
|
||||
|
||||
def __getuserkeys (self, macaddr):
|
||||
userids = self.__getuserids()
|
||||
userkeys = []
|
||||
for hash in KOBO_HASH_KEYS:
|
||||
deviceid = hashlib.sha256(hash + macaddr).hexdigest()
|
||||
for userid in userids:
|
||||
userkey = hashlib.sha256(deviceid + userid).hexdigest()
|
||||
userkeys.append(binascii.a2b_hex(userkey[32:]))
|
||||
return userkeys
|
||||
|
||||
class KoboBook(object):
|
||||
"""A Kobo book.
|
||||
@@ -404,16 +588,62 @@ class KoboFile(object):
|
||||
Returns True if the content was checked, False if it was not
|
||||
checked."""
|
||||
if self.mimetype == 'application/xhtml+xml':
|
||||
if contents[:5]=="<?xml":
|
||||
# assume utf-8 with no BOM
|
||||
textoffset = 0
|
||||
stride = 1
|
||||
print u"Checking text:{0}:".format(contents[:10])
|
||||
# check for byte order mark
|
||||
if contents[:3]=="\xef\xbb\xbf":
|
||||
# seems to be utf-8 with BOM
|
||||
print u"Could be utf-8 with BOM"
|
||||
textoffset = 3
|
||||
elif contents[:2]=="\xfe\xff":
|
||||
# seems to be utf-16BE
|
||||
print u"Could be utf-16BE"
|
||||
textoffset = 3
|
||||
stride = 2
|
||||
elif contents[:2]=="\xff\xfe":
|
||||
# seems to be utf-16LE
|
||||
print u"Could be utf-16LE"
|
||||
textoffset = 2
|
||||
stride = 2
|
||||
else:
|
||||
print u"Perhaps utf-8 without BOM"
|
||||
|
||||
# now check that the first few characters are in the ASCII range
|
||||
for i in xrange(textoffset,textoffset+5*stride,stride):
|
||||
if ord(contents[i])<32 or ord(contents[i])>127:
|
||||
# Non-ascii, so decryption probably failed
|
||||
print u"Bad character at {0}, value {1}".format(i,ord(contents[i]))
|
||||
raise ValueError
|
||||
print u"Seems to be good text"
|
||||
return True
|
||||
if contents[:5]=="<?xml" or contents[:8]=="\xef\xbb\xbf<?xml":
|
||||
# utf-8
|
||||
return True
|
||||
elif contents[:14]=="\xfe\xff\x00<\x00?\x00x\x00m\x00l":
|
||||
# utf-16BE
|
||||
return True
|
||||
elif contents[:14]=="\xff\xfe<\x00?\x00x\x00m\x00l\x00":
|
||||
# utf-16LE
|
||||
return True
|
||||
elif contents[:9]=="<!DOCTYPE" or contents[:12]=="\xef\xbb\xbf<!DOCTYPE":
|
||||
# utf-8 of weird <!DOCTYPE start
|
||||
return True
|
||||
elif contents[:22]=="\xfe\xff\x00<\x00!\x00D\x00O\x00C\x00T\x00Y\x00P\x00E":
|
||||
# utf-16BE of weird <!DOCTYPE start
|
||||
return True
|
||||
elif contents[:22]=="\xff\xfe<\x00!\x00D\x00O\x00C\x00T\x00Y\x00P\x00E\x00":
|
||||
# utf-16LE of weird <!DOCTYPE start
|
||||
return True
|
||||
else:
|
||||
print "Bad XML: ",contents[:5]
|
||||
print u"Bad XML: {0}".format(contents[:8])
|
||||
raise ValueError
|
||||
if self.mimetype == 'image/jpeg':
|
||||
elif self.mimetype == 'image/jpeg':
|
||||
if contents[:3] == '\xff\xd8\xff':
|
||||
return True
|
||||
else:
|
||||
print "Bad JPEG: ", contents[:3].encode('hex')
|
||||
print u"Bad JPEG: {0}".format(contents[:3].encode('hex'))
|
||||
raise ValueError()
|
||||
return False
|
||||
|
||||
@@ -435,36 +665,19 @@ class KoboFile(object):
|
||||
contents = contents[:-padding]
|
||||
return contents
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
lib = KoboLibrary()
|
||||
|
||||
for i, book in enumerate(lib.books):
|
||||
print ('%d: %s' % (i + 1, book.title)).encode('ascii', 'ignore')
|
||||
|
||||
num_string = raw_input("Convert book number... ")
|
||||
try:
|
||||
num = int(num_string)
|
||||
book = lib.books[num - 1]
|
||||
except (ValueError, IndexError):
|
||||
exit()
|
||||
|
||||
print "Converting", book.title
|
||||
|
||||
def decrypt_book(book, lib):
|
||||
print u"Converting {0}".format(book.title)
|
||||
zin = zipfile.ZipFile(book.filename, "r")
|
||||
# make filename out of Unicode alphanumeric and whitespace equivalents from title
|
||||
outname = "%s.epub" % (re.sub('[^\s\w]', '', book.title, 0, re.UNICODE))
|
||||
|
||||
outname = u"{0}.epub".format(re.sub('[^\s\w]', '_', book.title, 0, re.UNICODE))
|
||||
if (book.type == 'drm-free'):
|
||||
print "DRM-free book, conversion is not needed"
|
||||
print u"DRM-free book, conversion is not needed"
|
||||
shutil.copyfile(book.filename, outname)
|
||||
print "Book saved as", os.path.join(os.getcwd(), outname)
|
||||
exit(0)
|
||||
|
||||
print u"Book saved as {0}".format(os.path.join(os.getcwd(), outname))
|
||||
return 0
|
||||
result = 1
|
||||
for userkey in lib.userkeys:
|
||||
# print "Trying key: ",userkey.encode('hex_codec')
|
||||
confirmedGood = False
|
||||
print u"Trying key: {0}".format(userkey.encode('hex_codec'))
|
||||
try:
|
||||
zout = zipfile.ZipFile(outname, "w", zipfile.ZIP_DEFLATED)
|
||||
for filename in zin.namelist():
|
||||
@@ -473,18 +686,62 @@ if __name__ == '__main__':
|
||||
file = book.encryptedfiles[filename]
|
||||
contents = file.decrypt(userkey, contents)
|
||||
# Parse failures mean the key is probably wrong.
|
||||
if not confirmedGood:
|
||||
confirmedGood = file.check(contents)
|
||||
file.check(contents)
|
||||
zout.writestr(filename, contents)
|
||||
zout.close()
|
||||
print "Book saved as", os.path.join(os.getcwd(), outname)
|
||||
print u"Decryption succeeded."
|
||||
print u"Book saved as {0}".format(os.path.join(os.getcwd(), outname))
|
||||
result = 0
|
||||
break
|
||||
except ValueError:
|
||||
print "Decryption failed, trying next key"
|
||||
print u"Decryption failed."
|
||||
zout.close()
|
||||
os.remove(outname)
|
||||
|
||||
zin.close()
|
||||
return result
|
||||
|
||||
|
||||
def cli_main():
|
||||
description = __about__
|
||||
epilog = u"Parsing of arguments failed."
|
||||
parser = argparse.ArgumentParser(prog=sys.argv[0], description=description, epilog=epilog)
|
||||
parser.add_argument('--devicedir', default='/media/KOBOeReader', help="directory of connected Kobo device")
|
||||
parser.add_argument('--all', action='store_true', help="flag for converting all books on device")
|
||||
args = vars(parser.parse_args())
|
||||
serials = []
|
||||
devicedir = u""
|
||||
if args['devicedir']:
|
||||
devicedir = args['devicedir']
|
||||
|
||||
lib = KoboLibrary(serials, devicedir)
|
||||
|
||||
if args['all']:
|
||||
books = lib.books
|
||||
else:
|
||||
for i, book in enumerate(lib.books):
|
||||
print u"{0}: {1}".format(i + 1, book.title)
|
||||
print u"Or 'all'"
|
||||
|
||||
choice = raw_input(u"Convert book number... ")
|
||||
if choice == u'all':
|
||||
books = list(lib.books)
|
||||
else:
|
||||
try:
|
||||
num = int(choice)
|
||||
books = [lib.books[num - 1]]
|
||||
except (ValueError, IndexError):
|
||||
print u"Invalid choice. Exiting..."
|
||||
exit()
|
||||
|
||||
results = [decrypt_book(book, lib) for book in books]
|
||||
lib.close()
|
||||
exit(result)
|
||||
overall_result = all(result != 0 for result in results)
|
||||
if overall_result != 0:
|
||||
print u"Could not decrypt book with any of the keys found."
|
||||
return overall_result
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,10 +1,4 @@
|
||||
How-to:
|
||||
1) Make sure you can read all PDF files on Scuolabook Reader.
|
||||
2) Run Scuolabook DRM Remover.
|
||||
3) Get your free books from the directory where you started the program.
|
||||
The latest Scuolabook tool can be found at Hex's own blog:
|
||||
https://thisishex.wordpress.com/scuolabook-drm-remover/
|
||||
|
||||
Note:
|
||||
It is recommended to use Scuolabook version 2.0.1 and refuse all updates
|
||||
because the encryption algorithm may change making this tool useless.
|
||||
|
||||
Hex
|
||||
Harper.
|
||||
@@ -1,7 +1,7 @@
|
||||
# DeDRM_tools
|
||||
DeDRM tools for ebooks
|
||||
|
||||
This is a repository of all the scripts and other tools for removing DRM from ebooks that I could find, commited in date order as best as I could manage. (Except for the Requiem tools for Apple's iBooks, and Convert LIT for Microsoft's .lit ebooks.)
|
||||
This is a repository of all the scripts and other tools for removing DRM from ebooks that I could find, committed in date order as best as I could manage. (Except for the Requiem tools for Apple's iBooks, and Convert LIT for Microsoft's .lit ebooks.)
|
||||
|
||||
Mostly it tracks the tools releases by Apprentice Alf, athough it also includes the individual tools and their histories from before Alf had a blog.
|
||||
|
||||
|
||||
@@ -1,63 +1,70 @@
|
||||
Welcome to the tools!
|
||||
=====================
|
||||
|
||||
This ReadMe_First.txt is meant to give users a quick overview of what is available and how to get started. This document is part of the Tools v6.3.1 archive from Apprentice Alf's Blog: http://apprenticealf.wordpress.com/
|
||||
This ReadMe_First.txt is meant to give users a quick overview of what is available and how to get started. This document is part of the Tools v6.6.0 archive from Apprentice Harper's github repository: https://github.com/apprenticeharper/DeDRM_tools/
|
||||
|
||||
The is archive includes tools to remove DRM from:
|
||||
|
||||
- Kindle ebooks (from Kindle for Mac/PC, eInk Kindles and Kindle for Android).
|
||||
- Barnes and Noble ePubs
|
||||
- Adobe Digital Editions ePubs (including Kobo and Google ePubs downloaded to ADE)
|
||||
- Kindle ebooks (files from Kindle for Mac/PC* and eInk Kindles**).
|
||||
- Adobe Digital Editions (v2.0.1***) ePubs (including Kobo and Google ePubs downloaded to ADE)
|
||||
- Kobo kePubs from the Kobo Desktop application
|
||||
- Adobe Digital Editions PDFs
|
||||
- Barnes and Noble ePubs
|
||||
- Adobe Digital Editions (v2.0.1) PDFs
|
||||
- Scuolabooks (Link to solution by Hex)
|
||||
- Mobipocket ebooks
|
||||
- eReader PDB books
|
||||
- Scuolabooks (Windows only solution by Hex)
|
||||
- eReader PDB ebooks
|
||||
- Rocket ebooks (source only)
|
||||
|
||||
These tools do NOT work with Apple's iBooks FairPlay DRM (see end of this file.)
|
||||
|
||||
* With Kindle for PC/Mac 1.19 and later, Amazon included support for their new KFX format. While the tools now include a first attempt at supporting drm removal for KFX format, we recommend using Kindle for PC/Mac 1.17 or earlier which prevents downloads of the new format, as conversions from the olde KF8 format are likely to be more successful.
|
||||
|
||||
** Some later Kindles support Amazon's new KFX format. And some books download in a split azw3/azw6 format. For best results, instead of using files downloaded directly to your Kindle, download from Amazon's web site 'for transfer via USB'. This will give you an single file to import. See also the FAQ entry about this.
|
||||
|
||||
*** 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 2017.
|
||||
|
||||
About the tools
|
||||
---------------
|
||||
These tools are updated and maintained by Apprentice Alf and Apprentice Harper. You can find links to the latest updates and get support at Apprentice Alf's blog: http://www.apprenticealf.wordpress.com/
|
||||
If you re-post these tools, a link to the blog would be appreciated.
|
||||
These tools are updated and maintained by Apprentice Harper and many others. You can find the latest updates at Apprentice Harper's github repository https://github.com/apprenticeharper/DeDRM_tools/ and get support by creating an issue at the repository (github account required) or by posting a comment at Apprentice Alf's blog: http://www.apprenticealf.wordpress.com/
|
||||
|
||||
If you re-post these tools, a link to the repository and/or the blog would be appreciated.
|
||||
|
||||
|
||||
DeDRM plugin for calibre (Mac OS X, Windows, and Linux)
|
||||
-------------------------------------------------------
|
||||
Calibre is an open source freeware ebook library manager. It is the best tool around for keeping track of your ebooks. The DeDRM plugin for calibre provides the simplest way, especially on Windows, to remove DRM from your ebooks. Just install the DeDRM plugin from the DeDRM_calibre_plugin folder, following the instructions and configuration directions provided in the ReadMe and the help links.
|
||||
Calibre is an open source freeware ebook library manager. It is the best tool around for keeping track of your ebooks. The DeDRM plugin for calibre provides the simplest way, especially on Windows, to remove DRM from your Kindle and Adobe DRM ebooks. Just install the DeDRM plugin from the DeDRM_calibre_plugin folder, following the instructions and configuration directions provided in the ReadMe file and the help links in the plugin's configuration dialogs.
|
||||
|
||||
Once installed and configured, you can simply add a DRM book to calibre and the DeDRMed version will be imported into the calibre database. Note that DRM removal only occurs on IMPORT not on CONVERSION or at any other time, not even conversion to other formats. If you have already imported DRM books you'll need to remove them from calibre and re-import them.
|
||||
Once installed and configured, you can simply add a DRM book to calibre and the DRM-free version will be imported into the calibre database. Note that DRM removal only occurs on IMPORT not on CONVERSION or at any other time. If you have already imported DRM books you'll need to remove them from calibre and re-import them.
|
||||
|
||||
Linux users should read the section at the end the DeDRM_plugin_ReadMe.txt file.
|
||||
For instructions, see the DeDRM_plugin_ReadMe.txt file in the DeDRM_calibre_plugin folder.
|
||||
|
||||
|
||||
Obok plugin for calibre (Mac OS X and Windows)
|
||||
----------------------------------------------
|
||||
To import ebooks from the Kobo Desktop app or from a Kobo ebook reader, install the Obok plugin. This works in a different way to the DeDRM plugin, in that it finds your ebooks downloaded using the Kobo Desktop app, or on an attached Kobo ebooks reader, and displays them in a list, so that you can choose the ones you want to import into calibre.
|
||||
|
||||
For instructions, see the obok_plugin_ReadMe.txt file in the Obok_calibre_plugin folder.
|
||||
|
||||
|
||||
DeDRM application for Mac OS X users: (Mac OS X 10.4 and above)
|
||||
---------------------------------------------------------------
|
||||
This application is a stand-alone DRM removal application for Mac OS X users.
|
||||
This application is a stand-alone DRM removal application for Mac OS X users. It is only needed for people who cannot or will not use the calibre plugin. KFX support has not been tested yet.
|
||||
|
||||
For instructions, see the "DeDRM ReadMe.rtf" file in the DeDRM_Macintosh_Application folder.
|
||||
|
||||
N.B. Mac OS X 10.4 users need to take extra steps before using the application, see the ReadMe.
|
||||
|
||||
|
||||
DeDRM application for Windows users: (Windows XP through Windows 8)
|
||||
DeDRM application for Windows users: (Windows XP through Windows 10)
|
||||
------------------------------------------------------------------
|
||||
***This program requires that Python and PyCrypto be properly installed.***
|
||||
***See below for details on recommended versions and how to install them.***
|
||||
|
||||
This application is a stand-alone application for Windows users.
|
||||
This application is a stand-alone application for Windows users. It is only needed for people who cannot or will not use the calibre plugin. KFX support has not been tested yet.
|
||||
|
||||
For instructions, see the DeDRM_App_ReadMe.txt file in the DeDRM_Windows_Applications folder.
|
||||
|
||||
|
||||
Obok plugin for calibre (Mac OS X, Windows)
|
||||
-------------------------------------------
|
||||
This plugin allows you to import kePub ebooks from your kobo desktop application. It is separate from the DeDRM application because it has to work in a different way. It will find all the books downloaded to your kobo desktop application, and let you choose which ones to import, removing the DRM as they are imported.
|
||||
|
||||
For instructions, see the obok_plugin_ReadMe.txt file in the Obok_calibre_plugin folder.
|
||||
|
||||
|
||||
Other_Tools
|
||||
-----------
|
||||
This is a folder of other tools that may be useful for DRMed ebooks from certain sources or for Linux users. Most users won't need any of these tools.
|
||||
@@ -66,10 +73,10 @@ B_and_N_Download_Helper
|
||||
A Javascript to enable a download button at the B&N website for ebooks that normally won't download to your PC. Only for the adventurous.
|
||||
|
||||
DRM_Key_Scripts
|
||||
This folder contains python scripts that create or extract or fetch encryption keyfiles for Barnes and Noble ePubs, Adobe Digital Editions ePubs, Kindle for Mac/PC and Kindle for Android ebooks.
|
||||
This folder contains python scripts that create or extract or fetch encryption keyfiles for Barnes and Noble, Adobe Digital Editions, Kindle for Mac/PC and old versons of Kindle for Android.
|
||||
|
||||
Kindle_for_Android_Patches
|
||||
Definitely only for the adventurous, this folder contains information on how to modify the Kindle for Android app to b able to get a PID for use with the other Kindle tools (DeDRM apps and calibre plugin). This is now of historical interest only, as Android support has now been added to the tools more simply.
|
||||
Definitely only for the adventurous, this folder contains information on how to modify the Kindle for Android app to b able to get a PID for use with the other Kindle tools (DeDRM apps and calibre plugin).
|
||||
|
||||
Kobo
|
||||
Contains the standalone obok python script for removing DRM from kePubs downloaded using the kobo desktop application.
|
||||
@@ -78,39 +85,27 @@ Rocket_ebooks
|
||||
Information about the now-obsolete Rocket ebook format and DRM, along with source for a tool to remove the DRM.
|
||||
|
||||
Scuolabook_DRM
|
||||
A windows-only application (including source code) for removing DRM from ScuolaBooks PDFs, created by "Hex" and included with permission.
|
||||
A link to the tool for removing DRM from ScuolaBooks PDFs, created by "Hex".
|
||||
|
||||
|
||||
Windows and Python
|
||||
------------------
|
||||
We **strongly** recommend ActiveState's Active Python 2.7 Community Edition for Windows. This can be downloaded for free from:
|
||||
We **strongly** recommend using calibre and the plugin.
|
||||
|
||||
If you really want to use the WIndows app or the individual scripts, you'll need to install python.
|
||||
ActiveState's Active Python 2.7 Community Edition for Windowscan be downloaded for free from:
|
||||
|
||||
http://www.activestate.com/activepython/downloads
|
||||
|
||||
We do **NOT** recommend the version of Python from python.org as it is missing various Windows specific libraries, does not install the Tk Widget kit (for graphical user interfaces) by default, and does not properly update the system PATH environment variable. Therefore using the default python.org build on Windows is simply an exercise in frustration for most Windows users.
|
||||
|
||||
In addition, Windows Users need one of PyCrypto OR OpenSSL. Because of potential conflicts with other software, we recommend using PyCrypto.
|
||||
|
||||
For PyCrypto:
|
||||
In addition, Windows Users need PyCrypto:
|
||||
|
||||
There are many places to get PyCrypto installers for Windows. One such place is:
|
||||
|
||||
http://www.voidspace.org.uk/python/modules.shtml
|
||||
|
||||
Please get the latest (currently 2.6) PyCrypto meant for Windows Python version 2.7
|
||||
Please get the latest (currently 2.6) PyCrypto meant for Windows Python version 2.7. Note that the PyCrypto binaries have two version numbers. The first is the PyCrypto version, and the second is the python version that they work with. This can be confusing.
|
||||
|
||||
For OpenSSL:
|
||||
|
||||
Win32 OpenSSL v0.9.8o (8Mb)
|
||||
http://www.slproweb.com/download/Win32OpenSSL-0_9_8o.exe
|
||||
(if you get an error message about missing Visual C++
|
||||
redistributables... cancel the install and install the
|
||||
below support program from Microsoft, THEN install OpenSSL)
|
||||
|
||||
Visual C++ 2008 Redistributables (1.7Mb)
|
||||
http://www.microsoft.com/downloads/details.aspx?familyid=9B2DA534-3E03-4391-8A4D-074B9F2BC1BF
|
||||
|
||||
Once Windows users have installed Python 2.7, and the matching OpenSSL OR PyCrypto pieces, they are ready to run the DeDRM application or individual scripts.
|
||||
Once Windows users have installed Python 2.7, and the matching PyCrypto, they are ready to run the DeDRM application or individual scripts.
|
||||
|
||||
|
||||
|
||||
@@ -134,7 +129,7 @@ MD5: 1636862796d573c693d56bcc526b60bd
|
||||
|
||||
If you have any problems with Requiem, I suggest you contact Brahms directly through their Tor website.
|
||||
|
||||
No support for requiem is provided at Apprentice Alf's blog.
|
||||
No support for requiem is provided at Apprentice Alf's blog or Apprentice Harper's github repository.
|
||||
|
||||
|
||||
Credits
|
||||
@@ -143,6 +138,8 @@ 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 KFX format decryption was by lulzkabulz, converted to python by Apprentice Naomi and integrated into the tools by tomthumb1997
|
||||
|
||||
The original obok script was by Physisticated
|
||||
|
||||
The alfcrypto library is by some_updates
|
||||
|
||||
Reference in New Issue
Block a user