summaryrefslogtreecommitdiffstats
path: root/DeDRM_Macintosh_Application
diff options
context:
space:
mode:
authorApprentice Alf <[email protected]>2013-03-20 10:23:54 +0000
committerApprentice Alf <[email protected]>2015-03-07 14:34:21 +0000
commit20bc936e99ffefe1f7481bf77e543548412e4e74 (patch)
treeb2bd35eaae15c5a917c98407cd17f4b70d75c999 /DeDRM_Macintosh_Application
parent748bd2d4711ac35934f53a319e84cbf66df7314f (diff)
tools v6.0.0
The first unified calibre plugin
Diffstat (limited to 'DeDRM_Macintosh_Application')
-rw-r--r--DeDRM_Macintosh_Application/DeDRM ReadMe.rtf46
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app.txtbin59910 -> 131522 bytes
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist8
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Adobe Digital Editions Key_Help.htm55
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Barnes and Noble Key_Help.htm57
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_EInk Kindle Serial Number_Help.htm43
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Help.htm73
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Kindle for Mac and PC Key_Help.htm55
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Mobipocket PID_Help.htm42
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_eReader Key_Help.htm56
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/Scripts/main.scptbin273522 -> 293024 bytes
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/__init__.py450
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/adobekey.py (renamed from DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptkey.py)184
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/config.py482
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/convert2xml.py11
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/description.rtfd/TXT.rtf2
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/dialogs.py719
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/epubtest.py4
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/erdr2pml.py15
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/genbook.py8
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/getk4pcpids.py84
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignobleepub.py49
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignoblekeygen.py29
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptepub.py49
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py53
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mutils.py747
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4pcutils.py457
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kgenpids.py71
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kindlekey.py1893
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py2
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/openssl_des.py2
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/plugin-import-name-dedrm.txt0
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/scrolltextwidget.py27
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/subasyncio.py148
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/topazextract.py14
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/utilities.py39
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/zipfilerugged.py2
-rw-r--r--DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/zipfix.py53
38 files changed, 4323 insertions, 1706 deletions
diff --git a/DeDRM_Macintosh_Application/DeDRM ReadMe.rtf b/DeDRM_Macintosh_Application/DeDRM ReadMe.rtf
index 4c3219a..8813e94 100644
--- a/DeDRM_Macintosh_Application/DeDRM ReadMe.rtf
+++ b/DeDRM_Macintosh_Application/DeDRM ReadMe.rtf
@@ -1,23 +1,23 @@
-{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360
-{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
+\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fnil\fcharset0 Verdana;\f2\fnil\fcharset128 HiraKakuProN-W3;
+}
{\colortbl;\red255\green255\blue255;}
-\paperw11900\paperh16840\margl1440\margr1440\vieww12360\viewh16560\viewkind0
-\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\qc\pardirnatural
+\paperw11900\paperh16840\margl1440\margr1440\vieww18160\viewh16520\viewkind0
+\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\qc
\f0\b\fs24 \cf0 DeDRM ReadMe
\b0 \
-\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\ql\qnatural\pardirnatural
+\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural
\cf0 \
-\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\qj\pardirnatural
+\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\qj
\cf0 DeDRM is an application that packs all of the python dm-removal software into one easy to use program that remembers preferences and settings.\
-It works without manual configuration with Kindle for Mac ebooks and Adobe Adept ePub and PDF ebooks.\
+It works without manual configuration with Kindle for Mac ebooks and Adobe Adept (including nook) ePub and PDF ebooks.\
\
-To remove the DRM of Kindle ebooks from eInk Kindles, eReader pdb ebooks, Barnes&Noble ePubs, or Mobipocket ebooks, you must first run DeDRM application (by double-clicking it) and set some additional Preferences including:\
+To remove the DRM of Kindle ebooks from eInk Kindles, eReader pdb ebooks, Barnes & Noble ePubs, or Mobipocket ebooks, you must first run DeDRM application (by double-clicking it) and set some additional Preferences including:\
\
eInk Kindle (not Kindle Fire): 16 digit Serial Number\
-Kindle for iOS: 40 digit UDID, but this probably won't work anymore\
Barnes & Noble ePub: Name and CC number or key file (bnepubkey.b64)\
-eReader Social DRM: Name and last 8 digits of CC number\
+eReader: Name and last 8 digits of CC number\
Mobipocket: 10 digit PID\
\
A final preference is the destination folder for the DRM-free copies of your ebooks that the application produces. This can be either the same folder as the original ebook, or a folder of your choice.\
@@ -27,7 +27,7 @@ Once these preferences have been set, you can drag and drop ebooks (or folders o
This program requires Mac OS X 10.4 or above. It will not work on Mac OS X 10.3 or earlier.\
\
\
-\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\ql\qnatural\pardirnatural
+\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural
\b \cf0 Installation
\b0 \
@@ -41,7 +41,7 @@ Mac OS X 10.5 and above: You do
\i not
\i0 need to install Python.\
\
-Drag the DeDRM application from from tools_v5.6.2\\DeDRM_Applications\\Macintosh (the location of this ReadMe) to your Applications folder, or anywhere else you find convenient.\
+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.\
\
\
@@ -53,6 +53,24 @@ Drag the DeDRM application from from tools_v5.6.2\\DeDRM_Applications\\Macintosh
\
\b Troubleshooting\
-\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\ql\qnatural\pardirnatural
+\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural
-\b0 \cf0 A log is created on your desktop containing detailed information from all the scripts. If you have any problems decrypting your ebooks, quote the contents of this log in a comment at Apprentice Alf's blog.} \ No newline at end of file
+\b0 \cf0 A log is created on your desktop (DeDRM.log) containing detailed information from all the scripts. If you have any problems decrypting your ebooks, copy the contents of this log in a comment at Apprentice Alf's blog.\
+http://apprenticealf.wordpress.com/\
+\
+\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural
+
+\b \cf0 Credits\
+\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural
+
+\b0 \cf0 The mobidedrm and erdr2pml scripts were created by The Dark Reverser\
+The i
+\f1 \CocoaLigature0 gnobleepub, ignoblekeygen, ineptepub and adobe key scripts were created by
+\f0 \CocoaLigature1 i
+\f2 \CocoaLigature0 \uc0\u9829
+\f1 cabbages\
+The k4mobidedrm script and supporting scripts were written by some_updates with help from DiapDealer and Apprentice Alf, based on code by Bart Simpson (aka Skindle), CMBDTC and clarknova \
+The alfcrypto library was created by some_updates\
+The ePub encryption detection script was adapted by Apprentice Alf from a script by Paul Durrant\
+The DeDRM all-in-one AppleScript was created by Apprentice Alf\
+} \ No newline at end of file
diff --git a/DeDRM_Macintosh_Application/DeDRM.app.txt b/DeDRM_Macintosh_Application/DeDRM.app.txt
index cb177cb..249a927 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app.txt
+++ b/DeDRM_Macintosh_Application/DeDRM.app.txt
Binary files differ
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist
index 5d79f98..646912c 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist
@@ -24,17 +24,17 @@
<key>CFBundleExecutable</key>
<string>droplet</string>
<key>CFBundleGetInfoString</key>
- <string>DeDRM 5.6.2. AppleScript written 2010–2013 by Apprentice Alf and others.</string>
+ <string>DeDRM AppleScript 6.0.0. Written 2010–2013 by Apprentice Alf and others.</string>
<key>CFBundleIconFile</key>
<string>DeDRM</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
- <string>DeDRM 5.6.2</string>
+ <string>DeDRM AppleScript</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
- <string>5.6.2</string>
+ <string>6.0.0</string>
<key>CFBundleSignature</key>
<string>dplt</string>
<key>LSRequiresCarbon</key>
@@ -50,7 +50,7 @@
<key>positionOfDivider</key>
<real>0</real>
<key>savedFrame</key>
- <string>1846 -16 800 473 1440 -180 1920 1080 </string>
+ <string>1616 -35 765 818 1440 -180 1920 1080 </string>
<key>selectedTabView</key>
<string>event log</string>
</dict>
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Adobe Digital Editions Key_Help.htm b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Adobe Digital Editions Key_Help.htm
new file mode 100644
index 0000000..ee9edb2
--- /dev/null
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Adobe Digital Editions Key_Help.htm
@@ -0,0 +1,55 @@
+<!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 Adobe Digital Editions 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 Adobe Digital Editions Keys</h1>
+
+
+<p>If you have upgraded from an earlier version of the plugin, any existing Adobe Digital Editions keys will have been automatically imported, so you might not need to do any more configuration. In addition, on Windows and Mac, the default Adobe Digital Editions key is added the first time the plugin is run. Continue reading for key generation and management instructions.</p>
+
+<h3>Creating New Keys:</h3>
+
+<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog prompting you to enter a key name for the default Adobe Digital Editions key. </p>
+<ul>
+<li><span class="bold">Unique Key Name:</span> this is a unique name you choose to help you identify the key. This name will show in the list of configured keys.</li>
+</ul>
+
+<p>Click the OK button to create and store the Adobe Digital Editions key for the current installation of Adobe Digital Editions. Or Cancel if you don’t want to create the key.</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 promt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</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 ‘.der’ file name extension). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.</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 existing ‘.der’ key files. Key files might come from being exported from this or older plugins, or may have been generated using the adobekey.pyw script running under Wine on Linux systems.</p>
+
+<p>Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.</p>
+
+</body>
+
+</html>
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Barnes and Noble Key_Help.htm b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Barnes and Noble Key_Help.htm
new file mode 100644
index 0000000..ac1b693
--- /dev/null
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Barnes and Noble Key_Help.htm
@@ -0,0 +1,57 @@
+<!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 Barnes and Noble 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 Barnes and Noble Keys</h1>
+
+
+<p>If you have upgraded from an earlier version of the plugin, any existing Barnes and Noble keys will have been automatically imported, so you might not need to do any more configuration. Continue reading for key generation and management instructions.</p>
+
+<h3>Creating New Keys:</h3>
+
+<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering the necessary data to generate a new key.</p>
+<ul>
+<li><span class="bold">Unique Key Name:</span> this is a unique name you choose to help you identify the key. This name will show in the list of configured keys. Choose something that will help you remember the data (name, cc#) it was created with.</li>
+<li><span class="bold">Your Name:</span> This is the name used by Barnes and Noble to generate your encryption key. Seemingly at random, Barnes and Noble choose one of three places from which to take this name. Most commonly, it’s your name as set in your Barnes &amp; Noble account, My Account page, directly under PERSONAL INFORMATION. Sometimes it is the the name used in the default shipping address, and sometimes it’s the name listed for the active credit card. If these names are different in your Barnes and Noble account preferences, I suggest creating one key for each version of your name. This name will not be stored anywhere on your computer or in calibre. It will only be used in the creation of the one-way hash/key that’s stored in the preferences.</li>
+<li><span class="bold">Credit Card#:</span> this is the default credit card number that was on file with Barnes and Noble at the time of download of the ebook to be de-DRMed. Just enter the 16 (15 for American Express) digits. As with the name, this number will not be stored anywhere on your computer or in calibre. It will only be used in the creation of the one-way hash/key that’s stored in the preferences.</li>
+</ul>
+
+<p>Click the OK button to create and store the generated key. Or Cancel if you don’t want to create a key.</p>
+<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 promt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</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 ‘.b64’ file name extension). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.</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 existing ‘.b64’ key files. Key files might come from being exported from this or older plugins, or may have been generated using the original i♥cabbages script.</p>
+
+<p>Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.</p>
+
+</body>
+
+</html>
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_EInk Kindle Serial Number_Help.htm b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_EInk Kindle Serial Number_Help.htm
new file mode 100644
index 0000000..e79abd7
--- /dev/null
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_EInk Kindle Serial Number_Help.htm
@@ -0,0 +1,43 @@
+<!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 eInk Kindle 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 eInk Kindle serial numbers</h1>
+
+<p>If you have upgraded from an earlier version of the plugin, any existing eInk Kindle serial numbers will have been automatically imported, so you might not need to do any more configuration.</p>
+
+<p>Please note that Kindle serial numbers are only valid keys for eInk Kindles like the Kindle Touch and PaperWhite. The Kindle Fire and Fire HD do not use their serial number for DRM and it is useless to enter those serial numbers.</p>
+
+<h3>Creating New Kindle 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 green plus sign (+). Clicking this button will open a new dialog for entering a new Kindle serial number.</p>
+<ul>
+<li><span class="bold">Eink Kindle Serial Number:</span> this is the unique serial number of your device. It usually starts with a ‘B’ or a ‘9’ and is sixteen characters long. For a reference of where to find serial numbers and their ranges, please refere to this <a href="http://wiki.mobileread.com/wiki/Kindle_serial_numbers">mobileread wiki page.</a></li>
+</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 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>
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Help.htm b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Help.htm
new file mode 100644
index 0000000..69edade
--- /dev/null
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Help.htm
@@ -0,0 +1,73 @@
+<!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>DeDRM Plugin Configuration</title>
+<style type="text/css">
+span.version {font-size: 50%}
+span.bold {font-weight: bold}
+h3 {margin-bottom: 0}
+p {margin-top: 0}
+
+</style>
+</head>
+
+<body>
+
+<h1>DeDRM Plugin <span class="version">(v6.0.0)</span></h1>
+
+<p>This plugin removes DRM from ebooks when they are imported into calibre. If you already have DRMed ebooks in your calibre library, you will need to remove them and import them again.</p>
+
+<h3>Installation</h3>
+<p>You have obviously managed to install the plugin, as otherwise you wouldn’t be reading this help file. However, you should also delete any older DeDRM plugins, as this DeDRM plugin replaces the five older plugins: Kindle and Mobipocket DeDRM (K4MobiDeDRM), Ignoble Epub DeDRM (ignobleepub), Inept Epub DeDRM (ineptepub), Inept PDF DeDRM (ineptepub) and eReader PDB 2 PML (eReaderPDB2PML).</p>
+
+<h3>Configuration</h3>
+<p>On Windows and Mac, the keys for ebooks downloaded for Kindle for Mac/PC and Adobe Digital Editions are automatically generated. If all your DRMed ebooks can be opened and read in Kindle for Mac/PC and/or Adobe Digital Editions on the same computer on which you are running calibre, you do not need to do any configuration of this plugin. On Linux, keys for Kindle for PC and Adobe Digital Editions need to be generated separately (see the Linux section below)</p>
+
+<p>If you have other DRMed ebooks, you will need to enter extra configuration information. The buttons in this dialog will open individual configuration dialogs that will allow you to enter the needed information, depending on the type and source of your DRMed eBooks. Additional help on the information required is available in each of the the dialogs.</p>
+
+<p>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.</p>
+
+<p>When you have finished entering your configuration information, you <em>must</em> click the OK button to save it. If you click the Cancel button, all your changes in all the configuration dialogs will be lost.</p>
+
+<h3>Troubleshooting:</h3>
+
+<p >If you find that it’s not working for you , you can save a lot of time by trying to add the ebook to Calibre in debug mode. This will print out a lot of helpful info that can be copied into any online help requests.</p>
+
+<p>Open a command prompt (terminal window) and type "calibre-debug -g" (without the quotes). Calibre will launch, and you can can add the problem ebook the usual way. The debug info will be output to the original command prompt (terminal window). Copy the resulting output and paste it into the comment you make at my blog.</p>
+<p><span class="bold">Note:</span> The Mac version of Calibre doesn’t install the command line tools by default. If you go to the ‘Preferences’ page and click on the miscellaneous button, you’ll find the option to install the command line tools.</p>
+
+<h3>Credits:</h3>
+<ul>
+<li>The Dark Reverser for the Mobipocket and eReader scripts</li>
+<li>i♥cabbages for the Adobe Digital Editions scripts</li>
+<li>Skindle aka Bart Simpson for the Amazon Kindle for PC script</li>
+<li>CMBDTC for Amazon Topaz DRM removal script</li>
+<li>some_updates, clarknova and Bart Simpson for Amazon Topaz conversion scripts</li>
+<li>DiapDealer for the first calibre plugin versions of the tools</li>
+<li>some_updates, DiapDealer, Apprentice Alf and mdlnx for Amazon Kindle/Mobipocket tools</li>
+<li>some_updates for the DeDRM all-in-one Python tool</li>
+<li>Apprentice Alf for the DeDRM all-in-one AppleScript tool</li>
+<li>Apprentice Alf for the DeDRM all-in-one calibre plugin</li>
+<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>
+
+<h2>Linux Systems Only</h2>
+<h3>Generating decryption keys for Adobe Digital Editions and Kindle for PC</h3>
+<p>If you install Kindle for PC and/or Adobe Digital Editions in Wine, you will be able to download DRMed ebooks to them under Wine. To be able to remove the DRM, you will need to generate key files and add them in the plugin's customisation dialogs.</p>
+
+<p>To generate the key files you will need to install Python and PyCrypto under the same Wine setup as your Kindle for PC and/or Adobe Digital Editions installations. (Kindle for PC, Python and Pycrypto installation instructions in the ReadMe.)</p>
+
+<p>Once everything's installed under Wine, you'll need to run the adobekey.pyw script (for Adobe Digital Editions) and kindlekey.pyw (For Kindle for PC) using the python installation in your Wine system. The scripts can be found in Other_Tools/Key_Retrieval_Scripts.</p>
+
+<p>Each script will create a key file in the same folder as the script. Copy the key files to your Linux system and then load the key files using the Adobe Digital Editions ebooks dialog and the Kindle for Mac/PC ebooks dialog.</p>
+
+
+</body>
+
+</html>
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Kindle for Mac and PC Key_Help.htm b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Kindle for Mac and PC Key_Help.htm
new file mode 100644
index 0000000..c714581
--- /dev/null
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Kindle for Mac and PC Key_Help.htm
@@ -0,0 +1,55 @@
+<!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 Mac/PC 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 Mac/PC Keys</h1>
+
+
+<p>If you have upgraded from an earlier version of the plugin, any existing Kindle for Mac/PC keys will have been automatically imported, so you might not need to do any more configuration. In addition, on Windows and Mac, the default Kindle for Mac/PC key is added the first time the plugin is run. Continue reading for key generation and management instructions.</p>
+
+<h3>Creating New Keys:</h3>
+
+<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog prompting you to enter a key name for the default Kindle for Mac/PC key. </p>
+<ul>
+<li><span class="bold">Unique Key Name:</span> this is a unique name you choose to help you identify the key. This name will show in the list of configured keys.</li>
+</ul>
+
+<p>Click the OK button to create and store the Kindle for Mac/PC key for the current installation of Kindle for Mac/PC. Or Cancel if you don’t want to create the key.</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 promt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</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 ‘.der’ file name extension). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.</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 existing ‘.k4i’ key files. Key files might come from being exported from this plugin, or may have been generated using the kindlekey.pyw script running under Wine on Linux systems.</p>
+
+<p>Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.</p>
+
+</body>
+
+</html>
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Mobipocket PID_Help.htm b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Mobipocket PID_Help.htm
new file mode 100644
index 0000000..00aeeca
--- /dev/null
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Mobipocket PID_Help.htm
@@ -0,0 +1,42 @@
+<!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 Mobipocket PIDs</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 Mobipocket PIDs</h1>
+
+<p>If you have upgraded from an earlier version of the plugin, any existing Mobipocket PIDs will have been automatically imported, so you might not need to do any more configuration.</p>
+
+
+<h3>Creating New Mobipocket PIDs:</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 Mobipocket PID.</p>
+<ul>
+<li><span class="bold">PID:</span> this is a PID used to decrypt your Mobipocket ebooks. It is eight or ten characters long. Mobipocket PIDs are usualy displayed in the About screen of your Mobipocket device.</li>
+</ul>
+
+<p>Click the OK button to save the PID. Or Cancel if you didn’t want to enter a PID.</p>
+
+<h3>Deleting Mobipocket PIDs:</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 Mobipocket PID from the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.</p>
+
+<p>Once done creating/deleting PIDs, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.</p>
+
+</body>
+
+</html>
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_eReader Key_Help.htm b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_eReader Key_Help.htm
new file mode 100644
index 0000000..c1c78ad
--- /dev/null
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_eReader Key_Help.htm
@@ -0,0 +1,56 @@
+<!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 eReader 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 eReader Keys</h1>
+
+<p>If you have upgraded from an earlier version of the plugin, any existing eReader (Fictionwise ‘.pdb’) keys will have been automatically imported, so you might not need to do any more configuration. Continue reading for key generation and management instructions.</p>
+
+<h3>Creating New Keys:</h3>
+
+<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering the necessary data to generate a new key.</p>
+<ul>
+<li><span class="bold">Unique Key Name:</span> this is a unique name you choose to help you identify the key. This name will show in the list of configured keys. Choose something that will help you remember the data (name, cc#) it was created with.</li>
+<li><span class="bold">Your Name:</span> This is the name used by Fictionwise to generate your encryption key. Since Fictionwise has now closed down, you might not have easy access to this. It was often the name on the Credit Card used at Fictionwise.</li>
+<li><span class="bold">Credit Card#:</span> this is the default credit card number that was on file with Fictionwise at the time of download of the ebook to be de-DRMed. Just enter the last 8 digits of the number. As with the name, this number will not be stored anywhere on your computer or in calibre. It will only be used in the creation of the one-way hash/key that’s stored in the preferences.</li>
+</ul>
+
+<p>Click the OK button to create and store the generated key. Or Cancel if you don’t want to create a key.</p>
+<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 promt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</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 ‘.b63’ file name extension). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.</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 existing ‘.b63’ key files that have previously been exported.</p>
+
+<p>Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.</p>
+
+</body>
+
+</html>
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/Scripts/main.scpt b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/Scripts/main.scpt
index a0000f4..009e52d 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/Scripts/main.scpt
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/Scripts/main.scpt
Binary files differ
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/__init__.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/__init__.py
new file mode 100644
index 0000000..a9ac2fd
--- /dev/null
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/__init__.py
@@ -0,0 +1,450 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from __future__ import with_statement
+__license__ = 'GPL v3'
+__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.
+#
+# This plugin is meant to decrypt eReader PDBs, Adobe Adept ePubs, Barnes & Noble ePubs,
+# Adobe Adept PDFs, Amazon Kindle and Mobipocket files without having to
+# install any dependencies... other than having calibre installed, of course.
+#
+# Configuration:
+# Check out the plugin's configuration settings by clicking the "Customize plugin"
+# button when you have the "DeDRM" plugin highlighted (under Preferences->
+# Plugins->File type plugins). Once you have the configuration dialog open, you'll
+# see a Help link on the top right-hand side.
+#
+# Revision history:
+# 6.0.0 - Initial release
+
+"""
+Decrypt DRMed ebooks.
+"""
+
+PLUGIN_NAME = u"DeDRM"
+PLUGIN_VERSION_TUPLE = (6, 0, 0)
+PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE])
+# Include an html helpfile in the plugin's zipfile with the following name.
+RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
+
+import sys, os, re
+import time
+import zipfile
+import traceback
+from zipfile import ZipFile
+
+class DeDRMError(Exception):
+ pass
+
+from calibre.customize import FileTypePlugin
+from calibre.constants import iswindows, isosx
+from calibre.gui2 import is_ok_to_use_qt
+from calibre.utils.config import config_dir
+
+
+# Wrap a stream so that output gets flushed immediately
+# and also make sure that any unicode strings get safely
+# encoded using "replace" before writing them.
+class SafeUnbuffered:
+ def __init__(self, stream):
+ self.stream = stream
+ self.encoding = stream.encoding
+ if self.encoding == None:
+ self.encoding = "utf-8"
+ def write(self, data):
+ if isinstance(data,unicode):
+ data = data.encode(self.encoding,"replace")
+ self.stream.write(data)
+ self.stream.flush()
+ def __getattr__(self, attr):
+ return getattr(self.stream, attr)
+
+class DeDRM(FileTypePlugin):
+ name = PLUGIN_NAME
+ description = u"Removes DRM from Amazon Kindle, Adobe Adept (including Kobo), Barnes & Noble, Mobipocket and eReader ebooks. Credit given to i♥cabbages and The Dark Reverser for the original stand-alone scripts."
+ supported_platforms = ['linux', 'osx', 'windows']
+ author = u"DiapDealer, Apprentice Alf, The Dark Reverser and i♥cabbages"
+ version = PLUGIN_VERSION_TUPLE
+ minimum_calibre_version = (0, 7, 55) # Compiled python libraries cannot be imported in earlier versions.
+ file_types = set(['epub','pdf','pdb','prc','mobi','azw','azw1','azw3','azw4','tpz'])
+ on_import = True
+ priority = 600
+
+ def initialize(self):
+ # convert old preferences, if necessary.
+ import calibre_plugins.dedrm.config
+
+ config.convertprefs()
+
+ """
+ Dynamic modules can't be imported/loaded from a zipfile... so this routine
+ runs whenever the plugin gets initialized. This will extract the appropriate
+ library for the target OS and copy it to the 'alfcrypto' subdirectory of
+ calibre's configuration directory. That 'alfcrypto' directory is then
+ inserted into the syspath (as the very first entry) in the run function
+ so the CDLL stuff will work in the alfcrypto.py script.
+ """
+ try:
+ if iswindows:
+ names = [u"alfcrypto.dll",u"alfcrypto64.dll"]
+ elif isosx:
+ names = [u"libalfcrypto.dylib"]
+ else:
+ names = [u"libalfcrypto32.so",u"libalfcrypto64.so"]
+ lib_dict = self.load_resources(names)
+ self.pluginsdir = os.path.join(config_dir,u"plugins")
+ if not os.path.exists(self.pluginsdir):
+ os.mkdir(self.pluginsdir)
+ self.maindir = os.path.join(self.pluginsdir,u"DeDRM")
+ if not os.path.exists(self.maindir):
+ os.mkdir(self.maindir)
+ self.helpdir = os.path.join(self.maindir,u"help")
+ if not os.path.exists(self.helpdir):
+ os.mkdir(self.helpdir)
+ self.alfdir = os.path.join(self.maindir,u"alfcrypto")
+ if not os.path.exists(self.alfdir):
+ os.mkdir(self.alfdir)
+ for entry, data in lib_dict.items():
+ file_path = os.path.join(self.alfdir, entry)
+ open(file_path,'wb').write(data)
+ except Exception, e:
+ traceback.print_exc()
+ raise
+
+ def ePubDecrypt(self,path_to_ebook):
+ # Create a TemporaryPersistent file to work with.
+ # Check original epub archive for zip errors.
+ import calibre_plugins.dedrm.zipfix
+
+ inf = self.temporary_file(u".epub")
+ try:
+ print u"{0} v{1}: Verifying zip archive integrity.".format(PLUGIN_NAME, PLUGIN_VERSION)
+ fr = zipfix.fixZip(path_to_ebook, inf.name)
+ fr.fix()
+ except Exception, e:
+ print u"{0} v{1}: Error \'{2}\' when checking zip archive.".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0])
+ raise Exception(e)
+
+ # import the decryption keys
+ import calibre_plugins.dedrm.config as config
+
+ # import the Barnes & Noble ePub handler
+ import calibre_plugins.dedrm.ignobleepub as ignobleepub
+
+ #check the book
+ if ignobleepub.ignobleBook(inf.name):
+ print u"{0} v{1}: “{2}” is a secure Barnes & Noble ePub.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))
+
+ # Attempt to decrypt epub with each encryption key (generated or provided).
+ for keyname, userkey in config.dedrmprefs['bandnkeys'].items():
+ keyname_masked = u"".join((u'X' if (x.isdigit()) else x) for x in keyname)
+ print u"{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname_masked)
+ of = self.temporary_file(u".epub")
+
+ # Give the user key, ebook and TemporaryPersistent file to the decryption function.
+ result = ignobleepub.decryptBook(userkey, inf.name, of.name)
+
+ of.close()
+
+ if result == 0:
+ # Decryption was successful.
+ # Return the modified PersistentTemporary file to calibre.
+ return of.name
+
+ print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime)
+
+ print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
+ raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt “{2}” after {3:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook),time.time()-self.starttime))
+
+ # import the Adobe Adept ePub handler
+ import calibre_plugins.dedrm.ineptepub as ineptepub
+
+ if ineptepub.adeptBook(inf.name):
+ print u"{0} v{1}: {2} is a secure Adobe Adept ePub.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))
+
+ # Attempt to decrypt epub with each encryption key (generated or provided).
+ for keyname, userkeyhex in config.dedrmprefs['adeptkeys'].items():
+ userkey = userkeyhex.decode('hex')
+ print u"{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname)
+ of = self.temporary_file(u".epub")
+
+ # Give the user key, ebook and TemporaryPersistent file to the decryption function.
+ try:
+ result = ineptepub.decryptBook(userkey, inf.name, of.name)
+ except:
+ result = 1
+
+ of.close()
+
+ if result == 0:
+ # Decryption was successful.
+ # Return the modified PersistentTemporary file to calibre.
+ return of.name
+
+ print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime)
+
+ # perhaps we need to get a new default ADE key
+ if iswindows or isosx:
+ print u"{0} v{1}: Looking for new default Adobe Digital Editions Keys".format(PLUGIN_NAME, PLUGIN_VERSION)
+
+ # get the default Adobe keys
+ import calibre_plugins.dedrm.adobekey as adobe
+
+ try:
+ defaultkeys = adobe.adeptkeys()
+ except:
+ defaultkeys = []
+
+ newkeys = []
+ for keyvalue in defaultkeys:
+ if keyvalue.encode('hex') not in config.dedrmprefs['adeptkeys'].values():
+ newkeys.append(keyvalue)
+
+ if len(newkeys) > 0:
+ try:
+ for i,userkey in enumerate(newkeys):
+ print u"{0} v{1}: Trying a new default key.".format(PLUGIN_NAME, PLUGIN_VERSION)
+ of = self.temporary_file(u".epub")
+
+ # Give the user key, ebook and TemporaryPersistent file to the decryption function.
+ try:
+ result = ineptepub.decryptBook(userkey, inf.name, of.name)
+ except:
+ result = 1
+
+ of.close()
+
+ if result == 0:
+ # Decryption was a success
+ # Store the new successful key in the defaults
+ print u"{0} v{1}: Saving a new default key.".format(PLUGIN_NAME, PLUGIN_VERSION)
+ try:
+ config.addnamedvaluetoprefs('adeptkeys','default_key',keyvalue.encode('hex'))
+ config.writeprefs()
+ except:
+ traceback.print_exc()
+ # Return the modified PersistentTemporary file to calibre.
+ return of.name
+
+ print u"{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
+ except Exception, e:
+ pass
+
+ # Something went wrong with decryption.
+ print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
+ raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt “{2}” after {3:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook),time.time()-self.starttime))
+
+ # Not a Barnes & Noble nor an Adobe Adept
+ # Import the fixed epub.
+ print u"{0} v{1}: “{2}” is neither an Adobe Adept nor a Barnes & Noble encrypted ePub.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))
+ return inf.name
+
+ def PDFDecrypt(self,path_to_ebook):
+ import calibre_plugins.dedrm.config as config
+ import calibre_plugins.dedrm.ineptpdf
+
+ # Attempt to decrypt epub with each encryption key (generated or provided).
+ print u"{0} v{1}: {2} is a PDF ebook.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))
+ for keyname, userkeyhex in config.dedrmprefs['adeptkeys'].items():
+ userkey = userkeyhex.decode('hex')
+ print u"{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname)
+ of = self.temporary_file(u".pdf")
+
+ # Give the user key, ebook and TemporaryPersistent file to the decryption function.
+ try:
+ result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name)
+ except:
+ result = 1
+
+ of.close()
+
+ if result == 0:
+ # Decryption was successful.
+ # Return the modified PersistentTemporary file to calibre.
+ return of.name
+
+ # perhaps we need to get a new default ADE key
+ if iswindows or isosx:
+ print u"{0} v{1}: Looking for new default Adobe Digital Editions Keys".format(PLUGIN_NAME, PLUGIN_VERSION)
+
+ # get the default Adobe keys
+ import calibre_plugins.dedrm.adobekey as adobe
+
+ try:
+ defaultkeys = adobe.adeptkeys()
+ except:
+ defaultkeys = []
+
+ newkeys = []
+ for keyvalue in defaultkeys:
+ if keyvalue.encode('hex') not in config.dedrmprefs['adeptkeys'].values():
+ newkeys.append(keyvalue)
+
+ if len(newkeys) > 0:
+ try:
+ for i,userkey in enumerate(newkeys):
+ print u"{0} v{1}: Trying a new default key.".format(PLUGIN_NAME, PLUGIN_VERSION)
+ of = self.temporary_file(u".pdf")
+
+ # Give the user key, ebook and TemporaryPersistent file to the decryption function.
+ try:
+ result = ineptepdf.decryptBook(userkey, inf.name, of.name)
+ except:
+ result = 1
+
+ of.close()
+
+ if result == 0:
+ # Decryption was a success
+ # Store the new successful key in the defaults
+ print u"{0} v{1}: Saving a new default key.".format(PLUGIN_NAME, PLUGIN_VERSION)
+ try:
+ config.addnamedvaluetoprefs('adeptkeys','default_key',keyvalue.encode('hex'))
+ config.writeprefs()
+ except:
+ traceback.print_exc()
+ # Return the modified PersistentTemporary file to calibre.
+ return of.name
+
+ print u"{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
+ except Exception, e:
+ pass
+
+ # Something went wrong with decryption.
+ print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
+ raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt “{2}” after {3:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook),time.time()-self.starttime))
+
+ def KindleMobiDecrypt(self,path_to_ebook):
+
+ # add the alfcrypto directory to sys.path so alfcrypto.py
+ # will be able to locate the custom lib(s) for CDLL import.
+ sys.path.insert(0, self.alfdir)
+ # Had to move this import here so the custom libs can be
+ # extracted to the appropriate places beforehand these routines
+ # look for them.
+ import calibre_plugins.dedrm.config as config
+ import calibre_plugins.dedrm.k4mobidedrm
+
+ pids = config.dedrmprefs['pids']
+ serials = config.dedrmprefs['serials']
+ kindleDatabases = config.dedrmprefs['kindlekeys'].items()
+
+ try:
+ book = k4mobidedrm.GetDecryptedBook(path_to_ebook,kindleDatabases,serials,pids,self.starttime)
+ except Exception, e:
+ decoded = False
+ # perhaps we need to get a new default Kindle for Mac/PC key
+ if iswindows or isosx:
+ print u"{0} v{1}: Failed to decrypt with error: {2}".format(PLUGIN_NAME, PLUGIN_VERSION,e.args[0])
+ print u"{0} v{1}: Looking for new default Kindle Key".format(PLUGIN_NAME, PLUGIN_VERSION)
+ import calibre_plugins.dedrm.kindlekey as amazon
+
+ try:
+ defaultkeys = amazon.kindlekeys()
+ except:
+ defaultkeys = []
+ newkeys = {}
+ for i,keyvalue in enumerate(defaultkeys):
+ keyname = u"default_key_{0:d}".format(i+1)
+ if keyvalue not in config.dedrmprefs['kindlekeys'].values():
+ newkeys[keyname] = keyvalue
+ if len(newkeys) > 0:
+ try:
+ book = k4mobidedrm.GetDecryptedBook(path_to_ebook,newkeys.items(),[],[],self.starttime)
+ decoded = True
+ # store the new successful keys in the defaults
+ for keyvalue in newkeys.values():
+ config.addnamedvaluetoprefs('kindlekeys','default_key',keyvalue)
+ config.writeprefs()
+ except Exception, e:
+ pass
+ if not decoded:
+ #if you reached here then no luck raise and exception
+ print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
+ traceback.print_exc()
+ raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt “{4}” after {3:.1f} seconds with error: {2}\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0],time.time()-self.starttime,os.path.basename(path_to_ebook)))
+
+ of = self.temporary_file(book.getBookExtension())
+ book.getFile(of.name)
+ of.close()
+ book.cleanup()
+ return of.name
+
+
+ def eReaderDecrypt(self,path_to_ebook):
+
+ import calibre_plugins.dedrm.config as config
+ import calibre_plugins.dedrm.erdr2pml
+
+ # Attempt to decrypt epub with each encryption key (generated or provided).
+ for keyname, userkey in config.dedrmprefs['ereaderkeys'].items():
+ keyname_masked = u"".join((u'X' if (x.isdigit()) else x) for x in keyname)
+ print u"{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname_masked)
+ of = self.temporary_file(u".pmlz")
+
+ # Give the userkey, ebook and TemporaryPersistent file to the decryption function.
+ result = erdr2pml.decryptBook(path_to_ebook, of.name, True, userkey.decode('hex'))
+
+ of.close()
+
+ # Decryption was successful return the modified PersistentTemporary
+ # file to Calibre's import process.
+ if result == 0:
+ return of.name
+
+ print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds.".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime)
+
+ print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
+ raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt “{2}” after {3:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook),time.time()-self.starttime))
+
+
+ def run(self, path_to_ebook):
+
+ # make sure any unicode output gets converted safely with 'replace'
+ sys.stdout=SafeUnbuffered(sys.stdout)
+ sys.stderr=SafeUnbuffered(sys.stderr)
+
+ print u"{0} v{1}: Trying to decrypt {2}.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))
+ self.starttime = time.time()
+
+ booktype = os.path.splitext(path_to_ebook)[1].lower()[1:]
+ if booktype in ['prc','mobi','azw','azw1','azw3','azw4','tpz']:
+ # Kindle/Mobipocket
+ decrypted_ebook = self.KindleMobiDecrypt(path_to_ebook)
+ elif booktype == 'pdb':
+ # eReader
+ decrypted_ebook = self.eReaderDecrypt(path_to_ebook)
+ pass
+ elif booktype == 'pdf':
+ # Adobe Adept PDF (hopefully)
+ decrypted_ebook = self.PDFDecrypt(path_to_ebook)
+ pass
+ elif booktype == 'epub':
+ # Adobe Adept or B&N ePub
+ decrypted_ebook = self.ePubDecrypt(path_to_ebook)
+ else:
+ print u"Unknown booktype {0}. Passing back to calibre unchanged.".format(booktype)
+ return path_to_ebook
+ print u"{0} v{1}: Successfully decrypted book after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
+ return decrypted_ebook
+
+ def is_customizable(self):
+ # return true to allow customization via the Plugin->Preferences.
+ return True
+
+ def config_widget(self):
+ import calibre_plugins.dedrm.config as config
+ return config.ConfigWidget(self.plugin_path)
+
+ def save_settings(self, config_widget):
+ config_widget.save_settings()
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptkey.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/adobekey.py
index a9bc62d..94f7522 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptkey.py
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/adobekey.py
@@ -1,25 +1,31 @@
-#! /usr/bin/python
+#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import with_statement
-# ineptkey.pyw, version 5.6
+# adobekey.pyw, version 5.7
# Copyright © 2009-2010 i♥cabbages
# Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/>
-# Windows users: Before running this program, you must first install Python 2.6
-# from <http://www.python.org/download/> and PyCrypto from
-# <http://www.voidspace.org.uk/python/modules.shtml#pycrypto> (make certain
-# to install the version for Python 2.6). Then save this script file as
-# ineptkey.pyw and double-click on it to run it. It will create a file named
-# adeptkey.der in the same directory. This is your ADEPT user key.
+# Modified 2010–2013 by some_updates, DiapDealer and Apprentice Alf
+
+# Windows users: Before running this program, you must first install Python.
+# We recommend ActiveState Python 2.7.X for Windows (x86) from
+# http://www.activestate.com/activepython/downloads.
+# You must also install PyCrypto from
+# http://www.voidspace.org.uk/python/modules.shtml#pycrypto
+# (make certain to install the version for Python 2.7).
+# Then save this script file as adobekey.pyw and double-click on it to run it.
+# It will create a file named adobekey_1.der in in the same directory as the script.
+# This is your Adobe Digital Editions user key.
#
-# Mac OS X users: Save this script file as ineptkey.pyw. You can run this
-# program from the command line (pythonw ineptkey.pyw) or by double-clicking
+# Mac OS X users: Save this script file as adobekey.pyw. You can run this
+# program from the command line (python adobekey.pyw) or by double-clicking
# it when it has been associated with PythonLauncher. It will create a file
-# named adeptkey.der in the same directory. This is your ADEPT user key.
+# named adobekey_1.der in the same directory as the script.
+# This is your Adobe Digital Editions user key.
# Revision history:
# 1 - Initial release, for Adobe Digital Editions 1.7
@@ -30,24 +36,25 @@ from __future__ import with_statement
# 4.2 - added old 1.7.1 processing
# 4.3 - better key search
# 4.4 - Make it working on 64-bit Python
-# 5 - Clean up and improve 4.x changes;
-# Clean up and merge OS X support by unknown
+# 5 - Clean up and improve 4.x changes;
+# Clean up and merge OS X support by unknown
# 5.1 - add support for using OpenSSL on Windows in place of PyCrypto
# 5.2 - added support for output of key to a particular file
# 5.3 - On Windows try PyCrypto first, OpenSSL next
# 5.4 - Modify interface to allow use of import
# 5.5 - Fix for potential problem with PyCrypto
# 5.6 - Revised to allow use in Plugins to eliminate need for duplicate code
+# 5.7 - Unicode support added, renamed adobekey from ineptkey
+# 5.8 - Added getkey interface for Windows DeDRM application
"""
Retrieve Adobe ADEPT user key.
"""
__license__ = 'GPL v3'
+__version__ = '5.8'
-import sys
-import os
-import struct
+import sys, os, struct, getopt
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
@@ -79,8 +86,8 @@ def unicode_argv():
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
- # characters with '?'.
-
+ # characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
+ # as a list of Unicode strings and encode them as utf-8
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
@@ -101,7 +108,9 @@ def unicode_argv():
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
- return [u"ineptkey.py"]
+ # if we don't have any arguments at all, just pass back script name
+ # this should never happen
+ return [u"adobekey.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
@@ -349,7 +358,7 @@ if iswindows:
return CryptUnprotectData
CryptUnprotectData = CryptUnprotectData()
- def retrieve_keys():
+ def adeptkeys():
if AES is None:
raise ADEPTError("PyCrypto or OpenSSL must be installed")
root = GetSystemDirectory().split('\\')[0] + '\\'
@@ -406,6 +415,9 @@ elif isosx:
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
def findActivationDat():
+ import warnings
+ warnings.filterwarnings('ignore', category=FutureWarning)
+
home = os.getenv('HOME')
cmdline = 'find "' + home + '/Library/Application Support/Adobe/Digital Editions" -name "activation.dat"'
cmdline = cmdline.encode(sys.getfilesystemencoding())
@@ -413,6 +425,7 @@ elif isosx:
out1, out2 = p2.communicate()
reslst = out1.split('\n')
cnt = len(reslst)
+ ActDatPath = "activation.dat"
for j in xrange(cnt):
resline = reslst[j]
pp = resline.find('activation.dat')
@@ -423,10 +436,10 @@ elif isosx:
return ActDatPath
return None
- def retrieve_keys():
+ def adeptkeys():
actpath = findActivationDat()
if actpath is None:
- raise ADEPTError("Could not locate ADE activation")
+ raise ADEPTError("Could not find ADE activation.dat file.")
tree = etree.parse(actpath)
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
expr = '//%s/%s' % (adept('credentials'), adept('privateLicenseKey'))
@@ -436,35 +449,95 @@ elif isosx:
return [userkey]
else:
- def retrieve_keys(keypath):
+ def adeptkeys():
raise ADEPTError("This script only supports Windows and Mac OS X.")
return []
-def retrieve_key(keypath):
- keys = retrieve_keys()
- with open(keypath, 'wb') as f:
- f.write(keys[0])
- return True
+# interface for Python DeDRM
+def getkey(outpath):
+ keys = adeptkeys()
+ if len(keys) > 0:
+ if not os.path.isdir(outpath):
+ outfile = outpath
+ with file(outfile, 'wb') as keyfileout:
+ keyfileout.write(keys[0])
+ print u"Saved a key to {0}".format(outfile)
+ else:
+ keycount = 0
+ for key in keys:
+ while True:
+ keycount += 1
+ outfile = os.path.join(outpath,u"adobekey_{0:d}.der".format(keycount))
+ if not os.path.exists(outfile):
+ break
+ with file(outfile, 'wb') as keyfileout:
+ keyfileout.write(key)
+ print u"Saved a key to {0}".format(outfile)
+ return True
+ return False
+
+def usage(progname):
+ print u"Finds, decrypts and saves the default Adobe Adept encryption key(s)."
+ print u"Keys are saved to the current directory, or a specified output directory."
+ print u"If a file name is passed instead of a directory, only the first key is saved, in that file."
+ print u"Usage:"
+ print u" {0:s} [-h] [<outpath>]".format(progname)
+
+def cli_main(argv=unicode_argv()):
+ progname = os.path.basename(argv[0])
+ print u"{0} v{1}\nCopyright © 2009-2013 i♥cabbages and Apprentice Alf".format(progname,__version__)
-def extractKeyfile(keypath):
try:
- success = retrieve_key(keypath)
- except ADEPTError, e:
- print u"Key generation Error: {0}".format(e.args[0])
- return 1
- except Exception, e:
- print "General Error: {0}".format(e.args[0])
- return 1
- if not success:
- return 1
+ opts, args = getopt.getopt(argv[1:], "h")
+ except getopt.GetoptError, err:
+ print u"Error in options or arguments: {0}".format(err.args[0])
+ usage(progname)
+ sys.exit(2)
+
+ for o, a in opts:
+ if o == "-h":
+ usage(progname)
+ sys.exit(0)
+
+ if len(args) > 1:
+ usage(progname)
+ sys.exit(2)
+
+ if len(args) == 1:
+ # save to the specified file or directory
+ outpath = args[0]
+ if not os.path.isabs(outpath):
+ outpath = os.path.abspath(outpath)
+ else:
+ # save to the same directory as the script
+ outpath = os.path.dirname(argv[0])
+
+ # make sure the outpath is the
+ outpath = os.path.realpath(os.path.normpath(outpath))
+
+ keys = adeptkeys()
+ if len(keys) > 0:
+ if not os.path.isdir(outpath):
+ outfile = outpath
+ with file(outfile, 'wb') as keyfileout:
+ keyfileout.write(keys[0])
+ print u"Saved a key to {0}".format(outfile)
+ else:
+ keycount = 0
+ for key in keys:
+ while True:
+ keycount += 1
+ outfile = os.path.join(outpath,u"adobekey_{0:d}.der".format(keycount))
+ if not os.path.exists(outfile):
+ break
+ with file(outfile, 'wb') as keyfileout:
+ keyfileout.write(key)
+ print u"Saved a key to {0}".format(outfile)
+ else:
+ print u"Could not retrieve Adobe Adept key."
return 0
-def cli_main(argv=unicode_argv()):
- keypath = argv[1]
- return extractKeyfile(keypath)
-
-
def gui_main(argv=unicode_argv()):
import Tkinter
import Tkconstants
@@ -485,23 +558,32 @@ def gui_main(argv=unicode_argv()):
root = Tkinter.Tk()
root.withdraw()
- keypath, progname = os.path.split(argv[0])
- keypath = os.path.join(keypath, u"adeptkey.der")
+ progpath, progname = os.path.split(argv[0])
success = False
try:
- success = retrieve_key(keypath)
- except ADEPTError, e:
- tkMessageBox.showerror(u"ADEPT Key", "Error: {0}".format(e.args[0]))
+ keys = adeptkeys()
+ keycount = 0
+ for key in keys:
+ while True:
+ keycount += 1
+ outfile = os.path.join(progpath,u"adobekey_{0:d}.der".format(keycount))
+ if not os.path.exists(outfile):
+ break
+
+ with file(outfile, 'wb') as keyfileout:
+ keyfileout.write(key)
+ success = True
+ tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile))
+ except DrmException, e:
+ tkMessageBox.showerror(progname, u"Error: {0}".format(str(e)))
except Exception:
root.wm_state('normal')
- root.title('ADEPT Key')
+ root.title(progname)
text = traceback.format_exc()
ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1)
root.mainloop()
if not success:
return 1
- tkMessageBox.showinfo(
- u"ADEPT Key", u"Key successfully retrieved to {0}".format(keypath))
return 0
if __name__ == '__main__':
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/config.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/config.py
index 9521540..04d87c6 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/config.py
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/config.py
@@ -1,62 +1,464 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
-from PyQt4.Qt import QWidget, QVBoxLayout, QLabel, QLineEdit
+from __future__ import with_statement
-from calibre.utils.config import JSONConfig
+__license__ = 'GPL v3'
+
+# Standard Python modules.
+import os, sys, re, hashlib
+
+# PyQT4 modules (part of calibre).
+from PyQt4.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit,
+ QGroupBox, QPushButton, QListWidget, QListWidgetItem,
+ QAbstractItemView, QIcon, QDialog, QUrl, QString)
+from PyQt4 import QtGui
+
+import zipfile
+from zipfile import ZipFile
+
+# calibre modules and constants.
+from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url,
+ choose_dir, choose_files)
+from calibre.utils.config import dynamic, config_dir, JSONConfig
+from calibre.constants import iswindows, isosx
+
+# modules from this plugin's zipfile.
+from calibre_plugins.dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION
+from calibre_plugins.dedrm.__init__ import RESOURCE_NAME as help_file_name
+from calibre_plugins.dedrm.utilities import (uStrCmp, DETAILED_MESSAGE)
+
+import calibre_plugins.dedrm.dialogs as dialogs
+import calibre_plugins.dedrm.ignoblekeygen as bandn
+import calibre_plugins.dedrm.erdr2pml as ereader
+import calibre_plugins.dedrm.adobekey as adobe
+import calibre_plugins.dedrm.kindlekey as amazon
+
+JSON_NAME = PLUGIN_NAME.strip().lower().replace(' ', '_')
+JSON_PATH = os.path.join(u"plugins", JSON_NAME + '.json')
+
+IGNOBLEPLUGINNAME = "Ignoble Epub DeDRM"
+EREADERPLUGINNAME = "eReader PDB 2 PML"
+OLDKINDLEPLUGINNAME = "K4PC, K4Mac, Kindle Mobi and Topaz DeDRM"
# This is where all preferences for this plugin will be stored
# You should always prefix your config file name with plugins/,
# so as to ensure you dont accidentally clobber a calibre config file
-prefs = JSONConfig('plugins/K4MobiDeDRM')
+dedrmprefs = JSONConfig(JSON_PATH)
-# Set defaults
-prefs.defaults['pids'] = ""
-prefs.defaults['serials'] = ""
-prefs.defaults['WINEPREFIX'] = None
+# get prefs from older tools
+kindleprefs = JSONConfig(os.path.join(u"plugins", u"K4MobiDeDRM"))
+ignobleprefs = JSONConfig(os.path.join(u"plugins", u"ignoble_epub_dedrm"))
+# Set defaults for the prefs
+dedrmprefs.defaults['configured'] = False
+dedrmprefs.defaults['bandnkeys'] = {}
+dedrmprefs.defaults['adeptkeys'] = {}
+dedrmprefs.defaults['ereaderkeys'] = {}
+dedrmprefs.defaults['kindlekeys'] = {}
+dedrmprefs.defaults['pids'] = []
+dedrmprefs.defaults['serials'] = []
-class ConfigWidget(QWidget):
- def __init__(self):
+class ConfigWidget(QWidget):
+ def __init__(self, plugin_path):
QWidget.__init__(self)
- self.l = QVBoxLayout()
- self.setLayout(self.l)
- self.serialLabel = QLabel('eInk Kindle Serial numbers (First character B, 16 characters, use commas if more than one)')
- self.l.addWidget(self.serialLabel)
+ self.plugin_path = plugin_path
+
+ # get copy of the prefs from the file
+ # Otherwise we seem to get a persistent local copy.
+ self.dedrmprefs = JSONConfig(JSON_PATH)
- self.serials = QLineEdit(self)
- self.serials.setText(prefs['serials'])
- self.l.addWidget(self.serials)
- self.serialLabel.setBuddy(self.serials)
+ self.tempdedrmprefs = {}
+ self.tempdedrmprefs['bandnkeys'] = self.dedrmprefs['bandnkeys'].copy()
+ self.tempdedrmprefs['adeptkeys'] = self.dedrmprefs['adeptkeys'].copy()
+ self.tempdedrmprefs['ereaderkeys'] = self.dedrmprefs['ereaderkeys'].copy()
+ self.tempdedrmprefs['kindlekeys'] = self.dedrmprefs['kindlekeys'].copy()
+ self.tempdedrmprefs['pids'] = list(self.dedrmprefs['pids'])
+ self.tempdedrmprefs['serials'] = list(self.dedrmprefs['serials'])
- self.pidLabel = QLabel('Mobipocket PIDs (8 or 10 characters, use commas if more than one)')
- self.l.addWidget(self.pidLabel)
+ # Start Qt Gui dialog layout
+ layout = QVBoxLayout(self)
+ self.setLayout(layout)
- self.pids = QLineEdit(self)
- self.pids.setText(prefs['pids'])
- self.l.addWidget(self.pids)
- self.pidLabel.setBuddy(self.serials)
+ help_layout = QHBoxLayout()
+ layout.addLayout(help_layout)
+ # Add hyperlink to a help file at the right. We will replace the correct name when it is clicked.
+ help_label = QLabel('<a href="http://www.foo.com/">Plugin Help</a>', self)
+ help_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
+ help_label.setAlignment(Qt.AlignRight)
+ help_label.linkActivated.connect(self.help_link_activated)
+ help_layout.addWidget(help_label)
- self.wpLabel = QLabel('For Linux only: WINEPREFIX (enter absolute path)')
- self.l.addWidget(self.wpLabel)
+ keys_group_box = QGroupBox(_('Configuration:'), self)
+ layout.addWidget(keys_group_box)
+ keys_group_box_layout = QHBoxLayout()
+ keys_group_box.setLayout(keys_group_box_layout)
- self.wineprefix = QLineEdit(self)
- wineprefix = prefs['WINEPREFIX']
- if wineprefix is not None:
- self.wineprefix.setText(wineprefix)
- else:
- self.wineprefix.setText('')
- self.l.addWidget(self.wineprefix)
- self.wpLabel.setBuddy(self.wineprefix)
+ button_layout = QVBoxLayout()
+ keys_group_box_layout.addLayout(button_layout)
+ self.bandn_button = QtGui.QPushButton(self)
+ self.bandn_button.setToolTip(_(u"Click to manage keys for Barnes and Noble ebooks"))
+ self.bandn_button.setText(u"Barnes and Noble ebooks")
+ self.bandn_button.clicked.connect(self.bandn_keys)
+ self.kindle_serial_button = QtGui.QPushButton(self)
+ self.kindle_serial_button.setToolTip(_(u"Click to manage eInk Kindle serial numbers for Kindle ebooks"))
+ self.kindle_serial_button.setText(u"eInk Kindle ebooks")
+ self.kindle_serial_button.clicked.connect(self.kindle_serials)
+ self.kindle_key_button = QtGui.QPushButton(self)
+ self.kindle_key_button.setToolTip(_(u"Click to manage keys for Kindle for Mac/PC ebooks"))
+ self.kindle_key_button.setText(u"Kindle for Mac/PC ebooks")
+ self.kindle_key_button.clicked.connect(self.kindle_keys)
+ self.adept_button = QtGui.QPushButton(self)
+ self.adept_button.setToolTip(_(u"Click to manage keys for Adobe Digital Editions ebooks"))
+ self.adept_button.setText(u"Adobe Digital Editions ebooks")
+ self.adept_button.clicked.connect(self.adept_keys)
+ self.mobi_button = QtGui.QPushButton(self)
+ self.mobi_button.setToolTip(_(u"Click to manage PIDs for Mobipocket ebooks"))
+ self.mobi_button.setText(u"Mobipocket ebooks")
+ self.mobi_button.clicked.connect(self.mobi_keys)
+ self.ereader_button = QtGui.QPushButton(self)
+ self.ereader_button.setToolTip(_(u"Click to manage keys for eReader ebooks"))
+ self.ereader_button.setText(u"eReader ebooks")
+ self.ereader_button.clicked.connect(self.ereader_keys)
+ button_layout.addWidget(self.kindle_serial_button)
+ button_layout.addWidget(self.bandn_button)
+ button_layout.addWidget(self.mobi_button)
+ button_layout.addWidget(self.ereader_button)
+ button_layout.addWidget(self.adept_button)
+ button_layout.addWidget(self.kindle_key_button)
+
+ self.resize(self.sizeHint())
+
+ def kindle_serials(self):
+ d = dialogs.ManageKeysDialog(self,u"EInk Kindle Serial Number",self.tempdedrmprefs['serials'], dialogs.AddSerialDialog)
+ d.exec_()
+
+ def kindle_keys(self):
+ d = dialogs.ManageKeysDialog(self,u"Kindle for Mac and PC Key",self.tempdedrmprefs['kindlekeys'], dialogs.AddKindleDialog, 'k4i')
+ d.exec_()
+
+ def adept_keys(self):
+ d = dialogs.ManageKeysDialog(self,u"Adobe Digital Editions Key",self.tempdedrmprefs['adeptkeys'], dialogs.AddAdeptDialog, 'der')
+ d.exec_()
+
+ def mobi_keys(self):
+ d = dialogs.ManageKeysDialog(self,u"Mobipocket PID",self.tempdedrmprefs['pids'], dialogs.AddPIDDialog)
+ d.exec_()
+
+ def bandn_keys(self):
+ d = dialogs.ManageKeysDialog(self,u"Barnes and Noble Key",self.tempdedrmprefs['bandnkeys'], dialogs.AddBandNKeyDialog, 'b64')
+ d.exec_()
+
+ def ereader_keys(self):
+ d = dialogs.ManageKeysDialog(self,u"eReader Key",self.tempdedrmprefs['ereaderkeys'], dialogs.AddEReaderDialog, 'b63')
+ d.exec_()
+
+ def help_link_activated(self, url):
+ def get_help_file_resource():
+ # Copy the HTML helpfile to the plugin directory each time the
+ # link is clicked in case the helpfile is updated in newer plugins.
+ file_path = os.path.join(config_dir, u"plugins", u"DeDRM", u"help", help_file_name)
+ with open(file_path,'w') as f:
+ f.write(self.load_resource(help_file_name))
+ return file_path
+ url = 'file:///' + get_help_file_resource()
+ open_url(QUrl(url))
def save_settings(self):
- prefs['pids'] = str(self.pids.text()).replace(" ","")
- prefs['serials'] = str(self.serials.text()).replace(" ","")
- winepref=str(self.wineprefix.text())
- if winepref.strip() != '':
- prefs['WINEPREFIX'] = winepref
- else:
- prefs['WINEPREFIX'] = None
+ self.dedrmprefs['bandnkeys'] = self.tempdedrmprefs['bandnkeys']
+ self.dedrmprefs['adeptkeys'] = self.tempdedrmprefs['adeptkeys']
+ self.dedrmprefs['ereaderkeys'] = self.tempdedrmprefs['ereaderkeys']
+ self.dedrmprefs['kindlekeys'] = self.tempdedrmprefs['kindlekeys']
+ self.dedrmprefs['pids'] = self.tempdedrmprefs['pids']
+ self.dedrmprefs['serials'] = self.tempdedrmprefs['serials']
+ self.dedrmprefs['configured'] = True
+
+ def load_resource(self, name):
+ with ZipFile(self.plugin_path, 'r') as zf:
+ if name in zf.namelist():
+ return zf.read(name)
+ return ""
+
+def writeprefs(value = True):
+ dedrmprefs['configured'] = value
+
+def addnamedvaluetoprefs(prefkind, keyname, keyvalue):
+ try:
+ if keyvalue not in dedrmprefs[prefkind].values():
+ # ensure that the keyname is unique
+ # by adding a number (starting with 2) to the name if it is not
+ namecount = 1
+ newname = keyname
+ while newname in dedrmprefs[prefkind]:
+ namecount += 1
+ newname = "{0:s}_{1:d}".format(keyname,namecount)
+ # add to the preferences
+ dedrmprefs[prefkind][newname] = keyvalue
+ return (True, newname)
+ except:
+ pass
+ return (False, keyname)
+
+def addvaluetoprefs(prefkind, prefsvalue):
+ # ensure the keyvalue isn't already in the preferences
+ if prefsvalue not in dedrmprefs[prefkind]:
+ dedrmprefs[prefkind].append(prefsvalue)
+ return True
+ return False
+
+def convertprefs(always = False):
+
+ def parseIgnobleString(keystuff):
+ userkeys = {}
+ ar = keystuff.split(':')
+ for i, keystring in enumerate(ar):
+ try:
+ name, ccn = keystring.split(',')
+ # Generate Barnes & Noble EPUB user key from name and credit card number.
+ keyname = u"{0}_{1}_{2:d}".format(name.strip(),ccn.strip()[-4:],i+1)
+ keyvalue = bandn.generate_key(name, ccn)
+ if keyvalue not in userkeys.values():
+ while keyname in dedrmprefs['bandnkeys']:
+ keyname = keyname + keyname[-1]
+ userkeys[keyname] = keyvalue
+ except Exception, e:
+ print e.args[0]
+ pass
+ return userkeys
+
+ def parseeReaderString(keystuff):
+ userkeys = {}
+ ar = keystuff.split(':')
+ for i, keystring in enumerate(ar):
+ try:
+ name, cc = keystring.split(',')
+ # Generate eReader user key from name and credit card number.
+ keyname = u"{0}_{1}_{2:d}".format(name.strip(),cc.strip()[-4:],i+1)
+ keyvalue = ereader.getuser_key(name,cc).encode('hex')
+ if keyvalue not in userkeys.values():
+ while keyname in dedrmprefs['ereaderkeys']:
+ keyname = keyname + keyname[-1]
+ userkeys[keyname] = keyvalue
+ except Exception, e:
+ print e.args[0]
+ pass
+ return userkeys
+
+ def parseKindleString(keystuff):
+ pids = []
+ serials = []
+ ar = keystuff.split(',')
+ for keystring in ar:
+ keystring = str(keystring).strip().replace(" ","")
+ if len(keystring) == 10 or len(keystring) == 8 and keystring not in pids:
+ pids.append(keystring)
+ elif len(keystring) == 16 and keystring[0] == 'B' and keystring not in serials:
+ serials.append(keystring)
+ return (pids,serials)
+
+ def addConfigFiles(extension, prefskey, encoding = ''):
+ # get any files with extension 'extension' in the config dir
+ files = [f for f in os.listdir(config_dir) if f.endswith(extension)]
+ try:
+ priorkeycount = len(dedrmprefs[prefskey])
+ for filename in files:
+ fpath = os.path.join(config_dir, filename)
+ key = os.path.splitext(filename)[0]
+ value = open(fpath, 'rb').read()
+ if encoding is not '':
+ value = value.encode(encoding)
+ if value not in dedrmprefs[prefskey].values():
+ while key in dedrmprefs[prefskey]:
+ key = key+key[-1]
+ dedrmprefs[prefskey][key] = value
+ #os.remove(fpath)
+ return len(dedrmprefs[prefskey])-priorkeycount
+ except IOError:
+ return -1
+
+ if (not always) and dedrmprefs['configured']:
+ # We've already converted old preferences,
+ # and we're not being forced to do it again, so just return
+ return
+
+ # initialise
+ # we must actually set the prefs that are dictionaries and lists
+ # to empty dictionaries and lists, otherwise we are unable to add to them
+ # as then it just adds to the (memory only) dedrmprefs.defaults versions!
+ if dedrmprefs['bandnkeys'] == {}:
+ dedrmprefs['bandnkeys'] = {}
+ if dedrmprefs['adeptkeys'] == {}:
+ dedrmprefs['adeptkeys'] = {}
+ if dedrmprefs['ereaderkeys'] == {}:
+ dedrmprefs['ereaderkeys'] = {}
+ if dedrmprefs['kindlekeys'] == {}:
+ dedrmprefs['kindlekeys'] = {}
+ if dedrmprefs['pids'] == []:
+ dedrmprefs['pids'] = []
+ if dedrmprefs['serials'] == []:
+ dedrmprefs['serials'] = []
+
+ # get default adobe adept key(s)
+ priorkeycount = len(dedrmprefs['adeptkeys'])
+ try:
+ defaultkeys = adobe.adeptkeys()
+ except:
+ defaultkeys = []
+ defaultcount = 1
+ for keyvalue in defaultkeys:
+ keyname = u"default_key_{0:d}".format(defaultcount)
+ keyvaluehex = keyvalue.encode('hex')
+ if keyvaluehex not in dedrmprefs['adeptkeys'].values():
+ while keyname in dedrmprefs['adeptkeys']:
+ defaultcount += 1
+ keyname = u"default_key_{0:d}".format(defaultcount)
+ dedrmprefs['adeptkeys'][keyname] = keyvaluehex
+ addedkeycount = len(dedrmprefs['adeptkeys']) - priorkeycount
+ if addedkeycount > 0:
+ print u"{0} v{1}: {2:d} Default Adobe Adept {3} found.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys")
+ # Make the json write all the prefs to disk
+ writeprefs(False)
+
+
+ # get default kindle key(s)
+ priorkeycount = len(dedrmprefs['kindlekeys'])
+ try:
+ defaultkeys = amazon.kindlekeys()
+ except:
+ defaultkeys = []
+ defaultcount = 1
+ for keyvalue in defaultkeys:
+ keyname = u"default_key_{0:d}".format(defaultcount)
+ if keyvalue not in dedrmprefs['kindlekeys'].values():
+ while keyname in dedrmprefs['kindlekeys']:
+ defaultcount += 1
+ keyname = u"default_key_{0:d}".format(defaultcount)
+ dedrmprefs['kindlekeys'][keyname] = keyvalue
+ addedkeycount = len(dedrmprefs['kindlekeys']) - priorkeycount
+ if addedkeycount > 0:
+ print u"{0} v{1}: {2:d} Default Kindle for Mac/PC {3} found.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys")
+ # Make the json write all the prefs to disk
+ writeprefs(False)
+
+ print u"{0} v{1}: Importing configuration data from old DeDRM plugins".format(PLUGIN_NAME, PLUGIN_VERSION)
+
+ # Handle the old ignoble plugin's customization string by converting the
+ # old string to stored keys... get that personal data out of plain sight.
+ from calibre.customize.ui import config
+ sc = config['plugin_customization']
+ val = sc.pop(IGNOBLEPLUGINNAME, None)
+ if val is not None:
+ print u"{0} v{1}: Converting old Ignoble plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION)
+ priorkeycount = len(dedrmprefs['bandnkeys'])
+ userkeys = parseIgnobleString(str(val))
+ for key in userkeys:
+ value = userkeys[key]
+ if value not in dedrmprefs['bandnkeys'].values():
+ while key in dedrmprefs['bandnkeys']:
+ key = key+key[-1]
+ dedrmprefs['bandnkeys'][key] = value
+ addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount
+ print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from old Ignoble plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys")
+ # Make the json write all the prefs to disk
+ writeprefs(False)
+
+ # Handle the old eReader plugin's customization string by converting the
+ # old string to stored keys... get that personal data out of plain sight.
+ val = sc.pop(EREADERPLUGINNAME, None)
+ if val is not None:
+ print u"{0} v{1}: Converting old eReader plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION)
+ priorkeycount = len(dedrmprefs['ereaderkeys'])
+ userkeys = parseeReaderString(str(val))
+ for key in userkeys:
+ value = userkeys[key]
+ if value not in dedrmprefs['ereaderkeys'].values():
+ while key in dedrmprefs['ereaderkeys']:
+ key = key+key[-1]
+ dedrmprefs['ereaderkeys'][key] = value
+ addedkeycount = len(dedrmprefs['ereaderkeys'])-priorkeycount
+ print u"{0} v{1}: {2:d} eReader {3} imported from old eReader plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys")
+ # Make the json write all the prefs to disk
+ writeprefs(False)
+
+ # get old Kindle plugin configuration string
+ val = sc.pop(OLDKINDLEPLUGINNAME, None)
+ if val is not None:
+ print u"{0} v{1}: Converting old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION)
+ priorpidcount = len(dedrmprefs['pids'])
+ priorserialcount = len(dedrmprefs['serials'])
+ pids, serials = parseKindleString(val)
+ for pid in pids:
+ if pid not in dedrmprefs['pids']:
+ dedrmprefs['pids'].append(pid)
+ for serial in serials:
+ if serial not in dedrmprefs['serials']:
+ dedrmprefs['serials'].append(serial)
+ addedpidcount = len(dedrmprefs['pids']) - priorpidcount
+ addedserialcount = len(dedrmprefs['serials']) - priorserialcount
+ print u"{0} v{1}: {2:d} {3} and {4:d} {5} imported from old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, u"PID" if addedpidcount==1 else u"PIDs", addedserialcount, u"serial number" if addedserialcount==1 else u"serial numbers")
+ # Make the json write all the prefs to disk
+ writeprefs(False)
+
+ # copy the customisations back into calibre preferences, as we've now removed the nasty plaintext
+ config['plugin_customization'] = sc
+
+ # get any .b64 files in the config dir
+ ignoblecount = addConfigFiles('.b64', 'bandnkeys')
+ if ignoblecount > 0:
+ print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, ignoblecount, u"key file" if ignoblecount==1 else u"key files")
+ elif ignoblecount < 0:
+ print u"{0} v{1}: Error reading Barnes & Noble keyfiles from config directory.".format(PLUGIN_NAME, PLUGIN_VERSION)
+ # Make the json write all the prefs to disk
+ writeprefs(False)
+
+ # get any .der files in the config dir
+ ineptcount = addConfigFiles('.der', 'adeptkeys','hex')
+ if ineptcount > 0:
+ print u"{0} v{1}: {2:d} Adobe Adept {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, ineptcount, u"keyfile" if ineptcount==1 else u"keyfiles")
+ elif ineptcount < 0:
+ print u"{0} v{1}: Error reading Adobe Adept keyfiles from config directory.".format(PLUGIN_NAME, PLUGIN_VERSION)
+ # Make the json write all the prefs to disk
+ writeprefs(False)
+
+ # get ignoble json prefs
+ if 'keys' in ignobleprefs:
+ priorkeycount = len(dedrmprefs['bandnkeys'])
+ for key in ignobleprefs['keys']:
+ value = ignobleprefs['keys'][key]
+ if value not in dedrmprefs['bandnkeys'].values():
+ while key in dedrmprefs['bandnkeys']:
+ key = key+key[-1]
+ dedrmprefs['bandnkeys'][key] = value
+ addedkeycount = len(dedrmprefs['bandnkeys']) - priorkeycount
+ # no need to delete old prefs, since they contain no recoverable private data
+ if addedkeycount > 0:
+ print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from Ignoble plugin preferences.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys")
+ # Make the json write all the prefs to disk
+ writeprefs(False)
+
+ # get kindle json prefs
+ priorpidcount = len(dedrmprefs['pids'])
+ priorserialcount = len(dedrmprefs['serials'])
+ if 'pids' in kindleprefs:
+ pids, serials = parseKindleString(kindleprefs['pids'])
+ for pid in pids:
+ if pid not in dedrmprefs['pids']:
+ dedrmprefs['pids'].append(pid)
+ if 'serials' in kindleprefs:
+ pids, serials = parseKindleString(kindleprefs['serials'])
+ for serial in serials:
+ if serial not in dedrmprefs['serials']:
+ dedrmprefs['serials'].append(serial)
+ addedpidcount = len(dedrmprefs['pids']) - priorpidcount
+ if addedpidcount > 0:
+ print u"{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, u"PID" if addedpidcount==1 else u"PIDs")
+ addedserialcount = len(dedrmprefs['serials']) - priorserialcount
+ if addedserialcount > 0:
+ print u"{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedserialcount, u"serial number" if addedserialcount==1 else u"serial numbers")
+
+ # Make the json write all the prefs to disk
+ writeprefs()
+ print u"{0} v{1}: Finished setting up configuration data.".format(PLUGIN_NAME, PLUGIN_VERSION)
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/convert2xml.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/convert2xml.py
index c4e23b7..101c45a 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/convert2xml.py
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/convert2xml.py
@@ -264,6 +264,7 @@ class PageParser(object):
'img.color_src' : (1, 'scalar_number', 0, 0),
'img.gridBeginCenter' : (1, 'scalar_number', 0, 0),
'img.gridEndCenter' : (1, 'scalar_number', 0, 0),
+ 'img.image_type' : (1, 'scalar_number', 0, 0),
'paragraph' : (1, 'snippets', 1, 0),
'paragraph.class' : (1, 'scalar_text', 0, 0),
@@ -272,9 +273,9 @@ class PageParser(object):
'paragraph.lastWord' : (1, 'scalar_number', 0, 0),
'paragraph.gridSize' : (1, 'scalar_number', 0, 0),
'paragraph.gridBottomCenter' : (1, 'scalar_number', 0, 0),
- 'paragraph.gridTopCenter' : (1, 'scalar_number', 0, 0),
- 'paragraph.gridBeginCenter' : (1, 'scalar_number', 0, 0),
- 'paragraph.gridEndCenter' : (1, 'scalar_number', 0, 0),
+ 'paragraph.gridTopCenter' : (1, 'scalar_number', 0, 0),
+ 'paragraph.gridBeginCenter' : (1, 'scalar_number', 0, 0),
+ 'paragraph.gridEndCenter' : (1, 'scalar_number', 0, 0),
'word_semantic' : (1, 'snippets', 1, 1),
@@ -282,6 +283,10 @@ class PageParser(object):
'word_semantic.class' : (1, 'scalar_text', 0, 0),
'word_semantic.firstWord' : (1, 'scalar_number', 0, 0),
'word_semantic.lastWord' : (1, 'scalar_number', 0, 0),
+ 'word_semantic.gridBottomCenter' : (1, 'scalar_number', 0, 0),
+ 'word_semantic.gridTopCenter' : (1, 'scalar_number', 0, 0),
+ 'word_semantic.gridBeginCenter' : (1, 'scalar_number', 0, 0),
+ 'word_semantic.gridEndCenter' : (1, 'scalar_number', 0, 0),
'word' : (1, 'snippets', 1, 0),
'word.type' : (1, 'scalar_text', 0, 0),
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/description.rtfd/TXT.rtf b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/description.rtfd/TXT.rtf
index 4ea1054..cbc6490 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/description.rtfd/TXT.rtf
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/description.rtfd/TXT.rtf
@@ -1,4 +1,4 @@
-{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360
+{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
{\fonttbl}
{\colortbl;\red255\green255\blue255;}
} \ No newline at end of file
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/dialogs.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/dialogs.py
new file mode 100644
index 0000000..21c1dad
--- /dev/null
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/dialogs.py
@@ -0,0 +1,719 @@
+#!/usr/bin/env python
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+
+from __future__ import with_statement
+__license__ = 'GPL v3'
+
+# Standard Python modules.
+import os, sys, re, hashlib
+import json
+
+from PyQt4.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QListWidget, QListWidgetItem, QAbstractItemView, QLineEdit, QPushButton, QIcon, QGroupBox, QDialog, QDialogButtonBox, QUrl, QString)
+from PyQt4 import QtGui
+
+# calibre modules and constants.
+from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url,
+ choose_dir, choose_files)
+from calibre.utils.config import dynamic, config_dir, JSONConfig
+
+from calibre_plugins.dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION
+from calibre_plugins.dedrm.utilities import (uStrCmp, DETAILED_MESSAGE, parseCustString)
+from calibre_plugins.dedrm.ignoblekeygen import generate_key as generate_bandn_key
+from calibre_plugins.dedrm.erdr2pml import getuser_key as generate_ereader_key
+from calibre_plugins.dedrm.adobekey import adeptkeys as retrieve_adept_keys
+from calibre_plugins.dedrm.kindlekey import kindlekeys as retrieve_kindle_keys
+
+class ManageKeysDialog(QDialog):
+ def __init__(self, parent, key_type_name, plugin_keys, create_key, keyfile_ext = u""):
+ QDialog.__init__(self,parent)
+ self.parent = parent
+ self.key_type_name = key_type_name
+ self.plugin_keys = plugin_keys
+ self.create_key = create_key
+ self.keyfile_ext = keyfile_ext
+ self.import_key = (keyfile_ext != u"")
+ self.binary_file = (key_type_name == u"Adobe Digital Editions Key")
+ self.json_file = (key_type_name == u"Kindle for Mac and PC Key")
+
+ self.setWindowTitle("{0} {1}: Manage {2}s".format(PLUGIN_NAME, PLUGIN_VERSION, self.key_type_name))
+
+ # Start Qt Gui dialog layout
+ layout = QVBoxLayout(self)
+ self.setLayout(layout)
+
+ help_layout = QHBoxLayout()
+ layout.addLayout(help_layout)
+ # Add hyperlink to a help file at the right. We will replace the correct name when it is clicked.
+ help_label = QLabel('<a href="http://www.foo.com/">Help</a>', self)
+ help_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
+ help_label.setAlignment(Qt.AlignRight)
+ help_label.linkActivated.connect(self.help_link_activated)
+ help_layout.addWidget(help_label)
+
+ keys_group_box = QGroupBox(_(u"{0}s".format(self.key_type_name)), self)
+ layout.addWidget(keys_group_box)
+ keys_group_box_layout = QHBoxLayout()
+ keys_group_box.setLayout(keys_group_box_layout)
+
+ self.listy = QListWidget(self)
+ self.listy.setToolTip(u"{0}s that will be used to decrypt ebooks".format(self.key_type_name))
+ self.listy.setSelectionMode(QAbstractItemView.SingleSelection)
+ self.populate_list()
+ keys_group_box_layout.addWidget(self.listy)
+
+ button_layout = QVBoxLayout()
+ keys_group_box_layout.addLayout(button_layout)
+ self._add_key_button = QtGui.QToolButton(self)
+ self._add_key_button.setToolTip(u"Create new {0}".format(self.key_type_name))
+ self._add_key_button.setIcon(QIcon(I('plus.png')))
+ self._add_key_button.clicked.connect(self.add_key)
+ button_layout.addWidget(self._add_key_button)
+
+ self._delete_key_button = QtGui.QToolButton(self)
+ self._delete_key_button.setToolTip(_(u"Delete highlighted key"))
+ self._delete_key_button.setIcon(QIcon(I('list_remove.png')))
+ self._delete_key_button.clicked.connect(self.delete_key)
+ button_layout.addWidget(self._delete_key_button)
+
+ if type(self.plugin_keys) == dict:
+ self._rename_key_button = QtGui.QToolButton(self)
+ self._rename_key_button.setToolTip(_(u"Rename highlighted key"))
+ self._rename_key_button.setIcon(QIcon(I('edit-select-all.png')))
+ self._rename_key_button.clicked.connect(self.rename_key)
+ button_layout.addWidget(self._rename_key_button)
+
+ self.export_key_button = QtGui.QToolButton(self)
+ self.export_key_button.setToolTip(u"Save highlighted key to a .{0} file".format(self.keyfile_ext))
+ self.export_key_button.setIcon(QIcon(I('save.png')))
+ self.export_key_button.clicked.connect(self.export_key)
+ button_layout.addWidget(self.export_key_button)
+ spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
+ button_layout.addItem(spacerItem)
+
+ layout.addSpacing(5)
+ migrate_layout = QHBoxLayout()
+ layout.addLayout(migrate_layout)
+ if self.import_key:
+ migrate_layout.setAlignment(Qt.AlignJustify)
+ self.migrate_btn = QPushButton(u"Import Existing Keyfiles", self)
+ self.migrate_btn.setToolTip(u"Import *.{0} files (created using other tools).".format(self.keyfile_ext))
+ self.migrate_btn.clicked.connect(self.migrate_wrapper)
+ migrate_layout.addWidget(self.migrate_btn)
+ migrate_layout.addStretch()
+ self.button_box = QDialogButtonBox(QDialogButtonBox.Close)
+ self.button_box.rejected.connect(self.close)
+ migrate_layout.addWidget(self.button_box)
+
+ self.resize(self.sizeHint())
+
+ def populate_list(self):
+ if type(self.plugin_keys) == dict:
+ for key in self.plugin_keys.keys():
+ self.listy.addItem(QListWidgetItem(key))
+ else:
+ for key in self.plugin_keys:
+ self.listy.addItem(QListWidgetItem(key))
+
+ def add_key(self):
+ d = self.create_key(self)
+ d.exec_()
+
+ if d.result() != d.Accepted:
+ # New key generation cancelled.
+ return
+ new_key_value = d.key_value
+ if type(self.plugin_keys) == dict:
+ if new_key_value in self.plugin_keys.values():
+ old_key_name = [name for name, value in self.plugin_keys.iteritems() if value == new_key_value][0]
+ info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
+ u"The new {1} is the same as the existing {1} named <strong>{0}</strong> and has not been added.".format(old_key_name,self.key_type_name), show=True)
+ return
+ self.plugin_keys[d.key_name] = new_key_value
+ else:
+ if new_key_value in self.plugin_keys:
+ info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
+ u"This {0} is already in the list of {0}s has not been added.".format(self.key_type_name), show=True)
+ return
+
+ self.plugin_keys.append(d.key_value)
+ self.listy.clear()
+ self.populate_list()
+
+ def rename_key(self):
+ if not self.listy.currentItem():
+ errmsg = u"No {0} selected to rename. Highlight a keyfile first.".format(self.key_type_name)
+ r = error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
+ _(errmsg), show=True, show_copy_button=False)
+ return
+
+ d = RenameKeyDialog(self)
+ d.exec_()
+
+ if d.result() != d.Accepted:
+ # rename cancelled or moot.
+ return
+ keyname = unicode(self.listy.currentItem().text().toUtf8(),'utf8')
+ if not question_dialog(self, "{0} {1}: Confirm Rename".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to rename the {2} named <strong>{0}</strong> to <strong>{1}</strong>?".format(keyname,d.key_name,self.key_type_name), show_copy_button=False, default_yes=False):
+ return
+ self.plugin_keys[d.key_name] = self.plugin_keys[keyname]
+ del self.plugin_keys[keyname]
+
+ self.listy.clear()
+ self.populate_list()
+
+ def delete_key(self):
+ if not self.listy.currentItem():
+ return
+ keyname = unicode(self.listy.currentItem().text().toUtf8(), 'utf8')
+ if not question_dialog(self, "{0} {1}: Confirm Delete".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to delete the {1} <strong>{0}</strong>?".format(keyname, self.key_type_name), show_copy_button=False, default_yes=False):
+ return
+ if type(self.plugin_keys) == dict:
+ del self.plugin_keys[keyname]
+ else:
+ self.plugin_keys.remove(keyname)
+
+ self.listy.clear()
+ self.populate_list()
+
+ def help_link_activated(self, url):
+ def get_help_file_resource():
+ # Copy the HTML helpfile to the plugin directory each time the
+ # link is clicked in case the helpfile is updated in newer plugins.
+ help_file_name = u"{0}_{1}_Help.htm".format(PLUGIN_NAME, self.key_type_name)
+ file_path = os.path.join(config_dir, u"plugins", u"DeDRM", u"help", help_file_name)
+ with open(file_path,'w') as f:
+ f.write(self.parent.load_resource(help_file_name))
+ return file_path
+ url = 'file:///' + get_help_file_resource()
+ open_url(QUrl(url))
+
+ def migrate_files(self):
+ dynamic[PLUGIN_NAME + u"config_dir"] = config_dir
+ files = choose_files(self, PLUGIN_NAME + u"config_dir",
+ u"Select {0} files to import".format(self.key_type_name), [(u"{0} files".format(self.key_type_name), [self.keyfile_ext])], False)
+ counter = 0
+ skipped = 0
+ if files:
+ for filename in files:
+ fpath = os.path.join(config_dir, filename)
+ filename = os.path.basename(filename)
+ new_key_name = os.path.splitext(os.path.basename(filename))[0]
+ with open(fpath,'rb') as keyfile:
+ new_key_value = keyfile.read()
+ if self.binary_file:
+ new_key_value = new_key_value.encode('hex')
+ elif self.json_file:
+ new_key_value = json.loads(new_key_value)
+ match = False
+ for key in self.plugin_keys.keys():
+ if uStrCmp(new_key_name, key, True):
+ skipped += 1
+ msg = u"A key with the name <strong>{0}</strong> already exists!\nSkipping key file <strong>{1}</strong>.\nRename the existing key and import again".format(new_key_name,filename)
+ inf = info_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
+ _(msg), show_copy_button=False, show=True)
+ match = True
+ break
+ if not match:
+ if new_key_value in self.plugin_keys.values():
+ old_key_name = [name for name, value in self.plugin_keys.iteritems() if value == new_key_value][0]
+ skipped += 1
+ info_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
+ u"The key in file {0} is the same as the existing key <strong>{1}</strong> and has been skipped.".format(filename,old_key_name), show_copy_button=False, show=True)
+ else:
+ counter += 1
+ self.plugin_keys[new_key_name] = new_key_value
+
+ msg = u""
+ if counter+skipped > 1:
+ if counter > 0:
+ msg += u"Imported <strong>{0:d}</strong> key {1}. ".format(counter, u"file" if counter == 1 else u"files")
+ if skipped > 0:
+ msg += u"Skipped <strong>{0:d}</strong> key {1}.".format(skipped, u"file" if counter == 1 else u"files")
+ inf = info_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
+ _(msg), show_copy_button=False, show=True)
+ return counter > 0
+
+ def migrate_wrapper(self):
+ if self.migrate_files():
+ self.listy.clear()
+ self.populate_list()
+
+ def export_key(self):
+ if not self.listy.currentItem():
+ errmsg = u"No keyfile selected to export. Highlight a keyfile first."
+ r = error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
+ _(errmsg), show=True, show_copy_button=False)
+ return
+ filter = QString(u"{0} Files (*.{1})".format(self.key_type_name, self.keyfile_ext))
+ keyname = unicode(self.listy.currentItem().text().toUtf8(), 'utf8')
+ if dynamic.get(PLUGIN_NAME + 'save_dir'):
+ defaultname = os.path.join(dynamic.get(PLUGIN_NAME + 'save_dir'), u"{0}.{1}".format(keyname , self.keyfile_ext))
+ else:
+ defaultname = os.path.join(os.path.expanduser('~'), u"{0}.{1}".format(keyname , self.keyfile_ext))
+ filename = unicode(QtGui.QFileDialog.getSaveFileName(self, u"Save {0} File as...".format(self.key_type_name), defaultname,
+ u"{0} Files (*.{1})".format(self.key_type_name,self.keyfile_ext), filter))
+ if filename:
+ dynamic[PLUGIN_NAME + 'save_dir'] = os.path.split(filename)[0]
+ with file(filename, 'w') as fname:
+ if self.binary_file:
+ fname.write(self.plugin_keys[keyname].decode('hex'))
+ elif self.json_file:
+ fname.write(json.dumps(self.plugin_keys[keyname]))
+ else:
+ fname.write(self.plugin_keys[keyname])
+
+
+
+
+class RenameKeyDialog(QDialog):
+ def __init__(self, parent=None,):
+ print repr(self), repr(parent)
+ QDialog.__init__(self, parent)
+ self.parent = parent
+ self.setWindowTitle("{0} {1}: Rename {0}".format(PLUGIN_NAME, PLUGIN_VERSION, parent.key_type_name))
+ layout = QVBoxLayout(self)
+ self.setLayout(layout)
+
+ data_group_box = QGroupBox('', self)
+ layout.addWidget(data_group_box)
+ data_group_box_layout = QVBoxLayout()
+ data_group_box.setLayout(data_group_box_layout)
+
+ data_group_box_layout.addWidget(QLabel('New Key Name:', self))
+ self.key_ledit = QLineEdit(self.parent.listy.currentItem().text(), self)
+ self.key_ledit.setToolTip(u"Enter a new name for this existing {0}.".format(parent.key_type_name))
+ data_group_box_layout.addWidget(self.key_ledit)
+
+ layout.addSpacing(20)
+
+ self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+ self.button_box.accepted.connect(self.accept)
+ self.button_box.rejected.connect(self.reject)
+ layout.addWidget(self.button_box)
+
+ self.resize(self.sizeHint())
+
+ def accept(self):
+ if self.key_ledit.text().isEmpty() or unicode(self.key_ledit.text()).isspace():
+ errmsg = u"Key name field cannot be empty!"
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
+ _(errmsg), show=True, show_copy_button=False)
+ if len(self.key_ledit.text()) < 4:
+ errmsg = u"Key name must be at <i>least</i> 4 characters long!"
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
+ _(errmsg), show=True, show_copy_button=False)
+ if uStrCmp(self.key_ledit.text(), self.parent.listy.currentItem().text()):
+ # Same exact name ... do nothing.
+ return QDialog.reject(self)
+ for k in self.parent.plugin_keys.keys():
+ if (uStrCmp(self.key_ledit.text(), k, True) and
+ not uStrCmp(k, self.parent.listy.currentItem().text(), True)):
+ errmsg = u"The key name <strong>{0}</strong> is already being used.".format(self.key_ledit.text())
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
+ _(errmsg), show=True, show_copy_button=False)
+ QDialog.accept(self)
+
+ @property
+ def key_name(self):
+ return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
+
+
+
+
+
+
+
+
+class AddBandNKeyDialog(QDialog):
+ def __init__(self, parent=None,):
+ QDialog.__init__(self, parent)
+ self.parent = parent
+ self.setWindowTitle(u"{0} {1}: Create New Barnes & Noble Key".format(PLUGIN_NAME, PLUGIN_VERSION))
+ layout = QVBoxLayout(self)
+ self.setLayout(layout)
+
+ data_group_box = QGroupBox(u"", self)
+ layout.addWidget(data_group_box)
+ data_group_box_layout = QVBoxLayout()
+ data_group_box.setLayout(data_group_box_layout)
+
+ key_group = QHBoxLayout()
+ data_group_box_layout.addLayout(key_group)
+ key_group.addWidget(QLabel(u"Unique Key Name:", self))
+ self.key_ledit = QLineEdit("", self)
+ self.key_ledit.setToolTip(_(u"<p>Enter an identifying name for this new key.</p>" +
+ u"<p>It should be something that will help you remember " +
+ u"what personal information was used to create it."))
+ key_group.addWidget(self.key_ledit)
+ key_label = QLabel(_(''), self)
+ key_label.setAlignment(Qt.AlignHCenter)
+ data_group_box_layout.addWidget(key_label)
+
+ name_group = QHBoxLayout()
+ data_group_box_layout.addLayout(name_group)
+ name_group.addWidget(QLabel(u"Your Name:", self))
+ self.name_ledit = QLineEdit(u"", self)
+ self.name_ledit.setToolTip(_(u"<p>Enter your name as it appears in your B&N " +
+ u"account or on your credit card.</p>" +
+ u"<p>It will only be used to generate this " +
+ u"one-time key and won\'t be stored anywhere " +
+ u"in calibre or on your computer.</p>" +
+ u"<p>(ex: Jonathan Smith)"))
+ name_group.addWidget(self.name_ledit)
+ name_disclaimer_label = QLabel(_(u"(Will not be saved in configuration data)"), self)
+ name_disclaimer_label.setAlignment(Qt.AlignHCenter)
+ data_group_box_layout.addWidget(name_disclaimer_label)
+
+ ccn_group = QHBoxLayout()
+ data_group_box_layout.addLayout(ccn_group)
+ ccn_group.addWidget(QLabel(u"Credit Card#:", self))
+ self.cc_ledit = QLineEdit(u"", self)
+ self.cc_ledit.setToolTip(_(u"<p>Enter the full credit card number on record " +
+ u"in your B&N account.</p>" +
+ u"<p>No spaces or dashes... just the numbers. " +
+ u"This number will only be used to generate this " +
+ u"one-time key and won\'t be stored anywhere in " +
+ u"calibre or on your computer."))
+ ccn_group.addWidget(self.cc_ledit)
+ ccn_disclaimer_label = QLabel(_('(Will not be saved in configuration data)'), self)
+ ccn_disclaimer_label.setAlignment(Qt.AlignHCenter)
+ data_group_box_layout.addWidget(ccn_disclaimer_label)
+ layout.addSpacing(10)
+
+ self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+ self.button_box.accepted.connect(self.accept)
+ self.button_box.rejected.connect(self.reject)
+ layout.addWidget(self.button_box)
+
+ self.resize(self.sizeHint())
+
+ @property
+ def key_name(self):
+ return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
+
+ @property
+ def key_value(self):
+ return generate_bandn_key(self.user_name,self.cc_number)
+
+ @property
+ def user_name(self):
+ return unicode(self.name_ledit.text().toUtf8(), 'utf8').strip().lower().replace(' ','')
+
+ @property
+ def cc_number(self):
+ return unicode(self.cc_ledit.text().toUtf8(), 'utf8').strip().replace(' ', '').replace('-','')
+
+
+ def accept(self):
+ if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace():
+ errmsg = u"All fields are required!"
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+ if not self.cc_number.isdigit():
+ errmsg = u"Numbers only in the credit card number field!"
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+ if len(self.key_name) < 4:
+ errmsg = u"Key name must be at <i>least</i> 4 characters long!"
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+ QDialog.accept(self)
+
+class AddEReaderDialog(QDialog):
+ def __init__(self, parent=None,):
+ QDialog.__init__(self, parent)
+ self.parent = parent
+ self.setWindowTitle(u"{0} {1}: Create New eReader Key".format(PLUGIN_NAME, PLUGIN_VERSION))
+ layout = QVBoxLayout(self)
+ self.setLayout(layout)
+
+ data_group_box = QGroupBox(u"", self)
+ layout.addWidget(data_group_box)
+ data_group_box_layout = QVBoxLayout()
+ data_group_box.setLayout(data_group_box_layout)
+
+ key_group = QHBoxLayout()
+ data_group_box_layout.addLayout(key_group)
+ key_group.addWidget(QLabel(u"Unique Key Name:", self))
+ self.key_ledit = QLineEdit("", self)
+ self.key_ledit.setToolTip(u"<p>Enter an identifying name for this new key.\nIt should be something that will help you remember what personal information was used to create it.")
+ key_group.addWidget(self.key_ledit)
+ key_label = QLabel(_(''), self)
+ key_label.setAlignment(Qt.AlignHCenter)
+ data_group_box_layout.addWidget(key_label)
+
+ name_group = QHBoxLayout()
+ data_group_box_layout.addLayout(name_group)
+ name_group.addWidget(QLabel(u"Your Name:", self))
+ self.name_ledit = QLineEdit(u"", self)
+ self.name_ledit.setToolTip(u"Enter the name for this eReader key, usually the name on your credit card.\nIt will only be used to generate this one-time key and won\'t be stored anywhere in calibre or on your computer.\n(ex: Mr Jonathan Q Smith)")
+ name_group.addWidget(self.name_ledit)
+ name_disclaimer_label = QLabel(_(u"(Will not be saved in configuration data)"), self)
+ name_disclaimer_label.setAlignment(Qt.AlignHCenter)
+ data_group_box_layout.addWidget(name_disclaimer_label)
+
+ ccn_group = QHBoxLayout()
+ data_group_box_layout.addLayout(ccn_group)
+ ccn_group.addWidget(QLabel(u"Credit Card#:", self))
+ self.cc_ledit = QLineEdit(u"", self)
+ self.cc_ledit.setToolTip(u"<p>Enter the last 8 digits of credit card number for this eReader key.\nThey will only be used to generate this one-time key and won\'t be stored anywhere in calibre or on your computer.")
+ ccn_group.addWidget(self.cc_ledit)
+ ccn_disclaimer_label = QLabel(_('(Will not be saved in configuration data)'), self)
+ ccn_disclaimer_label.setAlignment(Qt.AlignHCenter)
+ data_group_box_layout.addWidget(ccn_disclaimer_label)
+ layout.addSpacing(10)
+
+ self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+ self.button_box.accepted.connect(self.accept)
+ self.button_box.rejected.connect(self.reject)
+ layout.addWidget(self.button_box)
+
+ self.resize(self.sizeHint())
+
+ @property
+ def key_name(self):
+ return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
+
+ @property
+ def key_value(self):
+ return generate_ereader_key(self.user_name,self.cc_number).encode('hex')
+
+ @property
+ def user_name(self):
+ return unicode(self.name_ledit.text().toUtf8(), 'utf8').strip().lower().replace(' ','')
+
+ @property
+ def cc_number(self):
+ return unicode(self.cc_ledit.text().toUtf8(), 'utf8').strip().replace(' ', '').replace('-','')
+
+
+ def accept(self):
+ if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace():
+ errmsg = u"All fields are required!"
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+ if not self.cc_number.isdigit():
+ errmsg = u"Numbers only in the credit card number field!"
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+ if len(self.key_name) < 4:
+ errmsg = u"Key name must be at <i>least</i> 4 characters long!"
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+ QDialog.accept(self)
+
+
+class AddAdeptDialog(QDialog):
+ def __init__(self, parent=None,):
+ QDialog.__init__(self, parent)
+ self.parent = parent
+ self.setWindowTitle(u"{0} {1}: Getting Default Adobe Digital Editions Key".format(PLUGIN_NAME, PLUGIN_VERSION))
+ layout = QVBoxLayout(self)
+ self.setLayout(layout)
+
+ try:
+ self.default_key = retrieve_adept_keys()[0]
+ except:
+ self.default_key = u""
+
+ self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+
+ if len(self.default_key)>0:
+ data_group_box = QGroupBox(u"", self)
+ layout.addWidget(data_group_box)
+ data_group_box_layout = QVBoxLayout()
+ data_group_box.setLayout(data_group_box_layout)
+
+ key_group = QHBoxLayout()
+ data_group_box_layout.addLayout(key_group)
+ key_group.addWidget(QLabel(u"Unique Key Name:", self))
+ self.key_ledit = QLineEdit("", self)
+ self.key_ledit.setToolTip(u"<p>Enter an identifying name for the current default Adobe Digital Editions key.")
+ key_group.addWidget(self.key_ledit)
+ key_label = QLabel(_(''), self)
+ key_label.setAlignment(Qt.AlignHCenter)
+ data_group_box_layout.addWidget(key_label)
+ self.button_box.accepted.connect(self.accept)
+ else:
+ default_key_error = QLabel(u"The default encryption key for Adobe Digital Editions could not be found.", self)
+ default_key_error.setAlignment(Qt.AlignHCenter)
+ layout.addWidget(default_key_error)
+ # if no default, bot buttons do the same
+ self.button_box.accepted.connect(self.reject)
+
+ self.button_box.rejected.connect(self.reject)
+ layout.addWidget(self.button_box)
+
+ self.resize(self.sizeHint())
+
+ @property
+ def key_name(self):
+ return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
+
+ @property
+ def key_value(self):
+ return self.default_key.encode('hex')
+
+
+ def accept(self):
+ if len(self.key_name) == 0 or self.key_name.isspace():
+ errmsg = u"All fields are required!"
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+ if len(self.key_name) < 4:
+ errmsg = u"Key name must be at <i>least</i> 4 characters long!"
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+ QDialog.accept(self)
+
+
+class AddKindleDialog(QDialog):
+ def __init__(self, parent=None,):
+ QDialog.__init__(self, parent)
+ self.parent = parent
+ self.setWindowTitle(u"{0} {1}: Getting Default Kindle for Mac/PC Key".format(PLUGIN_NAME, PLUGIN_VERSION))
+ layout = QVBoxLayout(self)
+ self.setLayout(layout)
+
+ try:
+ self.default_key = retrieve_kindle_keys()[0]
+ except:
+ self.default_key = u""
+
+ self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+
+ if len(self.default_key)>0:
+ data_group_box = QGroupBox(u"", self)
+ layout.addWidget(data_group_box)
+ data_group_box_layout = QVBoxLayout()
+ data_group_box.setLayout(data_group_box_layout)
+
+ key_group = QHBoxLayout()
+ data_group_box_layout.addLayout(key_group)
+ key_group.addWidget(QLabel(u"Unique Key Name:", self))
+ self.key_ledit = QLineEdit("", self)
+ self.key_ledit.setToolTip(u"<p>Enter an identifying name for the current default Kindle for Mac/PC key.")
+ key_group.addWidget(self.key_ledit)
+ key_label = QLabel(_(''), self)
+ key_label.setAlignment(Qt.AlignHCenter)
+ data_group_box_layout.addWidget(key_label)
+ self.button_box.accepted.connect(self.accept)
+ else:
+ default_key_error = QLabel(u"The default encryption key for Kindle for Mac/PC could not be found.", self)
+ default_key_error.setAlignment(Qt.AlignHCenter)
+ layout.addWidget(default_key_error)
+ # if no default, bot buttons do the same
+ self.button_box.accepted.connect(self.reject)
+
+ self.button_box.rejected.connect(self.reject)
+ layout.addWidget(self.button_box)
+
+ self.resize(self.sizeHint())
+
+ @property
+ def key_name(self):
+ return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
+
+ @property
+ def key_value(self):
+ return self.default_key
+
+
+ def accept(self):
+ if len(self.key_name) == 0 or self.key_name.isspace():
+ errmsg = u"All fields are required!"
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+ if len(self.key_name) < 4:
+ errmsg = u"Key name must be at <i>least</i> 4 characters long!"
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+ QDialog.accept(self)
+
+
+class AddSerialDialog(QDialog):
+ def __init__(self, parent=None,):
+ QDialog.__init__(self, parent)
+ self.parent = parent
+ self.setWindowTitle(u"{0} {1}: Add New EInk Kindle Serial Number".format(PLUGIN_NAME, PLUGIN_VERSION))
+ layout = QVBoxLayout(self)
+ self.setLayout(layout)
+
+ data_group_box = QGroupBox(u"", self)
+ layout.addWidget(data_group_box)
+ data_group_box_layout = QVBoxLayout()
+ data_group_box.setLayout(data_group_box_layout)
+
+ key_group = QHBoxLayout()
+ data_group_box_layout.addLayout(key_group)
+ key_group.addWidget(QLabel(u"EInk Kindle Serial Number:", self))
+ self.key_ledit = QLineEdit("", self)
+ self.key_ledit.setToolTip(u"Enter an eInk Kindle serial number. EInk Kindle serial numbers are 16 characters long and usually start with a 'B' or a '9'. Kindle Serial Numbers are case-sensitive, so be sure to enter the upper and lower case letters unchanged.")
+ key_group.addWidget(self.key_ledit)
+ key_label = QLabel(_(''), self)
+ key_label.setAlignment(Qt.AlignHCenter)
+ data_group_box_layout.addWidget(key_label)
+
+ self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+ self.button_box.accepted.connect(self.accept)
+ self.button_box.rejected.connect(self.reject)
+ layout.addWidget(self.button_box)
+
+ self.resize(self.sizeHint())
+
+ @property
+ def key_name(self):
+ return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
+
+ @property
+ def key_value(self):
+ return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
+
+ def accept(self):
+ if len(self.key_name) == 0 or self.key_name.isspace():
+ errmsg = u"Please enter an eInk Kindle Serial Number or click Cancel in the dialog."
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+ if len(self.key_name) != 16:
+ errmsg = u"EInk Kindle Serial Numbers must be 16 characters long. This is {0:d} characters long.".format(len(self.key_name))
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+ QDialog.accept(self)
+
+
+class AddPIDDialog(QDialog):
+ def __init__(self, parent=None,):
+ QDialog.__init__(self, parent)
+ self.parent = parent
+ self.setWindowTitle(u"{0} {1}: Add New Mobipocket PID".format(PLUGIN_NAME, PLUGIN_VERSION))
+ layout = QVBoxLayout(self)
+ self.setLayout(layout)
+
+ data_group_box = QGroupBox(u"", self)
+ layout.addWidget(data_group_box)
+ data_group_box_layout = QVBoxLayout()
+ data_group_box.setLayout(data_group_box_layout)
+
+ key_group = QHBoxLayout()
+ data_group_box_layout.addLayout(key_group)
+ key_group.addWidget(QLabel(u"PID:", self))
+ self.key_ledit = QLineEdit("", self)
+ self.key_ledit.setToolTip(u"Enter a Mobipocket PID. Mobipocket PIDs are 8 or 10 characters long. Mobipocket PIDs are case-sensitive, so be sure to enter the upper and lower case letters unchanged.")
+ key_group.addWidget(self.key_ledit)
+ key_label = QLabel(_(''), self)
+ key_label.setAlignment(Qt.AlignHCenter)
+ data_group_box_layout.addWidget(key_label)
+
+ self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+ self.button_box.accepted.connect(self.accept)
+ self.button_box.rejected.connect(self.reject)
+ layout.addWidget(self.button_box)
+
+ self.resize(self.sizeHint())
+
+ @property
+ def key_name(self):
+ return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
+
+ @property
+ def key_value(self):
+ return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
+
+ def accept(self):
+ if len(self.key_name) == 0 or self.key_name.isspace():
+ errmsg = u"Please enter a Mobipocket PID or click Cancel in the dialog."
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+ if len(self.key_name) != 8 and len(self.key_name) != 10:
+ errmsg = u"Mobipocket PIDs must be 8 or 10 characters long. This is {0:d} characters long.".format(len(self.key_name))
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+ QDialog.accept(self)
+
+
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/epubtest.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/epubtest.py
index a44308e..d91624f 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/epubtest.py
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/epubtest.py
@@ -7,8 +7,8 @@
# 1.00 - Initial version, with code from various other scripts
# 1.01 - Moved authorship announcement to usage section.
#
-# Changelog drmcheck
-# 1.00 - Cut to drmtest.py, testing ePub files only by Apprentice Alf
+# Changelog epubtest
+# 1.00 - Cut to epubtest.py, testing ePub files only by Apprentice Alf
#
# Written in 2011 by Paul Durrant
# Released with unlicense. See http://unlicense.org/
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/erdr2pml.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/erdr2pml.py
index 239c5ac..d982a44 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/erdr2pml.py
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/erdr2pml.py
@@ -70,7 +70,7 @@
__version__='0.22'
import sys, re
-import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile
+import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile, traceback
if 'calibre' in sys.modules:
inCalibre = True
@@ -139,28 +139,28 @@ Des = None
if iswindows:
# first try with pycrypto
if inCalibre:
- from calibre_plugins.erdrpdb2pml import pycrypto_des
+ from calibre_plugins.dedrm import pycrypto_des
else:
import pycrypto_des
Des = pycrypto_des.load_pycrypto()
if Des == None:
# they try with openssl
if inCalibre:
- from calibre_plugins.erdrpdb2pml import openssl_des
+ from calibre_plugins.dedrm import openssl_des
else:
import openssl_des
Des = openssl_des.load_libcrypto()
else:
# first try with openssl
if inCalibre:
- from calibre_plugins.erdrpdb2pml import openssl_des
+ from calibre_plugins.dedrm import openssl_des
else:
import openssl_des
Des = openssl_des.load_libcrypto()
if Des == None:
# then try with pycrypto
if inCalibre:
- from calibre_plugins.erdrpdb2pml import pycrypto_des
+ from calibre_plugins.dedrm import pycrypto_des
else:
import pycrypto_des
Des = pycrypto_des.load_pycrypto()
@@ -169,7 +169,7 @@ else:
# of DES and try to speed it up with Psycho
if Des == None:
if inCalibre:
- from calibre_plugins.erdrpdb2pml import python_des
+ from calibre_plugins.dedrm import python_des
else:
import python_des
Des = python_des.Des
@@ -522,7 +522,8 @@ def decryptBook(infile, outpath, make_pmlz, user_key):
print u"Output is in {0}".format(outdir)
print "done"
except ValueError, e:
- print u"Error: {0}".format(e.args[0])
+ print u"Error: {0}".format(e)
+ traceback.print_exc()
return 1
return 0
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/genbook.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/genbook.py
index 746178f..3ed925d 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/genbook.py
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/genbook.py
@@ -29,10 +29,10 @@ else:
inCalibre = False
if inCalibre :
- from calibre_plugins.k4mobidedrm import convert2xml
- from calibre_plugins.k4mobidedrm import flatxml2html
- from calibre_plugins.k4mobidedrm import flatxml2svg
- from calibre_plugins.k4mobidedrm import stylexml2css
+ from calibre_plugins.dedrm import convert2xml
+ from calibre_plugins.dedrm import flatxml2html
+ from calibre_plugins.dedrm import flatxml2svg
+ from calibre_plugins.dedrm import stylexml2css
else :
import convert2xml
import flatxml2html
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/getk4pcpids.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/getk4pcpids.py
deleted file mode 100644
index 1614a53..0000000
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/getk4pcpids.py
+++ /dev/null
@@ -1,84 +0,0 @@
-#!/usr/bin/python
-#
-# This is a python script. You need a Python interpreter to run it.
-# For example, ActiveState Python, which exists for windows.
-#
-# Changelog
-# 1.00 - Initial version
-# 1.01 - getPidList interface change
-
-__version__ = '1.01'
-
-import sys
-
-class SafeUnbuffered:
- def __init__(self, stream):
- self.stream = stream
- self.encoding = stream.encoding
- if self.encoding == None:
- self.encoding = "utf-8"
- def write(self, data):
- if isinstance(data,unicode):
- data = data.encode(self.encoding,"replace")
- self.stream.write(data)
- self.stream.flush()
- def __getattr__(self, attr):
- return getattr(self.stream, attr)
-sys.stdout=SafeUnbuffered(sys.stdout)
-sys.stderr=SafeUnbuffered(sys.stderr)
-
-import os
-import struct
-import binascii
-import kgenpids
-import topazextract
-import mobidedrm
-from alfcrypto import Pukall_Cipher
-
-class DrmException(Exception):
- pass
-
-def getK4PCpids(path_to_ebook):
- # Return Kindle4PC PIDs. Assumes that the caller checked that we are not on Linux, which will raise an exception
-
- mobi = True
- magic3 = file(path_to_ebook,'rb').read(3)
- if magic3 == 'TPZ':
- mobi = False
-
- if mobi:
- mb = mobidedrm.MobiBook(path_to_ebook)
- else:
- mb = topazextract.TopazBook(path_to_ebook)
-
- md1, md2 = mb.getPIDMetaInfo()
-
- return kgenpids.getPidList(md1, md2)
-
-
-def main(argv=sys.argv):
- print ('getk4pcpids.py v%(__version__)s. '
- 'Copyright 2012 Apprentice Alf' % globals())
-
- if len(argv)<2 or len(argv)>3:
- print "Gets the possible book-specific PIDs from K4PC for a particular book"
- print "Usage:"
- print " %s <bookfile> [<outfile>]" % sys.argv[0]
- return 1
- else:
- infile = argv[1]
- try:
- pidlist = getK4PCpids(infile)
- except DrmException, e:
- print "Error: %s" % e
- return 1
- pidstring = ','.join(pidlist)
- print "Possible PIDs are: ", pidstring
- if len(argv) is 3:
- outfile = argv[2]
- file(outfile, 'w').write(pidstring)
-
- return 0
-
-if __name__ == "__main__":
- sys.exit(main())
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignobleepub.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignobleepub.py
index b7cbdc5..4cf74ae 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignobleepub.py
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignobleepub.py
@@ -3,13 +3,13 @@
from __future__ import with_statement
-# ignobleepub.pyw, version 3.7
+# ignobleepub.pyw, version 3.8
# Copyright © 2009-2010 by i♥cabbages
# Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/>
-# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf
+# Modified 2010–2013 by some_updates, DiapDealer and Apprentice Alf
# Windows users: Before running this program, you must first install Python 2.6
# from <http://www.python.org/download/> and PyCrypto from
@@ -32,20 +32,21 @@ from __future__ import with_statement
# 3.5 - Fix for potential problem with PyCrypto
# 3.6 - Revised to allow use in calibre plugins to eliminate need for duplicate code
# 3.7 - Tweaked to match ineptepub more closely
+# 3.8 - Fixed to retain zip file metadata (e.g. file modification date)
"""
Decrypt Barnes & Noble encrypted ePub books.
"""
__license__ = 'GPL v3'
-__version__ = "3.7"
+__version__ = "3.8"
import sys
import os
import traceback
import zlib
import zipfile
-from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
+from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
from contextlib import closing
import xml.etree.ElementTree as etree
@@ -200,13 +201,6 @@ META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
NSMAP = {'adept': 'http://ns.adobe.com/adept',
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
-class ZipInfo(zipfile.ZipInfo):
- def __init__(self, *args, **kwargs):
- if 'compress_type' in kwargs:
- compress_type = kwargs.pop('compress_type')
- super(ZipInfo, self).__init__(*args, **kwargs)
- self.compress_type = compress_type
-
class Decryptor(object):
def __init__(self, bookkey, encryption):
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
@@ -282,11 +276,40 @@ def decryptBook(keyb64, inpath, outpath):
decryptor = Decryptor(bookkey[-16:], encryption)
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
- zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
+ zi = ZipInfo('mimetype')
+ zi.compress_type=ZIP_STORED
+ try:
+ # if the mimetype is present, get its info, including time-stamp
+ oldzi = inf.getinfo('mimetype')
+ # copy across fields to be preserved
+ zi.date_time = oldzi.date_time
+ zi.comment = oldzi.comment
+ zi.extra = oldzi.extra
+ zi.internal_attr = oldzi.internal_attr
+ # external attributes are dependent on the create system, so copy both.
+ zi.external_attr = oldzi.external_attr
+ zi.create_system = oldzi.create_system
+ except:
+ pass
outf.writestr(zi, inf.read('mimetype'))
for path in namelist:
data = inf.read(path)
- outf.writestr(path, decryptor.decrypt(path, data))
+ zi = ZipInfo(path)
+ zi.compress_type=ZIP_DEFLATED
+ try:
+ # get the file info, including time-stamp
+ oldzi = inf.getinfo(path)
+ # copy across useful fields
+ zi.date_time = oldzi.date_time
+ zi.comment = oldzi.comment
+ zi.extra = oldzi.extra
+ zi.internal_attr = oldzi.internal_attr
+ # external attributes are dependent on the create system, so copy both.
+ zi.external_attr = oldzi.external_attr
+ zi.create_system = oldzi.create_system
+ except:
+ pass
+ outf.writestr(zi, decryptor.decrypt(path, data))
except:
print u"Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc())
return 2
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignoblekeygen.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignoblekeygen.py
index f25359c..ec78e65 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignoblekeygen.py
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignoblekeygen.py
@@ -4,21 +4,23 @@
from __future__ import with_statement
# ignoblekeygen.pyw, version 2.5
-# Copyright © 2009-2010 by i♥cabbages
+# Copyright © 2009-2010 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 2010–2013 by some_updates, DiapDealer and Apprentice Alf
-# Windows users: Before running this program, you must first install Python 2.6
-# from <http://www.python.org/download/> and PyCrypto from
-# <http://www.voidspace.org.uk/python/modules.shtml#pycrypto> (make sure to
-# install the version for Python 2.6). Save this script file as
-# ignoblekeygen.pyw and double-click on it to run it.
+# Windows users: Before running this program, you must first install Python.
+# We recommend ActiveState Python 2.7.X for Windows (x86) from
+# http://www.activestate.com/activepython/downloads.
+# You must also install PyCrypto from
+# http://www.voidspace.org.uk/python/modules.shtml#pycrypto
+# (make certain to install the version for Python 2.7).
+# Then save this script file as ignoblekeygen.pyw and double-click on it to run it.
#
# Mac OS X users: Save this script file as ignoblekeygen.pyw. You can run this
-# program from the command line (pythonw ignoblekeygen.pyw) or by double-clicking
+# program from the command line (python ignoblekeygen.pyw) or by double-clicking
# it when it has been associated with PythonLauncher.
# Revision history:
@@ -58,8 +60,11 @@ class SafeUnbuffered:
def __getattr__(self, attr):
return getattr(self.stream, attr)
-iswindows = sys.platform.startswith('win')
-isosx = sys.platform.startswith('darwin')
+try:
+ from calibre.constants import iswindows, isosx
+except:
+ iswindows = sys.platform.startswith('win')
+ isosx = sys.platform.startswith('darwin')
def unicode_argv():
if iswindows:
@@ -68,8 +73,8 @@ def unicode_argv():
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
- # characters with '?'.
-
+ # characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
+ # as a list of Unicode strings and encode them as utf-8
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptepub.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptepub.py
index 48b7727..98a134e 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptepub.py
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptepub.py
@@ -3,13 +3,13 @@
from __future__ import with_statement
-# ineptepub.pyw, version 5.8
+# ineptepub.pyw, version 5.9
# 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 2010–2013 by some_updates, DiapDealer and Apprentice Alf
# Windows users: Before running this program, you must first install Python 2.6
# from <http://www.python.org/download/> and PyCrypto from
@@ -34,20 +34,21 @@ from __future__ import with_statement
# 5.6 - Modify interface to allow use with import
# 5.7 - Fix for potential problem with PyCrypto
# 5.8 - Revised to allow use in calibre plugins to eliminate need for duplicate code
+# 5.9 - Fixed to retain zip file metadata (e.g. file modification date)
"""
Decrypt Adobe Digital Editions encrypted ePub books.
"""
__license__ = 'GPL v3'
-__version__ = "5.8"
+__version__ = "5.9"
import sys
import os
import traceback
import zlib
import zipfile
-from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
+from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
from contextlib import closing
import xml.etree.ElementTree as etree
@@ -340,13 +341,6 @@ META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
NSMAP = {'adept': 'http://ns.adobe.com/adept',
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
-class ZipInfo(zipfile.ZipInfo):
- def __init__(self, *args, **kwargs):
- if 'compress_type' in kwargs:
- compress_type = kwargs.pop('compress_type')
- super(ZipInfo, self).__init__(*args, **kwargs)
- self.compress_type = compress_type
-
class Decryptor(object):
def __init__(self, bookkey, encryption):
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
@@ -424,11 +418,40 @@ def decryptBook(userkey, inpath, outpath):
decryptor = Decryptor(bookkey[-16:], encryption)
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
- zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
+ zi = ZipInfo('mimetype')
+ zi.compress_type=ZIP_STORED
+ try:
+ # if the mimetype is present, get its info, including time-stamp
+ oldzi = inf.getinfo('mimetype')
+ # copy across fields to be preserved
+ zi.date_time = oldzi.date_time
+ zi.comment = oldzi.comment
+ zi.extra = oldzi.extra
+ zi.internal_attr = oldzi.internal_attr
+ # external attributes are dependent on the create system, so copy both.
+ zi.external_attr = oldzi.external_attr
+ zi.create_system = oldzi.create_system
+ except:
+ pass
outf.writestr(zi, inf.read('mimetype'))
for path in namelist:
data = inf.read(path)
- outf.writestr(path, decryptor.decrypt(path, data))
+ zi = ZipInfo(path)
+ zi.compress_type=ZIP_DEFLATED
+ try:
+ # get the file info, including time-stamp
+ oldzi = inf.getinfo(path)
+ # copy across useful fields
+ zi.date_time = oldzi.date_time
+ zi.comment = oldzi.comment
+ zi.extra = oldzi.extra
+ zi.internal_attr = oldzi.internal_attr
+ # external attributes are dependent on the create system, so copy both.
+ zi.external_attr = oldzi.external_attr
+ zi.create_system = oldzi.create_system
+ except:
+ pass
+ outf.writestr(zi, decryptor.decrypt(path, data))
except:
print u"Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc())
return 2
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py
index 70ed898..1ae5c88 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py
@@ -51,8 +51,10 @@ from __future__ import with_statement
# 4.8 - Much better unicode handling, matching the updated inept and ignoble scripts
# - Moved back into plugin, __init__ in plugin now only contains plugin code.
# 4.9 - Missed some invalid characters in cleanup_name
+# 5.0 - Extraction of info from Kindle for PC/Mac moved into kindlekey.py
+# - tweaked GetDecryptedBook interface to leave passed parameters unchanged
-__version__ = '4.9'
+__version__ = '5.0'
import sys, os, re
@@ -62,6 +64,7 @@ import re
import traceback
import time
import htmlentitydefs
+import json
class DrmException(Exception):
pass
@@ -72,9 +75,9 @@ else:
inCalibre = False
if inCalibre:
- from calibre_plugins.k4mobidedrm import mobidedrm
- from calibre_plugins.k4mobidedrm import topazextract
- from calibre_plugins.k4mobidedrm import kgenpids
+ from calibre_plugins.dedrm import mobidedrm
+ from calibre_plugins.dedrm import topazextract
+ from calibre_plugins.dedrm import kgenpids
else:
import mobidedrm
import topazextract
@@ -180,13 +183,13 @@ def unescape(text):
return text # leave as is
return re.sub(u"&#?\w+;", fixup, text)
-def GetDecryptedBook(infile, kInfoFiles, serials, pids, starttime = time.time()):
+def GetDecryptedBook(infile, kDatabases, serials, pids, starttime = time.time()):
# handle the obvious cases at the beginning
if not os.path.isfile(infile):
raise DRMException (u"Input file does not exist.")
mobi = True
- magic3 = file(infile,'rb').read(3)
+ magic3 = open(infile,'rb').read(3)
if magic3 == 'TPZ':
mobi = False
@@ -198,13 +201,15 @@ def GetDecryptedBook(infile, kInfoFiles, serials, pids, starttime = time.time())
bookname = unescape(mb.getBookTitle())
print u"Decrypting {1} ebook: {0}".format(bookname, mb.getBookType())
+ # copy list of pids
+ totalpids = list(pids)
# extend PID list with book-specific PIDs
md1, md2 = mb.getPIDMetaInfo()
- pids.extend(kgenpids.getPidList(md1, md2, serials, kInfoFiles))
- print u"Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(pids))
+ totalpids.extend(kgenpids.getPidList(md1, md2, serials, kDatabases))
+ print u"Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(totalpids))
try:
- mb.processBook(pids)
+ mb.processBook(totalpids)
except:
mb.cleanup
raise
@@ -213,12 +218,24 @@ def GetDecryptedBook(infile, kInfoFiles, serials, pids, starttime = time.time())
return mb
-# infile, outdir and kInfoFiles should be unicode strings
-def decryptBook(infile, outdir, kInfoFiles, serials, pids):
+# kDatabaseFiles is a list of files created by kindlekey
+def decryptBook(infile, outdir, kDatabaseFiles, serials, pids):
starttime = time.time()
- print "Starting decryptBook routine."
+ kDatabases = []
+ for dbfile in kDatabaseFiles:
+ kindleDatabase = {}
+ try:
+ with open(dbfile, 'r') as keyfilein:
+ kindleDatabase = json.loads(keyfilein.read())
+ kDatabases.append([dbfile,kindleDatabase])
+ except Exception, e:
+ print u"Error getting database from file {0:s}: {1:s}".format(dbfile,e)
+ traceback.print_exc()
+
+
+
try:
- book = GetDecryptedBook(infile, kInfoFiles, serials, pids, starttime)
+ book = GetDecryptedBook(infile, kDatabases, serials, pids, starttime)
except Exception, e:
print u"Error decrypting book after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime)
traceback.print_exc()
@@ -254,14 +271,14 @@ def decryptBook(infile, outdir, kInfoFiles, serials, pids):
def usage(progname):
print u"Removes DRM protection from Mobipocket, Amazon KF8, Amazon Print Replica and Amazon Topaz ebooks"
print u"Usage:"
- print u" {0} [-k <kindle.info>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] <infile> <outdir>".format(progname)
+ print u" {0} [-k <kindle.k4i>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] <infile> <outdir>".format(progname)
#
# Main
#
def cli_main(argv=unicode_argv()):
progname = os.path.basename(argv[0])
- print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__)
+ print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2013 The Dark Reverser et al.".format(__version__)
try:
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:")
@@ -275,7 +292,7 @@ def cli_main(argv=unicode_argv()):
infile = args[0]
outdir = args[1]
- kInfoFiles = []
+ kDatabaseFiles = []
serials = []
pids = []
@@ -283,7 +300,7 @@ def cli_main(argv=unicode_argv()):
if o == "-k":
if a == None :
raise DrmException("Invalid parameter for -k")
- kInfoFiles.append(a)
+ kDatabaseFiles.append(a)
if o == "-p":
if a == None :
raise DrmException("Invalid parameter for -p")
@@ -296,7 +313,7 @@ def cli_main(argv=unicode_argv()):
# try with built in Kindle Info files if not on Linux
k4 = not sys.platform.startswith('linux')
- return decryptBook(infile, outdir, kInfoFiles, serials, pids)
+ return decryptBook(infile, outdir, kDatabaseFiles, serials, pids)
if __name__ == '__main__':
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mutils.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mutils.py
deleted file mode 100644
index bceb3a3..0000000
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mutils.py
+++ /dev/null
@@ -1,747 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-# standlone set of Mac OSX specific routines needed for KindleBooks
-
-from __future__ import with_statement
-
-import sys
-import os
-import os.path
-import re
-import copy
-import subprocess
-from struct import pack, unpack, unpack_from
-
-class DrmException(Exception):
- pass
-
-
-# interface to needed routines in openssl's libcrypto
-def _load_crypto_libcrypto():
- from ctypes import CDLL, byref, POINTER, c_void_p, c_char_p, c_int, c_long, \
- Structure, c_ulong, create_string_buffer, addressof, string_at, cast
- from ctypes.util import find_library
-
- libcrypto = find_library('crypto')
- if libcrypto is None:
- raise DrmException(u"libcrypto not found")
- libcrypto = CDLL(libcrypto)
-
- # From OpenSSL's crypto aes header
- #
- # AES_ENCRYPT 1
- # AES_DECRYPT 0
- # AES_MAXNR 14 (in bytes)
- # AES_BLOCK_SIZE 16 (in bytes)
- #
- # struct aes_key_st {
- # unsigned long rd_key[4 *(AES_MAXNR + 1)];
- # int rounds;
- # };
- # typedef struct aes_key_st AES_KEY;
- #
- # int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key);
- #
- # note: the ivec string, and output buffer are both mutable
- # void AES_cbc_encrypt(const unsigned char *in, unsigned char *out,
- # const unsigned long length, const AES_KEY *key, unsigned char *ivec, const int enc);
-
- AES_MAXNR = 14
- c_char_pp = POINTER(c_char_p)
- c_int_p = POINTER(c_int)
-
- class AES_KEY(Structure):
- _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)]
- AES_KEY_p = POINTER(AES_KEY)
-
- def F(restype, name, argtypes):
- func = getattr(libcrypto, name)
- func.restype = restype
- func.argtypes = argtypes
- return func
-
- AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,c_int])
-
- AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',[c_char_p, c_int, AES_KEY_p])
-
- # From OpenSSL's Crypto evp/p5_crpt2.c
- #
- # int PKCS5_PBKDF2_HMAC_SHA1(const char *pass, int passlen,
- # const unsigned char *salt, int saltlen, int iter,
- # int keylen, unsigned char *out);
-
- PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1',
- [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p])
-
- class LibCrypto(object):
- def __init__(self):
- self._blocksize = 0
- self._keyctx = None
- self._iv = 0
-
- def set_decrypt_key(self, userkey, iv):
- self._blocksize = len(userkey)
- if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
- raise DrmException(u"AES improper key used")
- return
- keyctx = self._keyctx = AES_KEY()
- self._iv = iv
- self._userkey = userkey
- rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
- if rv < 0:
- raise DrmException(u"Failed to initialize AES key")
-
- def decrypt(self, data):
- out = create_string_buffer(len(data))
- mutable_iv = create_string_buffer(self._iv, len(self._iv))
- keyctx = self._keyctx
- rv = AES_cbc_encrypt(data, out, len(data), keyctx, mutable_iv, 0)
- if rv == 0:
- raise DrmException(u"AES decryption failed")
- return out.raw
-
- def keyivgen(self, passwd, salt, iter, keylen):
- saltlen = len(salt)
- passlen = len(passwd)
- out = create_string_buffer(keylen)
- rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out)
- return out.raw
- return LibCrypto
-
-def _load_crypto():
- LibCrypto = None
- try:
- LibCrypto = _load_crypto_libcrypto()
- except (ImportError, DrmException):
- pass
- return LibCrypto
-
-LibCrypto = _load_crypto()
-
-#
-# Utility Routines
-#
-
-# crypto digestroutines
-import hashlib
-
-def MD5(message):
- ctx = hashlib.md5()
- ctx.update(message)
- return ctx.digest()
-
-def SHA1(message):
- ctx = hashlib.sha1()
- ctx.update(message)
- return ctx.digest()
-
-def SHA256(message):
- ctx = hashlib.sha256()
- ctx.update(message)
- return ctx.digest()
-
-# Various character maps used to decrypt books. Probably supposed to act as obfuscation
-charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M'
-charMap2 = 'ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM'
-
-# For kinf approach of K4Mac 1.6.X or later
-# On K4PC charMap5 = 'AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE'
-# For Mac they seem to re-use charMap2 here
-charMap5 = charMap2
-
-# new in K4M 1.9.X
-testMap8 = 'YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD'
-
-
-def encode(data, map):
- result = ''
- for char in data:
- value = ord(char)
- Q = (value ^ 0x80) // len(map)
- R = value % len(map)
- result += map[Q]
- result += map[R]
- return result
-
-# Hash the bytes in data and then encode the digest with the characters in map
-def encodeHash(data,map):
- return encode(MD5(data),map)
-
-# Decode the string in data with the characters in map. Returns the decoded bytes
-def decode(data,map):
- result = ''
- for i in range (0,len(data)-1,2):
- high = map.find(data[i])
- low = map.find(data[i+1])
- if (high == -1) or (low == -1) :
- break
- value = (((high * len(map)) ^ 0x80) & 0xFF) + low
- result += pack('B',value)
- return result
-
-# For K4M 1.6.X and later
-# generate table of prime number less than or equal to int n
-def primes(n):
- if n==2: return [2]
- elif n<2: return []
- s=range(3,n+1,2)
- mroot = n ** 0.5
- half=(n+1)/2-1
- i=0
- m=3
- while m <= mroot:
- if s[i]:
- j=(m*m-3)/2
- s[j]=0
- while j<half:
- s[j]=0
- j+=m
- i=i+1
- m=2*i+3
- return [2]+[x for x in s if x]
-
-
-# uses a sub process to get the Hard Drive Serial Number using ioreg
-# returns with the serial number of drive whose BSD Name is 'disk0'
-def GetVolumeSerialNumber():
- sernum = os.getenv('MYSERIALNUMBER')
- if sernum != None:
- return sernum
- cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver'
- cmdline = cmdline.encode(sys.getfilesystemencoding())
- p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
- out1, out2 = p.communicate()
- 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\" = \"')
- if pp >= 0:
- sernum = resline[pp+19:-1]
- sernum = sernum.strip()
- bb = resline.find('\"BSD Name\" = \"')
- if bb >= 0:
- bsdname = resline[bb+14:-1]
- bsdname = bsdname.strip()
- if (bsdname == 'disk0') and (sernum != None):
- foundIt = True
- break
- if not foundIt:
- sernum = ''
- return sernum
-
-def GetUserHomeAppSupKindleDirParitionName():
- home = os.getenv('HOME')
- dpath = home + '/Library'
- cmdline = '/sbin/mount'
- cmdline = cmdline.encode(sys.getfilesystemencoding())
- p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
- out1, out2 = p.communicate()
- reslst = out1.split('\n')
- cnt = len(reslst)
- disk = ''
- foundIt = False
- for j in xrange(cnt):
- resline = reslst[j]
- if resline.startswith('/dev'):
- (devpart, mpath) = resline.split(' on ')
- dpart = devpart[5:]
- pp = mpath.find('(')
- if pp >= 0:
- mpath = mpath[:pp-1]
- if dpath.startswith(mpath):
- disk = dpart
- return disk
-
-# uses a sub process to get the UUID of the specified disk partition using ioreg
-def GetDiskPartitionUUID(diskpart):
- uuidnum = os.getenv('MYUUIDNUMBER')
- if uuidnum != None:
- return uuidnum
- cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver'
- cmdline = cmdline.encode(sys.getfilesystemencoding())
- p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
- out1, out2 = p.communicate()
- 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 not foundIt:
- uuidnum = ''
- return uuidnum
-
-def GetMACAddressMunged():
- macnum = os.getenv('MYMACNUM')
- if macnum != None:
- return macnum
- cmdline = '/sbin/ifconfig 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 ')
- if pp >= 0:
- macnum = resline[pp+6:-1]
- 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
- for i in range(6):
- maclst[i] = int('0x' + maclst[i], 0)
- mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
- mlst[5] = maclst[5] ^ 0xa5
- mlst[4] = maclst[3] ^ 0xa5
- mlst[3] = maclst[4] ^ 0xa5
- mlst[2] = maclst[2] ^ 0xa5
- mlst[1] = maclst[1] ^ 0xa5
- mlst[0] = maclst[0] ^ 0xa5
- macnum = '%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x' % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5])
- foundIt = True
- break
- if not foundIt:
- macnum = ''
- return macnum
-
-
-# uses unix env to get username instead of using sysctlbyname
-def GetUserName():
- username = os.getenv('USER')
- return username
-
-def isNewInstall():
- home = os.getenv('HOME')
- # soccer game fan anyone
- dpath = home + '/Library/Application Support/Kindle/storage/.pes2011'
- # print dpath, os.path.exists(dpath)
- if os.path.exists(dpath):
- return True
- dpath = home + '/Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/storage/.pes2011'
- # print dpath, os.path.exists(dpath)
- if os.path.exists(dpath):
- return True
- return False
-
-
-class Memoize:
- """Memoize(fn) - an instance which acts like fn but memoizes its arguments
- Will only work on functions with non-mutable arguments
- """
- def __init__(self, fn):
- self.fn = fn
- self.memo = {}
- def __call__(self, *args):
- if not self.memo.has_key(args):
- self.memo[args] = self.fn(*args)
- return self.memo[args]
-
-@Memoize
-def GetIDString():
- # K4Mac now has an extensive set of ids strings it uses
- # in encoding pids and in creating unique passwords
- # for use in its own version of CryptUnprotectDataV2
-
- # BUT Amazon has now become nasty enough to detect when its app
- # is being run under a debugger and actually changes code paths
- # including which one of these strings is chosen, all to try
- # to prevent reverse engineering
-
- # Sad really ... they will only hurt their own sales ...
- # true book lovers really want to keep their books forever
- # and move them to their devices and DRM prevents that so they
- # will just buy from someplace else that they can remove
- # the DRM from
-
- # Amazon should know by now that true book lover's are not like
- # penniless kids that pirate music, we do not pirate books
-
- if isNewInstall():
- mungedmac = GetMACAddressMunged()
- if len(mungedmac) > 7:
- print('Using Munged MAC Address for ID: '+mungedmac)
- return mungedmac
- sernum = GetVolumeSerialNumber()
- if len(sernum) > 7:
- print('Using Volume Serial Number for ID: '+sernum)
- return sernum
- diskpart = GetUserHomeAppSupKindleDirParitionName()
- uuidnum = GetDiskPartitionUUID(diskpart)
- if len(uuidnum) > 7:
- print('Using Disk Partition UUID for ID: '+uuidnum)
- return uuidnum
- mungedmac = GetMACAddressMunged()
- if len(mungedmac) > 7:
- print('Using Munged MAC Address for ID: '+mungedmac)
- return mungedmac
- print('Using Fixed constant 9999999999 for ID.')
- return '9999999999'
-
-
-# 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):
- sernum = GetVolumeSerialNumber()
- if sernum == '':
- sernum = '9999999999'
- sp = sernum + '!@#' + 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):
- sp = GetUserName() + ':&%:' + GetIDString()
- 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):
- passwdData = 'header_key_data'
- salt = 'HEADER.2011'
- iter = 0x80
- keylen = 0x100
- crp = LibCrypto()
- key_iv = crp.keyivgen(passwdData, salt, iter, keylen)
- key = key_iv[0:32]
- iv = key_iv[32:48]
- crp.set_decrypt_key(key,iv)
- cleartext = crp.decrypt(encryptedData)
- return cleartext
-
-
-# implements an Pseudo Mac Version of Windows built-in Crypto routine
-# used for Kindle for Mac Versions >= 1.9.0
-class CryptUnprotectDataV3(object):
- def __init__(self, entropy):
- sp = GetUserName() + '+@#$%+' + GetIDString()
- passwdData = encode(SHA256(sp),charMap2)
- salt = entropy
- self.crp = LibCrypto()
- iter = 0x800
- keylen = 0x400
- key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen)
- self.key = key_iv[0:32]
- self.iv = key_iv[32:48]
- self.crp.set_decrypt_key(self.key, self.iv)
-
- def decrypt(self, encryptedData):
- cleartext = self.crp.decrypt(encryptedData)
- cleartext = decode(cleartext, charMap2)
- return cleartext
-
-
-# Locate the .kindle-info files
-def getKindleInfoFiles():
- # file searches can take a long time on some systems, so just look in known specific places.
- kInfoFiles=[]
- found = False
- home = os.getenv('HOME')
- # check for .kinf2011 file in new location (App Store Kindle for Mac)
- testpath = home + '/Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/storage/.kinf2011'
- if os.path.isfile(testpath):
- kInfoFiles.append(testpath)
- print('Found k4Mac kinf2011 file: ' + testpath)
- found = True
- # check for .kinf2011 files
- testpath = home + '/Library/Application Support/Kindle/storage/.kinf2011'
- if os.path.isfile(testpath):
- kInfoFiles.append(testpath)
- print('Found k4Mac kinf2011 file: ' + testpath)
- found = True
- # check for .rainier-2.1.1-kinf files
- testpath = home + '/Library/Application Support/Kindle/storage/.rainier-2.1.1-kinf'
- if os.path.isfile(testpath):
- kInfoFiles.append(testpath)
- print('Found k4Mac rainier file: ' + testpath)
- found = True
- # check for .rainier-2.1.1-kinf files
- testpath = home + '/Library/Application Support/Kindle/storage/.kindle-info'
- if os.path.isfile(testpath):
- kInfoFiles.append(testpath)
- print('Found k4Mac kindle-info file: ' + testpath)
- found = True
- if not found:
- print('No k4Mac kindle-info/rainier/kinf2011 files have been found.')
- return kInfoFiles
-
-# determine type of kindle info provided and return a
-# database of keynames and values
-def getDBfromFile(kInfoFile):
-
- names = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber', 'max_date', 'SIGVERIF']
- DB = {}
- cnt = 0
- infoReader = open(kInfoFile, 'r')
- hdr = infoReader.read(1)
- data = infoReader.read()
-
- if data.find('[') != -1 :
-
- # older style kindle-info file
- cud = CryptUnprotectData()
- 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)
- DB[keyname] = cleartext
- cnt = cnt + 1
- if cnt == 0:
- DB = None
- return DB
-
- if hdr == '/':
-
- # else newer style .kinf file used by K4Mac >= 1.6.0
- # the .kinf file uses '/' to separate it into records
- # so remove the trailing '/' to make it easy to use split
- data = data[:-1]
- items = data.split('/')
- cud = CryptUnprotectDataV2()
-
- # 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'
-
- # 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 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,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 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)
- DB[keyname] = cleartext
- cnt = cnt + 1
-
- if cnt == 0:
- DB = None
- return DB
-
- # 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)
-
- # 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
- DB[keyname] = cleartext
- cnt = cnt + 1
-
- if cnt == 0:
- DB = None
- return DB
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4pcutils.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4pcutils.py
deleted file mode 100644
index bb9289e..0000000
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4pcutils.py
+++ /dev/null
@@ -1,457 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-# K4PC Windows specific routines
-
-from __future__ import with_statement
-
-import sys, os, re
-from struct import pack, unpack, unpack_from
-
-from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
- create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
- string_at, Structure, c_void_p, cast
-
-import _winreg as winreg
-MAX_PATH = 255
-kernel32 = windll.kernel32
-advapi32 = windll.advapi32
-crypt32 = windll.crypt32
-
-import traceback
-
-# crypto digestroutines
-import hashlib
-
-def MD5(message):
- ctx = hashlib.md5()
- ctx.update(message)
- return ctx.digest()
-
-def SHA1(message):
- ctx = hashlib.sha1()
- ctx.update(message)
- return ctx.digest()
-
-def SHA256(message):
- ctx = hashlib.sha256()
- ctx.update(message)
- return ctx.digest()
-
-# For K4PC 1.9.X
-# use routines in alfcrypto:
-# AES_cbc_encrypt
-# AES_set_decrypt_key
-# PKCS5_PBKDF2_HMAC_SHA1
-
-from alfcrypto import AES_CBC, KeyIVGen
-
-def UnprotectHeaderData(encryptedData):
- passwdData = 'header_key_data'
- salt = 'HEADER.2011'
- iter = 0x80
- keylen = 0x100
- key_iv = KeyIVGen().pbkdf2(passwdData, salt, iter, keylen)
- key = key_iv[0:32]
- iv = key_iv[32:48]
- aes=AES_CBC()
- aes.set_decrypt_key(key, iv)
- cleartext = aes.decrypt(encryptedData)
- return cleartext
-
-
-# simple primes table (<= n) calculator
-def primes(n):
- if n==2: return [2]
- elif n<2: return []
- s=range(3,n+1,2)
- mroot = n ** 0.5
- half=(n+1)/2-1
- i=0
- m=3
- while m <= mroot:
- if s[i]:
- j=(m*m-3)/2
- s[j]=0
- while j<half:
- s[j]=0
- j+=m
- i=i+1
- m=2*i+3
- return [2]+[x for x in s if x]
-
-
-# Various character maps used to decrypt kindle info values.
-# Probably supposed to act as obfuscation
-charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_"
-charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE"
-# New maps in K4PC 1.9.0
-testMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
-testMap6 = "9YzAb0Cd1Ef2n5Pr6St7Uvh3Jk4M8WxG"
-testMap8 = "YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD"
-
-class DrmException(Exception):
- pass
-
-# Encode the bytes in data with the characters in map
-def encode(data, map):
- result = ""
- for char in data:
- value = ord(char)
- Q = (value ^ 0x80) // len(map)
- R = value % len(map)
- result += map[Q]
- result += map[R]
- return result
-
-# Hash the bytes in data and then encode the digest with the characters in map
-def encodeHash(data,map):
- return encode(MD5(data),map)
-
-# Decode the string in data with the characters in map. Returns the decoded bytes
-def decode(data,map):
- result = ""
- for i in range (0,len(data)-1,2):
- high = map.find(data[i])
- low = map.find(data[i+1])
- if (high == -1) or (low == -1) :
- break
- value = (((high * len(map)) ^ 0x80) & 0xFF) + low
- result += pack("B",value)
- return result
-
-
-# interface with Windows OS Routines
-class DataBlob(Structure):
- _fields_ = [('cbData', c_uint),
- ('pbData', c_void_p)]
-DataBlob_p = POINTER(DataBlob)
-
-
-def GetSystemDirectory():
- GetSystemDirectoryW = kernel32.GetSystemDirectoryW
- GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint]
- GetSystemDirectoryW.restype = c_uint
- def GetSystemDirectory():
- buffer = create_unicode_buffer(MAX_PATH + 1)
- GetSystemDirectoryW(buffer, len(buffer))
- return buffer.value
- return GetSystemDirectory
-GetSystemDirectory = GetSystemDirectory()
-
-def GetVolumeSerialNumber():
- GetVolumeInformationW = kernel32.GetVolumeInformationW
- GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint,
- POINTER(c_uint), POINTER(c_uint),
- POINTER(c_uint), c_wchar_p, c_uint]
- GetVolumeInformationW.restype = c_uint
- def GetVolumeSerialNumber(path = GetSystemDirectory().split('\\')[0] + '\\'):
- vsn = c_uint(0)
- GetVolumeInformationW(path, None, 0, byref(vsn), None, None, None, 0)
- return str(vsn.value)
- return GetVolumeSerialNumber
-GetVolumeSerialNumber = GetVolumeSerialNumber()
-
-def GetIDString():
- vsn = GetVolumeSerialNumber()
- print('Using Volume Serial Number for ID: '+vsn)
- return vsn
-
-def getLastError():
- GetLastError = kernel32.GetLastError
- GetLastError.argtypes = None
- GetLastError.restype = c_uint
- def getLastError():
- return GetLastError()
- return getLastError
-getLastError = getLastError()
-
-def GetUserName():
- GetUserNameW = advapi32.GetUserNameW
- GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
- GetUserNameW.restype = c_uint
- def GetUserName():
- buffer = create_unicode_buffer(2)
- size = c_uint(len(buffer))
- while not GetUserNameW(buffer, byref(size)):
- errcd = getLastError()
- if errcd == 234:
- # bad wine implementation up through wine 1.3.21
- return "AlternateUserName"
- buffer = create_unicode_buffer(len(buffer) * 2)
- size.value = len(buffer)
- return buffer.value.encode('utf-16-le')[::2]
- return GetUserName
-GetUserName = GetUserName()
-
-def CryptUnprotectData():
- _CryptUnprotectData = crypt32.CryptUnprotectData
- _CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
- c_void_p, c_void_p, c_uint, DataBlob_p]
- _CryptUnprotectData.restype = c_uint
- def CryptUnprotectData(indata, entropy, flags):
- indatab = create_string_buffer(indata)
- indata = DataBlob(len(indata), cast(indatab, c_void_p))
- entropyb = create_string_buffer(entropy)
- entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
- outdata = DataBlob()
- if not _CryptUnprotectData(byref(indata), None, byref(entropy),
- None, None, flags, byref(outdata)):
- # raise DrmException("Failed to Unprotect Data")
- return 'failed'
- return string_at(outdata.pbData, outdata.cbData)
- return CryptUnprotectData
-CryptUnprotectData = CryptUnprotectData()
-
-
-# Locate all of the kindle-info style files and return as list
-def getKindleInfoFiles():
- kInfoFiles = []
- # some 64 bit machines do not have the proper registry key for some reason
- # or the python interface to the 32 vs 64 bit registry is broken
- path = ""
- if 'LOCALAPPDATA' in os.environ.keys():
- path = os.environ['LOCALAPPDATA']
- else:
- # User Shell Folders show take precedent over Shell Folders if present
- try:
- regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\")
- path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
- except WindowsError:
- pass
- if not os.path.isdir(path):
- path = ""
- try:
- regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
- path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
- except WindowsError:
- pass
- if not os.path.isdir(path):
- path = ""
-
- found = False
- if path == "":
- print ('Could not find the folder in which to look for kinfoFiles.')
- else:
- print('searching for kinfoFiles in ' + path)
-
- # first look for older kindle-info files
- kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info'
- if os.path.isfile(kinfopath):
- found = True
- print('Found K4PC kindle.info file: ' + kinfopath)
- kInfoFiles.append(kinfopath)
-
- # now look for newer (K4PC 1.5.0 and later rainier.2.1.1.kinf file
-
- kinfopath = path +'\\Amazon\\Kindle For PC\\storage\\rainier.2.1.1.kinf'
- if os.path.isfile(kinfopath):
- found = True
- print('Found K4PC 1.5.X kinf file: ' + kinfopath)
- kInfoFiles.append(kinfopath)
-
- # now look for even newer (K4PC 1.6.0 and later) rainier.2.1.1.kinf file
- kinfopath = path +'\\Amazon\\Kindle\\storage\\rainier.2.1.1.kinf'
- if os.path.isfile(kinfopath):
- found = True
- print('Found K4PC 1.6.X kinf file: ' + kinfopath)
- kInfoFiles.append(kinfopath)
-
- # now look for even newer (K4PC 1.9.0 and later) .kinf2011 file
- kinfopath = path +'\\Amazon\\Kindle\\storage\\.kinf2011'
- if os.path.isfile(kinfopath):
- found = True
- print('Found K4PC kinf2011 file: ' + kinfopath)
- kInfoFiles.append(kinfopath)
-
- if not found:
- print('No K4PC kindle.info/kinf/kinf2011 files have been found.')
- return kInfoFiles
-
-
-# determine type of kindle info provided and return a
-# database of keynames and values
-def getDBfromFile(kInfoFile):
- names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber", "max_date", "SIGVERIF"]
- DB = {}
- cnt = 0
- infoReader = open(kInfoFile, 'r')
- hdr = infoReader.read(1)
- data = infoReader.read()
-
- 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)
- cnt = cnt + 1
- if cnt == 0:
- DB = None
- return DB
-
- if 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('/')
-
- # 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 raw keyhash string is used to create entropy for the actual
- # CryptProtectData Blob that represents that keys contents
- entropy = SHA1(keyhash)
-
- # 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,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)
- cnt = cnt + 1
-
- if cnt == 0:
- DB = None
- return DB
-
- # 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)
-
-
- # 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)
- DB[keyname] = cleartext
- cnt = cnt + 1
-
- if cnt == 0:
- DB = None
- return DB
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kgenpids.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kgenpids.py
index c5de9b9..dd88797 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kgenpids.py
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kgenpids.py
@@ -8,6 +8,7 @@ import binascii
import zlib
import re
from struct import pack, unpack, unpack_from
+import traceback
class DrmException(Exception):
pass
@@ -16,22 +17,6 @@ global charMap1
global charMap3
global charMap4
-if 'calibre' in sys.modules:
- inCalibre = True
- from calibre.constants import iswindows, isosx
- if iswindows:
- from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
- if isosx:
- from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
-else:
- inCalibre = False
- iswindows = sys.platform.startswith('win')
- isosx = sys.platform.startswith('darwin')
- if iswindows:
- from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
- if isosx:
- from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
-
charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M'
charMap3 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
@@ -178,6 +163,9 @@ def pidFromSerial(s, l):
def getKindlePids(rec209, token, serialnum):
pids=[]
+ if isinstance(serialnum,unicode):
+ serialnum = serialnum.encode('ascii')
+
# Compute book PID
pidHash = SHA1(serialnum+rec209+token)
bookPID = encodePID(pidHash)
@@ -196,35 +184,32 @@ def getKindlePids(rec209, token, serialnum):
keynames = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber']
-def getK4Pids(rec209, token, kInfoFile):
+def getK4Pids(rec209, token, kindleDatabase):
global charMap1
- kindleDatabase = None
pids = []
- try:
- kindleDatabase = getDBfromFile(kInfoFile)
- except Exception, message:
- print(message)
- kindleDatabase = None
- pass
-
- if kindleDatabase == None :
- return pids
try:
# Get the Mazama Random number
- MazamaRandomNumber = kindleDatabase['MazamaRandomNumber']
+ MazamaRandomNumber = (kindleDatabase[1])['MazamaRandomNumber'].decode('hex').encode('ascii')
# Get the kindle account token
- kindleAccountToken = kindleDatabase['kindle.account.tokens']
+ kindleAccountToken = (kindleDatabase[1])['kindle.account.tokens'].decode('hex').encode('ascii')
+
+ # Get the IDString used to decode the Kindle Info file
+ IDString = (kindleDatabase[1])['IDString'].decode('hex').encode('ascii')
+
+ # Get the UserName stored when the Kindle Info file was decoded
+ UserName = (kindleDatabase[1])['UserName'].decode('hex').encode('ascii')
+
except KeyError:
- print u"Keys not found in {0}".format(os.path.basename(kInfoFile))
+ print u"Keys not found in the database {0}.".format(kindleDatabase[0])
return pids
# Get the ID string used
- encodedIDString = encodeHash(GetIDString(),charMap1)
+ encodedIDString = encodeHash(IDString,charMap1)
# Get the current user name
- encodedUsername = encodeHash(GetUserName(),charMap1)
+ encodedUsername = encodeHash(UserName,charMap1)
# concat, hash and encode to calculate the DSN
DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1)
@@ -257,22 +242,26 @@ def getK4Pids(rec209, token, kInfoFile):
return pids
-def getPidList(md1, md2, serials=[], kInfoFiles=[]):
+def getPidList(md1, md2, serials=[], kDatabases=[]):
pidlst = []
- if kInfoFiles is None:
- kInfoFiles = []
+
+ if kDatabases is None:
+ kDatabases = []
if serials is None:
serials = []
- if iswindows or isosx:
- kInfoFiles.extend(getKindleInfoFiles())
- for infoFile in kInfoFiles:
+
+ for kDatabase in kDatabases:
try:
- pidlst.extend(getK4Pids(md1, md2, infoFile))
+ pidlst.extend(getK4Pids(md1, md2, kDatabase))
except Exception, e:
- print u"Error getting PIDs from {0}: {1}".format(os.path.basename(infoFile),e.args[0])
+ print u"Error getting PIDs from database {0}: {1}".format(kDatabase[0],e.args[0])
+ traceback.print_exc()
+
for serialnum in serials:
try:
pidlst.extend(getKindlePids(md1, md2, serialnum))
- except Exception, message:
+ except Exception, e:
print u"Error getting PIDs from serial number {0}: {1}".format(serialnum ,e.args[0])
+ traceback.print_exc()
+
return pidlst
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kindlekey.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kindlekey.py
new file mode 100644
index 0000000..e79622b
--- /dev/null
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kindlekey.py
@@ -0,0 +1,1893 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from __future__ import with_statement
+
+# kindlekey.py
+# Copyright © 2010-2013 by some_updates and Apprentice Alf
+#
+# Currently requires alfcrypto.py which requires the alfcrypto library
+
+# Revision history:
+# 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc.
+# 1.1 - Added Tkinter to match adobekey.py
+# 1.2 - Fixed testing of successful retrieval on Mac
+# 1.3 - Added getkey interface for Windows DeDRM application
+# Simplified some of the Kindle for Mac code.
+# 1.4 - Remove dependency on alfcrypto
+
+"""
+Retrieve Kindle for PC/Mac user key.
+"""
+
+__license__ = 'GPL v3'
+__version__ = '1.4'
+
+import sys, os, re
+from struct import pack, unpack, unpack_from
+import json
+import getopt
+
+# Routines common to Mac and PC
+
+# Wrap a stream so that output gets flushed immediately
+# and also make sure that any unicode strings get
+# encoded using "replace" before writing them.
+class SafeUnbuffered:
+ def __init__(self, stream):
+ self.stream = stream
+ self.encoding = stream.encoding
+ if self.encoding == None:
+ self.encoding = "utf-8"
+ def write(self, data):
+ if isinstance(data,unicode):
+ data = data.encode(self.encoding,"replace")
+ self.stream.write(data)
+ self.stream.flush()
+ def __getattr__(self, attr):
+ return getattr(self.stream, attr)
+
+try:
+ from calibre.constants import iswindows, isosx
+except:
+ iswindows = sys.platform.startswith('win')
+ isosx = sys.platform.startswith('darwin')
+
+def unicode_argv():
+ if iswindows:
+ # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
+ # strings.
+
+ # Versions 2.x of Python don't support Unicode in sys.argv on
+ # Windows, with the underlying Windows API instead replacing multi-byte
+ # characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
+ # as a list of Unicode strings and encode them as utf-8
+
+ from ctypes import POINTER, byref, cdll, c_int, windll
+ from ctypes.wintypes import LPCWSTR, LPWSTR
+
+ GetCommandLineW = cdll.kernel32.GetCommandLineW
+ GetCommandLineW.argtypes = []
+ GetCommandLineW.restype = LPCWSTR
+
+ CommandLineToArgvW = windll.shell32.CommandLineToArgvW
+ CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
+ CommandLineToArgvW.restype = POINTER(LPWSTR)
+
+ cmd = GetCommandLineW()
+ argc = c_int(0)
+ argv = CommandLineToArgvW(cmd, byref(argc))
+ if argc.value > 0:
+ # Remove Python executable and commands if present
+ start = argc.value - len(sys.argv)
+ return [argv[i] for i in
+ xrange(start, argc.value)]
+ # if we don't have any arguments at all, just pass back script name
+ # this should never happen
+ return [u"kindlekey.py"]
+ else:
+ argvencoding = sys.stdin.encoding
+ if argvencoding == None:
+ argvencoding = "utf-8"
+ return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
+
+class DrmException(Exception):
+ pass
+
+# crypto digestroutines
+import hashlib
+
+def MD5(message):
+ ctx = hashlib.md5()
+ ctx.update(message)
+ return ctx.digest()
+
+def SHA1(message):
+ ctx = hashlib.sha1()
+ ctx.update(message)
+ return ctx.digest()
+
+def SHA256(message):
+ ctx = hashlib.sha256()
+ ctx.update(message)
+ return ctx.digest()
+
+# For K4M/PC 1.6.X and later
+# generate table of prime number less than or equal to int n
+def primes(n):
+ if n==2: return [2]
+ elif n<2: return []
+ s=range(3,n+1,2)
+ mroot = n ** 0.5
+ half=(n+1)/2-1
+ i=0
+ m=3
+ while m <= mroot:
+ if s[i]:
+ j=(m*m-3)/2
+ s[j]=0
+ while j<half:
+ s[j]=0
+ j+=m
+ i=i+1
+ m=2*i+3
+ return [2]+[x for x in s if x]
+
+# Encode the bytes in data with the characters in map
+def encode(data, map):
+ result = ''
+ for char in data:
+ value = ord(char)
+ Q = (value ^ 0x80) // len(map)
+ R = value % len(map)
+ result += map[Q]
+ result += map[R]
+ return result
+
+# Hash the bytes in data and then encode the digest with the characters in map
+def encodeHash(data,map):
+ return encode(MD5(data),map)
+
+# Decode the string in data with the characters in map. Returns the decoded bytes
+def decode(data,map):
+ result = ''
+ for i in range (0,len(data)-1,2):
+ high = map.find(data[i])
+ low = map.find(data[i+1])
+ if (high == -1) or (low == -1) :
+ break
+ value = (((high * len(map)) ^ 0x80) & 0xFF) + low
+ result += pack('B',value)
+ return result
+
+# Routines unique to Mac and PC
+if iswindows:
+ from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
+ create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
+ string_at, Structure, c_void_p, cast
+
+ import _winreg as winreg
+ MAX_PATH = 255
+ kernel32 = windll.kernel32
+ advapi32 = windll.advapi32
+ crypt32 = windll.crypt32
+
+ try:
+ # try to get fast routines from alfcrypto
+ from alfcrypto import AES_CBC, KeyIVGen
+ except:
+ # alfcrypto not available, so use python implementations
+ """
+ Routines for doing AES CBC in one file
+
+ Modified by some_updates to extract
+ and combine only those parts needed for AES CBC
+ into one simple to add python file
+
+ Original Version
+ Copyright (c) 2002 by Paul A. Lambert
+ Under:
+ CryptoPy Artisitic License Version 1.0
+ See the wonderful pure python package cryptopy-1.2.5
+ and read its LICENSE.txt for complete license details.
+ """
+
+ class CryptoError(Exception):
+ """ Base class for crypto exceptions """
+ def __init__(self,errorMessage='Error!'):
+ self.message = errorMessage
+ def __str__(self):
+ return self.message
+
+ class InitCryptoError(CryptoError):
+ """ Crypto errors during algorithm initialization """
+ class BadKeySizeError(InitCryptoError):
+ """ Bad key size error """
+ class EncryptError(CryptoError):
+ """ Error in encryption processing """
+ class DecryptError(CryptoError):
+ """ Error in decryption processing """
+ class DecryptNotBlockAlignedError(DecryptError):
+ """ Error in decryption processing """
+
+ def xorS(a,b):
+ """ XOR two strings """
+ assert len(a)==len(b)
+ x = []
+ for i in range(len(a)):
+ x.append( chr(ord(a[i])^ord(b[i])))
+ return ''.join(x)
+
+ def xor(a,b):
+ """ XOR two strings """
+ x = []
+ for i in range(min(len(a),len(b))):
+ x.append( chr(ord(a[i])^ord(b[i])))
+ return ''.join(x)
+
+ """
+ Base 'BlockCipher' and Pad classes for cipher instances.
+ BlockCipher supports automatic padding and type conversion. The BlockCipher
+ class was written to make the actual algorithm code more readable and
+ not for performance.
+ """
+
+ class BlockCipher:
+ """ Block ciphers """
+ def __init__(self):
+ self.reset()
+
+ def reset(self):
+ self.resetEncrypt()
+ self.resetDecrypt()
+ def resetEncrypt(self):
+ self.encryptBlockCount = 0
+ self.bytesToEncrypt = ''
+ def resetDecrypt(self):
+ self.decryptBlockCount = 0
+ self.bytesToDecrypt = ''
+
+ def encrypt(self, plainText, more = None):
+ """ Encrypt a string and return a binary string """
+ self.bytesToEncrypt += plainText # append plainText to any bytes from prior encrypt
+ numBlocks, numExtraBytes = divmod(len(self.bytesToEncrypt), self.blockSize)
+ cipherText = ''
+ for i in range(numBlocks):
+ bStart = i*self.blockSize
+ ctBlock = self.encryptBlock(self.bytesToEncrypt[bStart:bStart+self.blockSize])
+ self.encryptBlockCount += 1
+ cipherText += ctBlock
+ if numExtraBytes > 0: # save any bytes that are not block aligned
+ self.bytesToEncrypt = self.bytesToEncrypt[-numExtraBytes:]
+ else:
+ self.bytesToEncrypt = ''
+
+ if more == None: # no more data expected from caller
+ finalBytes = self.padding.addPad(self.bytesToEncrypt,self.blockSize)
+ if len(finalBytes) > 0:
+ ctBlock = self.encryptBlock(finalBytes)
+ self.encryptBlockCount += 1
+ cipherText += ctBlock
+ self.resetEncrypt()
+ return cipherText
+
+ def decrypt(self, cipherText, more = None):
+ """ Decrypt a string and return a string """
+ self.bytesToDecrypt += cipherText # append to any bytes from prior decrypt
+
+ numBlocks, numExtraBytes = divmod(len(self.bytesToDecrypt), self.blockSize)
+ if more == None: # no more calls to decrypt, should have all the data
+ if numExtraBytes != 0:
+ raise DecryptNotBlockAlignedError, 'Data not block aligned on decrypt'
+
+ # hold back some bytes in case last decrypt has zero len
+ if (more != None) and (numExtraBytes == 0) and (numBlocks >0) :
+ numBlocks -= 1
+ numExtraBytes = self.blockSize
+
+ plainText = ''
+ for i in range(numBlocks):
+ bStart = i*self.blockSize
+ ptBlock = self.decryptBlock(self.bytesToDecrypt[bStart : bStart+self.blockSize])
+ self.decryptBlockCount += 1
+ plainText += ptBlock
+
+ if numExtraBytes > 0: # save any bytes that are not block aligned
+ self.bytesToEncrypt = self.bytesToEncrypt[-numExtraBytes:]
+ else:
+ self.bytesToEncrypt = ''
+
+ if more == None: # last decrypt remove padding
+ plainText = self.padding.removePad(plainText, self.blockSize)
+ self.resetDecrypt()
+ return plainText
+
+
+ class Pad:
+ def __init__(self):
+ pass # eventually could put in calculation of min and max size extension
+
+ class padWithPadLen(Pad):
+ """ Pad a binary string with the length of the padding """
+
+ def addPad(self, extraBytes, blockSize):
+ """ Add padding to a binary string to make it an even multiple
+ of the block size """
+ blocks, numExtraBytes = divmod(len(extraBytes), blockSize)
+ padLength = blockSize - numExtraBytes
+ return extraBytes + padLength*chr(padLength)
+
+ def removePad(self, paddedBinaryString, blockSize):
+ """ Remove padding from a binary string """
+ if not(0<len(paddedBinaryString)):
+ raise DecryptNotBlockAlignedError, 'Expected More Data'
+ return paddedBinaryString[:-ord(paddedBinaryString[-1])]
+
+ class noPadding(Pad):
+ """ No padding. Use this to get ECB behavior from encrypt/decrypt """
+
+ def addPad(self, extraBytes, blockSize):
+ """ Add no padding """
+ return extraBytes
+
+ def removePad(self, paddedBinaryString, blockSize):
+ """ Remove no padding """
+ return paddedBinaryString
+
+ """
+ Rijndael encryption algorithm
+ This byte oriented implementation is intended to closely
+ match FIPS specification for readability. It is not implemented
+ for performance.
+ """
+
+ class Rijndael(BlockCipher):
+ """ Rijndael encryption algorithm """
+ def __init__(self, key = None, padding = padWithPadLen(), keySize=16, blockSize=16 ):
+ self.name = 'RIJNDAEL'
+ self.keySize = keySize
+ self.strength = keySize*8
+ self.blockSize = blockSize # blockSize is in bytes
+ self.padding = padding # change default to noPadding() to get normal ECB behavior
+
+ assert( keySize%4==0 and NrTable[4].has_key(keySize/4)),'key size must be 16,20,24,29 or 32 bytes'
+ assert( blockSize%4==0 and NrTable.has_key(blockSize/4)), 'block size must be 16,20,24,29 or 32 bytes'
+
+ self.Nb = self.blockSize/4 # Nb is number of columns of 32 bit words
+ self.Nk = keySize/4 # Nk is the key length in 32-bit words
+ self.Nr = NrTable[self.Nb][self.Nk] # The number of rounds (Nr) is a function of
+ # the block (Nb) and key (Nk) sizes.
+ if key != None:
+ self.setKey(key)
+
+ def setKey(self, key):
+ """ Set a key and generate the expanded key """
+ assert( len(key) == (self.Nk*4) ), 'Key length must be same as keySize parameter'
+ self.__expandedKey = keyExpansion(self, key)
+ self.reset() # BlockCipher.reset()
+
+ def encryptBlock(self, plainTextBlock):
+ """ Encrypt a block, plainTextBlock must be a array of bytes [Nb by 4] """
+ self.state = self._toBlock(plainTextBlock)
+ AddRoundKey(self, self.__expandedKey[0:self.Nb])
+ for round in range(1,self.Nr): #for round = 1 step 1 to Nr
+ SubBytes(self)
+ ShiftRows(self)
+ MixColumns(self)
+ AddRoundKey(self, self.__expandedKey[round*self.Nb:(round+1)*self.Nb])
+ SubBytes(self)
+ ShiftRows(self)
+ AddRoundKey(self, self.__expandedKey[self.Nr*self.Nb:(self.Nr+1)*self.Nb])
+ return self._toBString(self.state)
+
+
+ def decryptBlock(self, encryptedBlock):
+ """ decrypt a block (array of bytes) """
+ self.state = self._toBlock(encryptedBlock)
+ AddRoundKey(self, self.__expandedKey[self.Nr*self.Nb:(self.Nr+1)*self.Nb])
+ for round in range(self.Nr-1,0,-1):
+ InvShiftRows(self)
+ InvSubBytes(self)
+ AddRoundKey(self, self.__expandedKey[round*self.Nb:(round+1)*self.Nb])
+ InvMixColumns(self)
+ InvShiftRows(self)
+ InvSubBytes(self)
+ AddRoundKey(self, self.__expandedKey[0:self.Nb])
+ return self._toBString(self.state)
+
+ def _toBlock(self, bs):
+ """ Convert binary string to array of bytes, state[col][row]"""
+ assert ( len(bs) == 4*self.Nb ), 'Rijndarl blocks must be of size blockSize'
+ return [[ord(bs[4*i]),ord(bs[4*i+1]),ord(bs[4*i+2]),ord(bs[4*i+3])] for i in range(self.Nb)]
+
+ def _toBString(self, block):
+ """ Convert block (array of bytes) to binary string """
+ l = []
+ for col in block:
+ for rowElement in col:
+ l.append(chr(rowElement))
+ return ''.join(l)
+ #-------------------------------------
+ """ Number of rounds Nr = NrTable[Nb][Nk]
+
+ Nb Nk=4 Nk=5 Nk=6 Nk=7 Nk=8
+ ------------------------------------- """
+ NrTable = {4: {4:10, 5:11, 6:12, 7:13, 8:14},
+ 5: {4:11, 5:11, 6:12, 7:13, 8:14},
+ 6: {4:12, 5:12, 6:12, 7:13, 8:14},
+ 7: {4:13, 5:13, 6:13, 7:13, 8:14},
+ 8: {4:14, 5:14, 6:14, 7:14, 8:14}}
+ #-------------------------------------
+ def keyExpansion(algInstance, keyString):
+ """ Expand a string of size keySize into a larger array """
+ Nk, Nb, Nr = algInstance.Nk, algInstance.Nb, algInstance.Nr # for readability
+ key = [ord(byte) for byte in keyString] # convert string to list
+ w = [[key[4*i],key[4*i+1],key[4*i+2],key[4*i+3]] for i in range(Nk)]
+ for i in range(Nk,Nb*(Nr+1)):
+ temp = w[i-1] # a four byte column
+ if (i%Nk) == 0 :
+ temp = temp[1:]+[temp[0]] # RotWord(temp)
+ temp = [ Sbox[byte] for byte in temp ]
+ temp[0] ^= Rcon[i/Nk]
+ elif Nk > 6 and i%Nk == 4 :
+ temp = [ Sbox[byte] for byte in temp ] # SubWord(temp)
+ w.append( [ w[i-Nk][byte]^temp[byte] for byte in range(4) ] )
+ return w
+
+ Rcon = (0,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36, # note extra '0' !!!
+ 0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6,
+ 0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91)
+
+ #-------------------------------------
+ def AddRoundKey(algInstance, keyBlock):
+ """ XOR the algorithm state with a block of key material """
+ for column in range(algInstance.Nb):
+ for row in range(4):
+ algInstance.state[column][row] ^= keyBlock[column][row]
+ #-------------------------------------
+
+ def SubBytes(algInstance):
+ for column in range(algInstance.Nb):
+ for row in range(4):
+ algInstance.state[column][row] = Sbox[algInstance.state[column][row]]
+
+ def InvSubBytes(algInstance):
+ for column in range(algInstance.Nb):
+ for row in range(4):
+ algInstance.state[column][row] = InvSbox[algInstance.state[column][row]]
+
+ Sbox = (0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,
+ 0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
+ 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,
+ 0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
+ 0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,
+ 0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
+ 0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,
+ 0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
+ 0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,
+ 0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
+ 0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,
+ 0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
+ 0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,
+ 0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
+ 0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,
+ 0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
+ 0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,
+ 0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
+ 0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,
+ 0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
+ 0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,
+ 0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
+ 0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,
+ 0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
+ 0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,
+ 0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
+ 0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,
+ 0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
+ 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,
+ 0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
+ 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,
+ 0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16)
+
+ InvSbox = (0x52,0x09,0x6a,0xd5,0x30,0x36,0xa5,0x38,
+ 0xbf,0x40,0xa3,0x9e,0x81,0xf3,0xd7,0xfb,
+ 0x7c,0xe3,0x39,0x82,0x9b,0x2f,0xff,0x87,
+ 0x34,0x8e,0x43,0x44,0xc4,0xde,0xe9,0xcb,
+ 0x54,0x7b,0x94,0x32,0xa6,0xc2,0x23,0x3d,
+ 0xee,0x4c,0x95,0x0b,0x42,0xfa,0xc3,0x4e,
+ 0x08,0x2e,0xa1,0x66,0x28,0xd9,0x24,0xb2,
+ 0x76,0x5b,0xa2,0x49,0x6d,0x8b,0xd1,0x25,
+ 0x72,0xf8,0xf6,0x64,0x86,0x68,0x98,0x16,
+ 0xd4,0xa4,0x5c,0xcc,0x5d,0x65,0xb6,0x92,
+ 0x6c,0x70,0x48,0x50,0xfd,0xed,0xb9,0xda,
+ 0x5e,0x15,0x46,0x57,0xa7,0x8d,0x9d,0x84,
+ 0x90,0xd8,0xab,0x00,0x8c,0xbc,0xd3,0x0a,
+ 0xf7,0xe4,0x58,0x05,0xb8,0xb3,0x45,0x06,
+ 0xd0,0x2c,0x1e,0x8f,0xca,0x3f,0x0f,0x02,
+ 0xc1,0xaf,0xbd,0x03,0x01,0x13,0x8a,0x6b,
+ 0x3a,0x91,0x11,0x41,0x4f,0x67,0xdc,0xea,
+ 0x97,0xf2,0xcf,0xce,0xf0,0xb4,0xe6,0x73,
+ 0x96,0xac,0x74,0x22,0xe7,0xad,0x35,0x85,
+ 0xe2,0xf9,0x37,0xe8,0x1c,0x75,0xdf,0x6e,
+ 0x47,0xf1,0x1a,0x71,0x1d,0x29,0xc5,0x89,
+ 0x6f,0xb7,0x62,0x0e,0xaa,0x18,0xbe,0x1b,
+ 0xfc,0x56,0x3e,0x4b,0xc6,0xd2,0x79,0x20,
+ 0x9a,0xdb,0xc0,0xfe,0x78,0xcd,0x5a,0xf4,
+ 0x1f,0xdd,0xa8,0x33,0x88,0x07,0xc7,0x31,
+ 0xb1,0x12,0x10,0x59,0x27,0x80,0xec,0x5f,
+ 0x60,0x51,0x7f,0xa9,0x19,0xb5,0x4a,0x0d,
+ 0x2d,0xe5,0x7a,0x9f,0x93,0xc9,0x9c,0xef,
+ 0xa0,0xe0,0x3b,0x4d,0xae,0x2a,0xf5,0xb0,
+ 0xc8,0xeb,0xbb,0x3c,0x83,0x53,0x99,0x61,
+ 0x17,0x2b,0x04,0x7e,0xba,0x77,0xd6,0x26,
+ 0xe1,0x69,0x14,0x63,0x55,0x21,0x0c,0x7d)
+
+ #-------------------------------------
+ """ For each block size (Nb), the ShiftRow operation shifts row i
+ by the amount Ci. Note that row 0 is not shifted.
+ Nb C1 C2 C3
+ ------------------- """
+ shiftOffset = { 4 : ( 0, 1, 2, 3),
+ 5 : ( 0, 1, 2, 3),
+ 6 : ( 0, 1, 2, 3),
+ 7 : ( 0, 1, 2, 4),
+ 8 : ( 0, 1, 3, 4) }
+ def ShiftRows(algInstance):
+ tmp = [0]*algInstance.Nb # list of size Nb
+ for r in range(1,4): # row 0 reamains unchanged and can be skipped
+ for c in range(algInstance.Nb):
+ tmp[c] = algInstance.state[(c+shiftOffset[algInstance.Nb][r]) % algInstance.Nb][r]
+ for c in range(algInstance.Nb):
+ algInstance.state[c][r] = tmp[c]
+ def InvShiftRows(algInstance):
+ tmp = [0]*algInstance.Nb # list of size Nb
+ for r in range(1,4): # row 0 reamains unchanged and can be skipped
+ for c in range(algInstance.Nb):
+ tmp[c] = algInstance.state[(c+algInstance.Nb-shiftOffset[algInstance.Nb][r]) % algInstance.Nb][r]
+ for c in range(algInstance.Nb):
+ algInstance.state[c][r] = tmp[c]
+ #-------------------------------------
+ def MixColumns(a):
+ Sprime = [0,0,0,0]
+ for j in range(a.Nb): # for each column
+ Sprime[0] = mul(2,a.state[j][0])^mul(3,a.state[j][1])^mul(1,a.state[j][2])^mul(1,a.state[j][3])
+ Sprime[1] = mul(1,a.state[j][0])^mul(2,a.state[j][1])^mul(3,a.state[j][2])^mul(1,a.state[j][3])
+ Sprime[2] = mul(1,a.state[j][0])^mul(1,a.state[j][1])^mul(2,a.state[j][2])^mul(3,a.state[j][3])
+ Sprime[3] = mul(3,a.state[j][0])^mul(1,a.state[j][1])^mul(1,a.state[j][2])^mul(2,a.state[j][3])
+ for i in range(4):
+ a.state[j][i] = Sprime[i]
+
+ def InvMixColumns(a):
+ """ Mix the four bytes of every column in a linear way
+ This is the opposite operation of Mixcolumn """
+ Sprime = [0,0,0,0]
+ for j in range(a.Nb): # for each column
+ Sprime[0] = mul(0x0E,a.state[j][0])^mul(0x0B,a.state[j][1])^mul(0x0D,a.state[j][2])^mul(0x09,a.state[j][3])
+ Sprime[1] = mul(0x09,a.state[j][0])^mul(0x0E,a.state[j][1])^mul(0x0B,a.state[j][2])^mul(0x0D,a.state[j][3])
+ Sprime[2] = mul(0x0D,a.state[j][0])^mul(0x09,a.state[j][1])^mul(0x0E,a.state[j][2])^mul(0x0B,a.state[j][3])
+ Sprime[3] = mul(0x0B,a.state[j][0])^mul(0x0D,a.state[j][1])^mul(0x09,a.state[j][2])^mul(0x0E,a.state[j][3])
+ for i in range(4):
+ a.state[j][i] = Sprime[i]
+
+ #-------------------------------------
+ def mul(a, b):
+ """ Multiply two elements of GF(2^m)
+ needed for MixColumn and InvMixColumn """
+ if (a !=0 and b!=0):
+ return Alogtable[(Logtable[a] + Logtable[b])%255]
+ else:
+ return 0
+
+ Logtable = ( 0, 0, 25, 1, 50, 2, 26, 198, 75, 199, 27, 104, 51, 238, 223, 3,
+ 100, 4, 224, 14, 52, 141, 129, 239, 76, 113, 8, 200, 248, 105, 28, 193,
+ 125, 194, 29, 181, 249, 185, 39, 106, 77, 228, 166, 114, 154, 201, 9, 120,
+ 101, 47, 138, 5, 33, 15, 225, 36, 18, 240, 130, 69, 53, 147, 218, 142,
+ 150, 143, 219, 189, 54, 208, 206, 148, 19, 92, 210, 241, 64, 70, 131, 56,
+ 102, 221, 253, 48, 191, 6, 139, 98, 179, 37, 226, 152, 34, 136, 145, 16,
+ 126, 110, 72, 195, 163, 182, 30, 66, 58, 107, 40, 84, 250, 133, 61, 186,
+ 43, 121, 10, 21, 155, 159, 94, 202, 78, 212, 172, 229, 243, 115, 167, 87,
+ 175, 88, 168, 80, 244, 234, 214, 116, 79, 174, 233, 213, 231, 230, 173, 232,
+ 44, 215, 117, 122, 235, 22, 11, 245, 89, 203, 95, 176, 156, 169, 81, 160,
+ 127, 12, 246, 111, 23, 196, 73, 236, 216, 67, 31, 45, 164, 118, 123, 183,
+ 204, 187, 62, 90, 251, 96, 177, 134, 59, 82, 161, 108, 170, 85, 41, 157,
+ 151, 178, 135, 144, 97, 190, 220, 252, 188, 149, 207, 205, 55, 63, 91, 209,
+ 83, 57, 132, 60, 65, 162, 109, 71, 20, 42, 158, 93, 86, 242, 211, 171,
+ 68, 17, 146, 217, 35, 32, 46, 137, 180, 124, 184, 38, 119, 153, 227, 165,
+ 103, 74, 237, 222, 197, 49, 254, 24, 13, 99, 140, 128, 192, 247, 112, 7)
+
+ Alogtable= ( 1, 3, 5, 15, 17, 51, 85, 255, 26, 46, 114, 150, 161, 248, 19, 53,
+ 95, 225, 56, 72, 216, 115, 149, 164, 247, 2, 6, 10, 30, 34, 102, 170,
+ 229, 52, 92, 228, 55, 89, 235, 38, 106, 190, 217, 112, 144, 171, 230, 49,
+ 83, 245, 4, 12, 20, 60, 68, 204, 79, 209, 104, 184, 211, 110, 178, 205,
+ 76, 212, 103, 169, 224, 59, 77, 215, 98, 166, 241, 8, 24, 40, 120, 136,
+ 131, 158, 185, 208, 107, 189, 220, 127, 129, 152, 179, 206, 73, 219, 118, 154,
+ 181, 196, 87, 249, 16, 48, 80, 240, 11, 29, 39, 105, 187, 214, 97, 163,
+ 254, 25, 43, 125, 135, 146, 173, 236, 47, 113, 147, 174, 233, 32, 96, 160,
+ 251, 22, 58, 78, 210, 109, 183, 194, 93, 231, 50, 86, 250, 21, 63, 65,
+ 195, 94, 226, 61, 71, 201, 64, 192, 91, 237, 44, 116, 156, 191, 218, 117,
+ 159, 186, 213, 100, 172, 239, 42, 126, 130, 157, 188, 223, 122, 142, 137, 128,
+ 155, 182, 193, 88, 232, 35, 101, 175, 234, 37, 111, 177, 200, 67, 197, 84,
+ 252, 31, 33, 99, 165, 244, 7, 9, 27, 45, 119, 153, 176, 203, 70, 202,
+ 69, 207, 74, 222, 121, 139, 134, 145, 168, 227, 62, 66, 198, 81, 243, 14,
+ 18, 54, 90, 238, 41, 123, 141, 140, 143, 138, 133, 148, 167, 242, 13, 23,
+ 57, 75, 221, 124, 132, 151, 162, 253, 28, 36, 108, 180, 199, 82, 246, 1)
+
+
+
+
+ """
+ AES Encryption Algorithm
+ The AES algorithm is just Rijndael algorithm restricted to the default
+ blockSize of 128 bits.
+ """
+
+ class AES(Rijndael):
+ """ The AES algorithm is the Rijndael block cipher restricted to block
+ sizes of 128 bits and key sizes of 128, 192 or 256 bits
+ """
+ def __init__(self, key = None, padding = padWithPadLen(), keySize=16):
+ """ Initialize AES, keySize is in bytes """
+ if not (keySize == 16 or keySize == 24 or keySize == 32) :
+ raise BadKeySizeError, 'Illegal AES key size, must be 16, 24, or 32 bytes'
+
+ Rijndael.__init__( self, key, padding=padding, keySize=keySize, blockSize=16 )
+
+ self.name = 'AES'
+
+
+ """
+ CBC mode of encryption for block ciphers.
+ This algorithm mode wraps any BlockCipher to make a
+ Cipher Block Chaining mode.
+ """
+ from random import Random # should change to crypto.random!!!
+
+
+ class CBC(BlockCipher):
+ """ The CBC class wraps block ciphers to make cipher block chaining (CBC) mode
+ algorithms. The initialization (IV) is automatic if set to None. Padding
+ is also automatic based on the Pad class used to initialize the algorithm
+ """
+ def __init__(self, blockCipherInstance, padding = padWithPadLen()):
+ """ CBC algorithms are created by initializing with a BlockCipher instance """
+ self.baseCipher = blockCipherInstance
+ self.name = self.baseCipher.name + '_CBC'
+ self.blockSize = self.baseCipher.blockSize
+ self.keySize = self.baseCipher.keySize
+ self.padding = padding
+ self.baseCipher.padding = noPadding() # baseCipher should NOT pad!!
+ self.r = Random() # for IV generation, currently uses
+ # mediocre standard distro version <----------------
+ import time
+ newSeed = time.ctime()+str(self.r) # seed with instance location
+ self.r.seed(newSeed) # to make unique
+ self.reset()
+
+ def setKey(self, key):
+ self.baseCipher.setKey(key)
+
+ # Overload to reset both CBC state and the wrapped baseCipher
+ def resetEncrypt(self):
+ BlockCipher.resetEncrypt(self) # reset CBC encrypt state (super class)
+ self.baseCipher.resetEncrypt() # reset base cipher encrypt state
+
+ def resetDecrypt(self):
+ BlockCipher.resetDecrypt(self) # reset CBC state (super class)
+ self.baseCipher.resetDecrypt() # reset base cipher decrypt state
+
+ def encrypt(self, plainText, iv=None, more=None):
+ """ CBC encryption - overloads baseCipher to allow optional explicit IV
+ when iv=None, iv is auto generated!
+ """
+ if self.encryptBlockCount == 0:
+ self.iv = iv
+ else:
+ assert(iv==None), 'IV used only on first call to encrypt'
+
+ return BlockCipher.encrypt(self,plainText, more=more)
+
+ def decrypt(self, cipherText, iv=None, more=None):
+ """ CBC decryption - overloads baseCipher to allow optional explicit IV
+ when iv=None, iv is auto generated!
+ """
+ if self.decryptBlockCount == 0:
+ self.iv = iv
+ else:
+ assert(iv==None), 'IV used only on first call to decrypt'
+
+ return BlockCipher.decrypt(self, cipherText, more=more)
+
+ def encryptBlock(self, plainTextBlock):
+ """ CBC block encryption, IV is set with 'encrypt' """
+ auto_IV = ''
+ if self.encryptBlockCount == 0:
+ if self.iv == None:
+ # generate IV and use
+ self.iv = ''.join([chr(self.r.randrange(256)) for i in range(self.blockSize)])
+ self.prior_encr_CT_block = self.iv
+ auto_IV = self.prior_encr_CT_block # prepend IV if it's automatic
+ else: # application provided IV
+ assert(len(self.iv) == self.blockSize ),'IV must be same length as block'
+ self.prior_encr_CT_block = self.iv
+ """ encrypt the prior CT XORed with the PT """
+ ct = self.baseCipher.encryptBlock( xor(self.prior_encr_CT_block, plainTextBlock) )
+ self.prior_encr_CT_block = ct
+ return auto_IV+ct
+
+ def decryptBlock(self, encryptedBlock):
+ """ Decrypt a single block """
+
+ if self.decryptBlockCount == 0: # first call, process IV
+ if self.iv == None: # auto decrypt IV?
+ self.prior_CT_block = encryptedBlock
+ return ''
+ else:
+ assert(len(self.iv)==self.blockSize),"Bad IV size on CBC decryption"
+ self.prior_CT_block = self.iv
+
+ dct = self.baseCipher.decryptBlock(encryptedBlock)
+ """ XOR the prior decrypted CT with the prior CT """
+ dct_XOR_priorCT = xor( self.prior_CT_block, dct )
+
+ self.prior_CT_block = encryptedBlock
+
+ return dct_XOR_priorCT
+
+
+ """
+ AES_CBC Encryption Algorithm
+ """
+
+ class aescbc_AES_CBC(CBC):
+ """ AES encryption in CBC feedback mode """
+ def __init__(self, key=None, padding=padWithPadLen(), keySize=16):
+ CBC.__init__( self, AES(key, noPadding(), keySize), padding)
+ self.name = 'AES_CBC'
+
+ class AES_CBC(object):
+ def __init__(self):
+ self._key = None
+ self._iv = None
+ self.aes = None
+
+ def set_decrypt_key(self, userkey, iv):
+ self._key = userkey
+ self._iv = iv
+ self.aes = aescbc_AES_CBC(userkey, noPadding(), len(userkey))
+
+ def decrypt(self, data):
+ iv = self._iv
+ cleartext = self.aes.decrypt(iv + data)
+ return cleartext
+
+ import hmac
+
+ class KeyIVGen(object):
+ # this only exists in openssl so we will use pure python implementation instead
+ # PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1',
+ # [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p])
+ def pbkdf2(self, passwd, salt, iter, keylen):
+
+ def xorstr( a, b ):
+ if len(a) != len(b):
+ raise Exception("xorstr(): lengths differ")
+ return ''.join((chr(ord(x)^ord(y)) for x, y in zip(a, b)))
+
+ def prf( h, data ):
+ hm = h.copy()
+ hm.update( data )
+ return hm.digest()
+
+ def pbkdf2_F( h, salt, itercount, blocknum ):
+ U = prf( h, salt + pack('>i',blocknum ) )
+ T = U
+ for i in range(2, itercount+1):
+ U = prf( h, U )
+ T = xorstr( T, U )
+ return T
+
+ sha = hashlib.sha1
+ digest_size = sha().digest_size
+ # l - number of output blocks to produce
+ l = keylen / digest_size
+ if keylen % digest_size != 0:
+ l += 1
+ h = hmac.new( passwd, None, sha )
+ T = ""
+ for i in range(1, l+1):
+ T += pbkdf2_F( h, salt, iter, i )
+ return T[0: keylen]
+
+ def UnprotectHeaderData(encryptedData):
+ passwdData = 'header_key_data'
+ salt = 'HEADER.2011'
+ iter = 0x80
+ keylen = 0x100
+ key_iv = KeyIVGen().pbkdf2(passwdData, salt, iter, keylen)
+ key = key_iv[0:32]
+ iv = key_iv[32:48]
+ aes=AES_CBC()
+ aes.set_decrypt_key(key, iv)
+ cleartext = aes.decrypt(encryptedData)
+ return cleartext
+
+ # Various character maps used to decrypt kindle info values.
+ # Probably supposed to act as obfuscation
+ charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_"
+ charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE"
+ # New maps in K4PC 1.9.0
+ testMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
+ testMap6 = "9YzAb0Cd1Ef2n5Pr6St7Uvh3Jk4M8WxG"
+ testMap8 = "YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD"
+
+ # interface with Windows OS Routines
+ class DataBlob(Structure):
+ _fields_ = [('cbData', c_uint),
+ ('pbData', c_void_p)]
+ DataBlob_p = POINTER(DataBlob)
+
+
+ def GetSystemDirectory():
+ GetSystemDirectoryW = kernel32.GetSystemDirectoryW
+ GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint]
+ GetSystemDirectoryW.restype = c_uint
+ def GetSystemDirectory():
+ buffer = create_unicode_buffer(MAX_PATH + 1)
+ GetSystemDirectoryW(buffer, len(buffer))
+ return buffer.value
+ return GetSystemDirectory
+ GetSystemDirectory = GetSystemDirectory()
+
+ def GetVolumeSerialNumber():
+ GetVolumeInformationW = kernel32.GetVolumeInformationW
+ GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint,
+ POINTER(c_uint), POINTER(c_uint),
+ POINTER(c_uint), c_wchar_p, c_uint]
+ GetVolumeInformationW.restype = c_uint
+ def GetVolumeSerialNumber(path = GetSystemDirectory().split('\\')[0] + '\\'):
+ vsn = c_uint(0)
+ GetVolumeInformationW(path, None, 0, byref(vsn), None, None, None, 0)
+ return str(vsn.value)
+ return GetVolumeSerialNumber
+ GetVolumeSerialNumber = GetVolumeSerialNumber()
+
+ def GetIDString():
+ vsn = GetVolumeSerialNumber()
+ #print('Using Volume Serial Number for ID: '+vsn)
+ return vsn
+
+ def getLastError():
+ GetLastError = kernel32.GetLastError
+ GetLastError.argtypes = None
+ GetLastError.restype = c_uint
+ def getLastError():
+ return GetLastError()
+ return getLastError
+ getLastError = getLastError()
+
+ def GetUserName():
+ GetUserNameW = advapi32.GetUserNameW
+ GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
+ GetUserNameW.restype = c_uint
+ def GetUserName():
+ buffer = create_unicode_buffer(2)
+ size = c_uint(len(buffer))
+ while not GetUserNameW(buffer, byref(size)):
+ errcd = getLastError()
+ if errcd == 234:
+ # bad wine implementation up through wine 1.3.21
+ return "AlternateUserName"
+ buffer = create_unicode_buffer(len(buffer) * 2)
+ size.value = len(buffer)
+ return buffer.value.encode('utf-16-le')[::2]
+ return GetUserName
+ GetUserName = GetUserName()
+
+ def CryptUnprotectData():
+ _CryptUnprotectData = crypt32.CryptUnprotectData
+ _CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
+ c_void_p, c_void_p, c_uint, DataBlob_p]
+ _CryptUnprotectData.restype = c_uint
+ def CryptUnprotectData(indata, entropy, flags):
+ indatab = create_string_buffer(indata)
+ indata = DataBlob(len(indata), cast(indatab, c_void_p))
+ entropyb = create_string_buffer(entropy)
+ entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
+ outdata = DataBlob()
+ if not _CryptUnprotectData(byref(indata), None, byref(entropy),
+ None, None, flags, byref(outdata)):
+ # raise DrmException("Failed to Unprotect Data")
+ return 'failed'
+ return string_at(outdata.pbData, outdata.cbData)
+ return CryptUnprotectData
+ CryptUnprotectData = CryptUnprotectData()
+
+
+ # Locate all of the kindle-info style files and return as list
+ def getKindleInfoFiles():
+ kInfoFiles = []
+ # some 64 bit machines do not have the proper registry key for some reason
+ # or the pythonn interface to the 32 vs 64 bit registry is broken
+ path = ""
+ if 'LOCALAPPDATA' in os.environ.keys():
+ path = os.environ['LOCALAPPDATA']
+ else:
+ # User Shell Folders show take precedent over Shell Folders if present
+ try:
+ regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\")
+ path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
+ if not os.path.isdir(path):
+ path = ""
+ try:
+ regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
+ path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
+ if not os.path.isdir(path):
+ path = ""
+ except RegError:
+ pass
+ except RegError:
+ pass
+
+ found = False
+ if path == "":
+ print ('Could not find the folder in which to look for kinfoFiles.')
+ else:
+ print('searching for kinfoFiles in ' + path)
+
+ # look for (K4PC 1.9.0 and later) .kinf2011 file
+ kinfopath = path +'\\Amazon\\Kindle\\storage\\.kinf2011'
+ if os.path.isfile(kinfopath):
+ found = True
+ print('Found K4PC 1.9+ kinf2011 file: ' + kinfopath)
+ kInfoFiles.append(kinfopath)
+
+ # look for (K4PC 1.6.0 and later) rainier.2.1.1.kinf file
+ kinfopath = path +'\\Amazon\\Kindle\\storage\\rainier.2.1.1.kinf'
+ if os.path.isfile(kinfopath):
+ found = True
+ print('Found K4PC 1.6-1.8 kinf file: ' + kinfopath)
+ kInfoFiles.append(kinfopath)
+
+ # look for (K4PC 1.5.0 and later) rainier.2.1.1.kinf file
+ kinfopath = path +'\\Amazon\\Kindle For PC\\storage\\rainier.2.1.1.kinf'
+ if os.path.isfile(kinfopath):
+ found = True
+ print('Found K4PC 1.5 kinf file: ' + kinfopath)
+ kInfoFiles.append(kinfopath)
+
+ # look for original (earlier than K4PC 1.5.0) kindle-info files
+ kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info'
+ if os.path.isfile(kinfopath):
+ found = True
+ print('Found K4PC kindle.info file: ' + kinfopath)
+ kInfoFiles.append(kinfopath)
+
+ if not found:
+ print('No K4PC kindle.info/kinf/kinf2011 files have been found.')
+ return kInfoFiles
+
+
+ # determine type of kindle info provided and return a
+ # database of keynames and values
+ def getDBfromFile(kInfoFile):
+ names = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber', 'max_date', 'SIGVERIF']
+ DB = {}
+ with open(kInfoFile, 'rb') as infoReader:
+ hdr = infoReader.read(1)
+ data = infoReader.read()
+
+ 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('/')
+
+ # 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 raw keyhash string is used to create entropy for the actual
+ # CryptProtectData Blob that represents that keys contents
+ entropy = SHA1(keyhash)
+
+ # 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,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)
+
+
+ # 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)
+ DB[keyname] = cleartext
+
+ if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB:
+ print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(GetIDString(), GetUserName())
+ # store values used in decryption
+ DB['IDString'] = GetIDString()
+ DB['UserName'] = GetUserName()
+ else:
+ DB = {}
+ return DB
+elif isosx:
+ import copy
+ import subprocess
+
+ # interface to needed routines in openssl's libcrypto
+ def _load_crypto_libcrypto():
+ from ctypes import CDLL, byref, POINTER, c_void_p, c_char_p, c_int, c_long, \
+ Structure, c_ulong, create_string_buffer, addressof, string_at, cast
+ from ctypes.util import find_library
+
+ libcrypto = find_library('crypto')
+ if libcrypto is None:
+ raise DrmException(u"libcrypto not found")
+ libcrypto = CDLL(libcrypto)
+
+ # From OpenSSL's crypto aes header
+ #
+ # AES_ENCRYPT 1
+ # AES_DECRYPT 0
+ # AES_MAXNR 14 (in bytes)
+ # AES_BLOCK_SIZE 16 (in bytes)
+ #
+ # struct aes_key_st {
+ # unsigned long rd_key[4 *(AES_MAXNR + 1)];
+ # int rounds;
+ # };
+ # typedef struct aes_key_st AES_KEY;
+ #
+ # int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key);
+ #
+ # note: the ivec string, and output buffer are both mutable
+ # void AES_cbc_encrypt(const unsigned char *in, unsigned char *out,
+ # const unsigned long length, const AES_KEY *key, unsigned char *ivec, const int enc);
+
+ AES_MAXNR = 14
+ c_char_pp = POINTER(c_char_p)
+ c_int_p = POINTER(c_int)
+
+ class AES_KEY(Structure):
+ _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)]
+ AES_KEY_p = POINTER(AES_KEY)
+
+ def F(restype, name, argtypes):
+ func = getattr(libcrypto, name)
+ func.restype = restype
+ func.argtypes = argtypes
+ return func
+
+ AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,c_int])
+
+ AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',[c_char_p, c_int, AES_KEY_p])
+
+ # From OpenSSL's Crypto evp/p5_crpt2.c
+ #
+ # int PKCS5_PBKDF2_HMAC_SHA1(const char *pass, int passlen,
+ # const unsigned char *salt, int saltlen, int iter,
+ # int keylen, unsigned char *out);
+
+ PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1',
+ [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p])
+
+ class LibCrypto(object):
+ def __init__(self):
+ self._blocksize = 0
+ self._keyctx = None
+ self._iv = 0
+
+ def set_decrypt_key(self, userkey, iv):
+ self._blocksize = len(userkey)
+ if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
+ raise DrmException(u"AES improper key used")
+ return
+ keyctx = self._keyctx = AES_KEY()
+ self._iv = iv
+ self._userkey = userkey
+ rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
+ if rv < 0:
+ raise DrmException(u"Failed to initialize AES key")
+
+ def decrypt(self, data):
+ out = create_string_buffer(len(data))
+ mutable_iv = create_string_buffer(self._iv, len(self._iv))
+ keyctx = self._keyctx
+ rv = AES_cbc_encrypt(data, out, len(data), keyctx, mutable_iv, 0)
+ if rv == 0:
+ raise DrmException(u"AES decryption failed")
+ return out.raw
+
+ def keyivgen(self, passwd, salt, iter, keylen):
+ saltlen = len(salt)
+ passlen = len(passwd)
+ out = create_string_buffer(keylen)
+ rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out)
+ return out.raw
+ return LibCrypto
+
+ def _load_crypto():
+ LibCrypto = None
+ try:
+ LibCrypto = _load_crypto_libcrypto()
+ except (ImportError, DrmException):
+ pass
+ return LibCrypto
+
+ LibCrypto = _load_crypto()
+
+ # Various character maps used to decrypt books. Probably supposed to act as obfuscation
+ charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M'
+ charMap2 = 'ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM'
+
+ # For kinf approach of K4Mac 1.6.X or later
+ # On K4PC charMap5 = 'AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE'
+ # For Mac they seem to re-use charMap2 here
+ charMap5 = charMap2
+
+ # new in K4M 1.9.X
+ testMap8 = 'YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD'
+
+ # uses a sub process to get the Hard Drive Serial Number using ioreg
+ # returns serial numbers of all internal hard drive drives
+ def GetVolumesSerialNumbers():
+ sernum = os.getenv('MYSERIALNUMBER')
+ if sernum != None:
+ return [sernum]
+ sernums = []
+ cmdline = '/usr/sbin/ioreg -w 0 -r -c AppleAHCIDiskDriver'
+ cmdline = cmdline.encode(sys.getfilesystemencoding())
+ p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
+ out1, out2 = p.communicate()
+ 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\" = \"')
+ if pp >= 0:
+ sernum = resline[pp+19:-1]
+ sernums.append(sernum.strip())
+ return [sernum]
+
+ def GetUserHomeAppSupKindleDirParitionName():
+ home = os.getenv('HOME')
+ dpath = home + '/Library'
+ cmdline = '/sbin/mount'
+ cmdline = cmdline.encode(sys.getfilesystemencoding())
+ p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
+ out1, out2 = p.communicate()
+ reslst = out1.split('\n')
+ cnt = len(reslst)
+ disk = ''
+ foundIt = False
+ for j in xrange(cnt):
+ resline = reslst[j]
+ if resline.startswith('/dev'):
+ (devpart, mpath) = resline.split(' on ')
+ dpart = devpart[5:]
+ pp = mpath.find('(')
+ if pp >= 0:
+ mpath = mpath[:pp-1]
+ if dpath.startswith(mpath):
+ disk = dpart
+ return disk
+
+ # uses a sub process to get the UUID of the specified disk partition using ioreg
+ def GetDiskPartitionUUID(diskpart):
+ uuidnum = os.getenv('MYUUIDNUMBER')
+ if uuidnum != None:
+ return uuidnum
+ cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver'
+ cmdline = cmdline.encode(sys.getfilesystemencoding())
+ p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
+ out1, out2 = p.communicate()
+ 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 not foundIt:
+ uuidnum = ''
+ return uuidnum
+
+ def GetMACAddressMunged():
+ macnum = os.getenv('MYMACNUM')
+ if macnum != None:
+ return macnum
+ cmdline = '/sbin/ifconfig 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 ')
+ if pp >= 0:
+ macnum = resline[pp+6:-1]
+ 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
+ for i in range(6):
+ maclst[i] = int('0x' + maclst[i], 0)
+ mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+ mlst[5] = maclst[5] ^ 0xa5
+ mlst[4] = maclst[3] ^ 0xa5
+ mlst[3] = maclst[4] ^ 0xa5
+ mlst[2] = maclst[2] ^ 0xa5
+ mlst[1] = maclst[1] ^ 0xa5
+ mlst[0] = maclst[0] ^ 0xa5
+ macnum = '%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x' % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5])
+ foundIt = True
+ break
+ if not foundIt:
+ macnum = ''
+ return macnum
+
+
+ # uses unix env to get username instead of using sysctlbyname
+ def GetUserName():
+ username = os.getenv('USER')
+ return username
+
+ def GetIDStrings():
+ # Return all possible ID Strings
+ strings = []
+ strings.append(GetMACAddressMunged())
+ strings.extend(GetVolumesSerialNumbers())
+ diskpart = GetUserHomeAppSupKindleDirParitionName()
+ strings.append(GetDiskPartitionUUID(diskpart))
+ strings.append('9999999999')
+ 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):
+ passwdData = 'header_key_data'
+ salt = 'HEADER.2011'
+ iter = 0x80
+ keylen = 0x100
+ crp = LibCrypto()
+ key_iv = crp.keyivgen(passwdData, salt, iter, keylen)
+ key = key_iv[0:32]
+ iv = key_iv[32:48]
+ crp.set_decrypt_key(key,iv)
+ cleartext = crp.decrypt(encryptedData)
+ return cleartext
+
+
+ # implements an Pseudo Mac Version of Windows built-in Crypto routine
+ # used for Kindle for Mac Versions >= 1.9.0
+ class CryptUnprotectDataV3(object):
+ def __init__(self, entropy, IDString):
+ sp = GetUserName() + '+@#$%+' + IDString
+ passwdData = encode(SHA256(sp),charMap2)
+ salt = entropy
+ self.crp = LibCrypto()
+ iter = 0x800
+ keylen = 0x400
+ key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen)
+ self.key = key_iv[0:32]
+ self.iv = key_iv[32:48]
+ self.crp.set_decrypt_key(self.key, self.iv)
+
+ def decrypt(self, encryptedData):
+ cleartext = self.crp.decrypt(encryptedData)
+ cleartext = decode(cleartext, charMap2)
+ return cleartext
+
+
+ # Locate the .kindle-info files
+ def getKindleInfoFiles():
+ # file searches can take a long time on some systems, so just look in known specific places.
+ kInfoFiles=[]
+ found = False
+ home = os.getenv('HOME')
+ # check for .kinf2011 file in new location (App Store Kindle for Mac)
+ testpath = home + '/Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/storage/.kinf2011'
+ if os.path.isfile(testpath):
+ kInfoFiles.append(testpath)
+ print('Found k4Mac kinf2011 file: ' + testpath)
+ found = True
+ # check for .kinf2011 files
+ testpath = home + '/Library/Application Support/Kindle/storage/.kinf2011'
+ if os.path.isfile(testpath):
+ kInfoFiles.append(testpath)
+ print('Found k4Mac kinf2011 file: ' + testpath)
+ found = True
+ # check for .rainier-2.1.1-kinf files
+ testpath = home + '/Library/Application Support/Kindle/storage/.rainier-2.1.1-kinf'
+ if os.path.isfile(testpath):
+ kInfoFiles.append(testpath)
+ print('Found k4Mac rainier file: ' + testpath)
+ found = True
+ # check for .kindle-info files
+ testpath = home + '/Library/Application Support/Kindle/storage/.kindle-info'
+ if os.path.isfile(testpath):
+ kInfoFiles.append(testpath)
+ print('Found k4Mac kindle-info file: ' + testpath)
+ found = True
+ if not found:
+ print('No k4Mac kindle-info/rainier/kinf2011 files have been found.')
+ return kInfoFiles
+
+ # determine type of kindle info provided and return a
+ # database of keynames and values
+ def getDBfromFile(kInfoFile):
+ names = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber', 'max_date', 'SIGVERIF']
+ with open(kInfoFile, 'rb') as infoReader:
+ filehdr = infoReader.read(1)
+ filedata = infoReader.read()
+
+ 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)
+
+ # 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'
+
+ # 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 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,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 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
+ except:
+ pass
+ if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB:
+ # store values used in decryption
+ print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString, GetUserName())
+ DB['IDString'] = IDString
+ DB['UserName'] = GetUserName()
+ else:
+ print u"Couldn't decrypt file."
+ DB = {}
+ return DB
+else:
+ def getDBfromFile(kInfoFile):
+ raise DrmException(u"This script only runs under Windows or Mac OS X.")
+ return {}
+
+def kindlekeys(files = []):
+ keys = []
+ if files == []:
+ files = getKindleInfoFiles()
+ for file in files:
+ key = getDBfromFile(file)
+ if key:
+ # convert all values to hex, just in case.
+ for keyname in key:
+ key[keyname]=key[keyname].encode('hex')
+ keys.append(key)
+ return keys
+
+# interface for Python DeDRM
+# returns single key or multiple keys, depending on path or file passed in
+def getkey(outpath, files=[]):
+ keys = kindlekeys(files)
+ if len(keys) > 0:
+ if not os.path.isdir(outpath):
+ outfile = outpath
+ with file(outfile, 'w') as keyfileout:
+ keyfileout.write(json.dumps(keys[0]))
+ print u"Saved a key to {0}".format(outfile)
+ else:
+ keycount = 0
+ for key in keys:
+ while True:
+ keycount += 1
+ outfile = os.path.join(outpath,u"kindlekey{0:d}.k4i".format(keycount))
+ if not os.path.exists(outfile):
+ break
+ with file(outfile, 'w') as keyfileout:
+ keyfileout.write(json.dumps(key))
+ print u"Saved a key to {0}".format(outfile)
+ return True
+ return False
+
+def usage(progname):
+ print u"Finds, decrypts and saves the default Kindle For Mac/PC encryption keys."
+ print u"Keys are saved to the current directory, or a specified output directory."
+ print u"If a file name is passed instead of a directory, only the first key is saved, in that file."
+ print u"Usage:"
+ print u" {0:s} [-h] [-k <kindle.info>] [<outpath>]".format(progname)
+
+
+def cli_main(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__)
+
+ try:
+ opts, args = getopt.getopt(argv[1:], "hk:")
+ except getopt.GetoptError, err:
+ print u"Error in options or arguments: {0}".format(err.args[0])
+ usage(progname)
+ sys.exit(2)
+
+ files = []
+ for o, a in opts:
+ if o == "-h":
+ usage(progname)
+ sys.exit(0)
+ if o == "-k":
+ files = [a]
+
+ if len(args) > 1:
+ usage(progname)
+ sys.exit(2)
+
+ if len(args) == 1:
+ # save to the specified file or directory
+ outpath = args[0]
+ if not os.path.isabs(outpath):
+ outpath = os.path.abspath(outpath)
+ else:
+ # save to the same directory as the script
+ outpath = os.path.dirname(argv[0])
+
+ # make sure the outpath is the
+ outpath = os.path.realpath(os.path.normpath(outpath))
+
+ if not getkey(outpath, files):
+ print u"Could not retrieve Kindle for Mac/PC key."
+ return 0
+
+
+def gui_main(argv=unicode_argv()):
+ import Tkinter
+ import Tkconstants
+ import tkMessageBox
+ import traceback
+
+ class ExceptionDialog(Tkinter.Frame):
+ def __init__(self, root, text):
+ Tkinter.Frame.__init__(self, root, border=5)
+ label = Tkinter.Label(self, text=u"Unexpected error:",
+ anchor=Tkconstants.W, justify=Tkconstants.LEFT)
+ label.pack(fill=Tkconstants.X, expand=0)
+ self.text = Tkinter.Text(self)
+ self.text.pack(fill=Tkconstants.BOTH, expand=1)
+
+ self.text.insert(Tkconstants.END, text)
+
+
+ root = Tkinter.Tk()
+ root.withdraw()
+ progpath, progname = os.path.split(argv[0])
+ success = False
+ try:
+ keys = kindlekeys()
+ keycount = 0
+ for key in keys:
+ while True:
+ keycount += 1
+ outfile = os.path.join(progpath,u"kindlekey{0:d}.k4i".format(keycount))
+ if not os.path.exists(outfile):
+ break
+
+ with file(outfile, 'w') as keyfileout:
+ keyfileout.write(json.dumps(key))
+ success = True
+ tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile))
+ except DrmException, e:
+ tkMessageBox.showerror(progname, u"Error: {0}".format(str(e)))
+ except Exception:
+ root.wm_state('normal')
+ root.title(progname)
+ text = traceback.format_exc()
+ ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1)
+ root.mainloop()
+ if not success:
+ return 1
+ return 0
+
+if __name__ == '__main__':
+ if len(sys.argv) > 1:
+ sys.stdout=SafeUnbuffered(sys.stdout)
+ sys.stderr=SafeUnbuffered(sys.stderr)
+ sys.exit(cli_main())
+ sys.exit(gui_main())
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py
index 264c175..ccbac4e 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py
@@ -462,7 +462,7 @@ class MobiBook:
raise DrmException(u"Encryption not initialised. Must be opened with Mobipocket Reader first.")
found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids)
if not found_key:
- raise DrmException(u"No key found in {0:d} keys tried. Read the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(len(goodpids)))
+ raise DrmException(u"No key found in {0:d} keys tried.".format(len(goodpids)))
# kill the drm keys
self.patchSection(0, '\0' * drm_size, drm_ptr)
# kill the drm pointers
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/openssl_des.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/openssl_des.py
index a4a40ca..9a84e58 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/openssl_des.py
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/openssl_des.py
@@ -65,7 +65,7 @@ def load_libcrypto():
class DES(object):
def __init__(self, key):
if len(key) != 8 :
- raise Error('DES improper key used')
+ raise Exception('DES improper key used')
return
self.key = key
self.keyschedule = DES_KEY_SCHEDULE()
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/plugin-import-name-dedrm.txt b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/plugin-import-name-dedrm.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/plugin-import-name-dedrm.txt
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/scrolltextwidget.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/scrolltextwidget.py
deleted file mode 100644
index 98b4147..0000000
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/scrolltextwidget.py
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env python
-# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
-
-import Tkinter
-import Tkconstants
-
-# basic scrolled text widget
-class ScrolledText(Tkinter.Text):
- def __init__(self, master=None, **kw):
- self.frame = Tkinter.Frame(master)
- self.vbar = Tkinter.Scrollbar(self.frame)
- self.vbar.pack(side=Tkconstants.RIGHT, fill=Tkconstants.Y)
- kw.update({'yscrollcommand': self.vbar.set})
- Tkinter.Text.__init__(self, self.frame, **kw)
- self.pack(side=Tkconstants.LEFT, fill=Tkconstants.BOTH, expand=True)
- self.vbar['command'] = self.yview
- # Copy geometry methods of self.frame without overriding Text
- # methods = hack!
- text_meths = vars(Tkinter.Text).keys()
- methods = vars(Tkinter.Pack).keys() + vars(Tkinter.Grid).keys() + vars(Tkinter.Place).keys()
- methods = set(methods).difference(text_meths)
- for m in methods:
- if m[0] != '_' and m != 'config' and m != 'configure':
- setattr(self, m, getattr(self.frame, m))
-
- def __str__(self):
- return str(self.frame)
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/subasyncio.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/subasyncio.py
deleted file mode 100644
index de084d3..0000000
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/subasyncio.py
+++ /dev/null
@@ -1,148 +0,0 @@
-#!/usr/bin/env python
-# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
-
-import os, sys
-import signal
-import threading
-import subprocess
-from subprocess import Popen, PIPE, STDOUT
-
-# **heavily** chopped up and modfied version of asyncproc.py
-# to make it actually work on Windows as well as Mac/Linux
-# For the original see:
-# "http://www.lysator.liu.se/~bellman/download/"
-# author is "Thomas Bellman <[email protected]>"
-# available under GPL version 3 or Later
-
-# create an asynchronous subprocess whose output can be collected in
-# a non-blocking manner
-
-# What a mess! Have to use threads just to get non-blocking io
-# in a cross-platform manner
-
-# luckily all thread use is hidden within this class
-
-class Process(object):
- def __init__(self, *params, **kwparams):
- if len(params) <= 3:
- kwparams.setdefault('stdin', subprocess.PIPE)
- if len(params) <= 4:
- kwparams.setdefault('stdout', subprocess.PIPE)
- if len(params) <= 5:
- kwparams.setdefault('stderr', subprocess.PIPE)
- self.__pending_input = []
- self.__collected_outdata = []
- self.__collected_errdata = []
- self.__exitstatus = None
- self.__lock = threading.Lock()
- self.__inputsem = threading.Semaphore(0)
- self.__quit = False
-
- self.__process = subprocess.Popen(*params, **kwparams)
-
- if self.__process.stdin:
- self.__stdin_thread = threading.Thread(
- name="stdin-thread",
- target=self.__feeder, args=(self.__pending_input,
- self.__process.stdin))
- self.__stdin_thread.setDaemon(True)
- self.__stdin_thread.start()
-
- if self.__process.stdout:
- self.__stdout_thread = threading.Thread(
- name="stdout-thread",
- target=self.__reader, args=(self.__collected_outdata,
- self.__process.stdout))
- self.__stdout_thread.setDaemon(True)
- self.__stdout_thread.start()
-
- if self.__process.stderr:
- self.__stderr_thread = threading.Thread(
- name="stderr-thread",
- target=self.__reader, args=(self.__collected_errdata,
- self.__process.stderr))
- self.__stderr_thread.setDaemon(True)
- self.__stderr_thread.start()
-
- def pid(self):
- return self.__process.pid
-
- def kill(self, signal):
- self.__process.send_signal(signal)
-
- # check on subprocess (pass in 'nowait') to act like poll
- def wait(self, flag):
- if flag.lower() == 'nowait':
- rc = self.__process.poll()
- else:
- rc = self.__process.wait()
- if rc != None:
- if self.__process.stdin:
- self.closeinput()
- if self.__process.stdout:
- self.__stdout_thread.join()
- if self.__process.stderr:
- self.__stderr_thread.join()
- return self.__process.returncode
-
- def terminate(self):
- if self.__process.stdin:
- self.closeinput()
- self.__process.terminate()
-
- # thread gets data from subprocess stdout
- def __reader(self, collector, source):
- while True:
- data = os.read(source.fileno(), 65536)
- self.__lock.acquire()
- collector.append(data)
- self.__lock.release()
- if data == "":
- source.close()
- break
- return
-
- # thread feeds data to subprocess stdin
- def __feeder(self, pending, drain):
- while True:
- self.__inputsem.acquire()
- self.__lock.acquire()
- if not pending and self.__quit:
- drain.close()
- self.__lock.release()
- break
- data = pending.pop(0)
- self.__lock.release()
- drain.write(data)
-
- # non-blocking read of data from subprocess stdout
- def read(self):
- self.__lock.acquire()
- outdata = "".join(self.__collected_outdata)
- del self.__collected_outdata[:]
- self.__lock.release()
- return outdata
-
- # non-blocking read of data from subprocess stderr
- def readerr(self):
- self.__lock.acquire()
- errdata = "".join(self.__collected_errdata)
- del self.__collected_errdata[:]
- self.__lock.release()
- return errdata
-
- # non-blocking write to stdin of subprocess
- def write(self, data):
- if self.__process.stdin is None:
- raise ValueError("Writing to process with stdin not a pipe")
- self.__lock.acquire()
- self.__pending_input.append(data)
- self.__inputsem.release()
- self.__lock.release()
-
- # close stdinput of subprocess
- def closeinput(self):
- self.__lock.acquire()
- self.__quit = True
- self.__inputsem.release()
- self.__lock.release()
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/topazextract.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/topazextract.py
index 3e4db39..71fe8ab 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/topazextract.py
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/topazextract.py
@@ -74,7 +74,7 @@ debug = False
if 'calibre' in sys.modules:
inCalibre = True
- from calibre_plugins.k4mobidedrm import kgenpids
+ from calibre_plugins.dedrm import kgenpids
else:
inCalibre = False
import kgenpids
@@ -321,7 +321,7 @@ class TopazBook:
self.extractFiles()
print u"Successfully Extracted Topaz contents"
if inCalibre:
- from calibre_plugins.k4mobidedrm import genbook
+ from calibre_plugins.dedrm import genbook
else:
import genbook
@@ -355,7 +355,7 @@ class TopazBook:
self.extractFiles()
print u"Successfully Extracted Topaz contents"
if inCalibre:
- from calibre_plugins.k4mobidedrm import genbook
+ from calibre_plugins.dedrm import genbook
else:
import genbook
@@ -439,7 +439,7 @@ class TopazBook:
def usage(progname):
print u"Removes DRM protection from Topaz ebooks and extracts the contents"
print u"Usage:"
- print u" {0} [-k <kindle.info>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] <infile> <outdir>".format(progname)
+ print u" {0} [-k <kindle.k4i>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] <infile> <outdir>".format(progname)
# Main
def cli_main(argv=unicode_argv()):
@@ -466,7 +466,7 @@ def cli_main(argv=unicode_argv()):
print u"Output Directory {0} Does Not Exist.".format(outdir)
return 1
- kInfoFiles = []
+ kDatabaseFiles = []
serials = []
pids = []
@@ -474,7 +474,7 @@ def cli_main(argv=unicode_argv()):
if o == '-k':
if a == None :
raise DrmException("Invalid parameter for -k")
- kInfoFiles.append(a)
+ kDatabaseFiles.append(a)
if o == '-p':
if a == None :
raise DrmException("Invalid parameter for -p")
@@ -490,7 +490,7 @@ def cli_main(argv=unicode_argv()):
title = tb.getBookTitle()
print u"Processing Book: {0}".format(title)
md1, md2 = tb.getPIDMetaInfo()
- pids.extend(kgenpids.getPidList(md1, md2, serials, kInfoFiles))
+ pids.extend(kgenpids.getPidList(md1, md2, serials, kDatabaseFiles))
try:
print u"Decrypting Book"
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/utilities.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/utilities.py
new file mode 100644
index 0000000..c730607
--- /dev/null
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/utilities.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from __future__ import with_statement
+
+__license__ = 'GPL v3'
+
+DETAILED_MESSAGE = \
+'You have personal information stored in this plugin\'s customization '+ \
+'string from a previous version of this plugin.\n\n'+ \
+'This new version of the plugin can convert that info '+ \
+'into key data that the new plugin can then use (which doesn\'t '+ \
+'require personal information to be stored/displayed in an insecure '+ \
+'manner like the old plugin did).\n\nIf you choose NOT to migrate this data at this time '+ \
+'you will be prompted to save that personal data to a file elsewhere; and you\'ll have '+ \
+'to manually re-configure this plugin with your information.\n\nEither way... ' + \
+'this new version of the plugin will not be responsible for storing that personal '+ \
+'info in plain sight any longer.'
+
+def uStrCmp (s1, s2, caseless=False):
+ import unicodedata as ud
+ str1 = s1 if isinstance(s1, unicode) else unicode(s1)
+ str2 = s2 if isinstance(s2, unicode) else unicode(s2)
+ if caseless:
+ return ud.normalize('NFC', str1.lower()) == ud.normalize('NFC', str2.lower())
+ else:
+ return ud.normalize('NFC', str1) == ud.normalize('NFC', str2)
+
+def parseCustString(keystuff):
+ userkeys = []
+ ar = keystuff.split(':')
+ for i in ar:
+ try:
+ name, ccn = i.split(',')
+ # Generate Barnes & Noble EPUB user key from name and credit card number.
+ userkeys.append(generate_key(name, ccn))
+ except:
+ pass
+ return userkeys
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/zipfilerugged.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/zipfilerugged.py
index adf3c53..4a55a69 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/zipfilerugged.py
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/zipfilerugged.py
@@ -354,7 +354,7 @@ class ZipInfo (object):
def _decodeFilename(self):
if self.flag_bits & 0x800:
try:
- print "decoding filename",self.filename
+ #print "decoding filename",self.filename
return self.filename.decode('utf-8')
except:
return self.filename
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/zipfix.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/zipfix.py
index eaee20d..8ddfae3 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/zipfix.py
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/zipfix.py
@@ -1,6 +1,23 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
+# zipfix.py, version 1.1
+# Copyright © 2010-2013 by some_updates, DiapDealer and Apprentice Alf
+
+# Released under the terms of the GNU General Public Licence, version 3
+# <http://www.gnu.org/licenses/>
+
+# Revision history:
+# 1.0 - Initial release
+# 1.1 - Updated to handle zip file metadata correctly
+
+"""
+Re-write zip (or ePub) fixing problems with file names (and mimetype entry).
+"""
+
+__license__ = 'GPL v3'
+__version__ = "1.1"
+
import sys
import zlib
import zipfilerugged
@@ -96,25 +113,41 @@ class fixZip:
# if epub write mimetype file first, with no compression
if self.ztype == 'epub':
- nzinfo = ZipInfo('mimetype', compress_type=zipfilerugged.ZIP_STORED)
- self.outzip.writestr(nzinfo, _MIMETYPE)
+ # first get a ZipInfo with current time and no compression
+ mimeinfo = ZipInfo('mimetype',compress_type=zipfilerugged.ZIP_STORED)
+ mimeinfo.internal_attr = 1 # text file
+ try:
+ # if the mimetype is present, get its info, including time-stamp
+ oldmimeinfo = self.inzip.getinfo('mimetype')
+ # copy across useful fields
+ mimeinfo.date_time = oldmimeinfo.date_time
+ mimeinfo.comment = oldmimeinfo.comment
+ mimeinfo.extra = oldmimeinfo.extra
+ mimeinfo.internal_attr = oldmimeinfo.internal_attr
+ mimeinfo.external_attr = oldmimeinfo.external_attr
+ mimeinfo.create_system = oldmimeinfo.create_system
+ except:
+ pass
+ self.outzip.writestr(mimeinfo, _MIMETYPE)
# write the rest of the files
for zinfo in self.inzip.infolist():
- if zinfo.filename != "mimetype" or self.ztype == '.zip':
+ if zinfo.filename != "mimetype" or self.ztype != 'epub':
data = None
- nzinfo = zinfo
try:
data = self.inzip.read(zinfo.filename)
except zipfilerugged.BadZipfile or zipfilerugged.error:
local_name = self.getlocalname(zinfo)
data = self.getfiledata(zinfo)
- nzinfo.filename = local_name
-
- nzinfo.date_time = zinfo.date_time
- nzinfo.compress_type = zinfo.compress_type
- nzinfo.flag_bits = 0
- nzinfo.internal_attr = 0
+ zinfo.filename = local_name
+
+ # create new ZipInfo with only the useful attributes from the old info
+ nzinfo = ZipInfo(zinfo.filename, zinfo.date_time, compress_type=zinfo.compress_type)
+ nzinfo.comment=zinfo.comment
+ nzinfo.extra=zinfo.extra
+ nzinfo.internal_attr=zinfo.internal_attr
+ nzinfo.external_attr=zinfo.external_attr
+ nzinfo.create_system=zinfo.create_system
self.outzip.writestr(nzinfo,data)
self.bzf.close()