diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist index cfd9fcc..6ed5ade 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist @@ -24,19 +24,19 @@ CFBundleExecutable droplet CFBundleGetInfoString - DeDRM AppleScript 6.0.8. Written 2010–2013 by Apprentice Alf and others. + DeDRM AppleScript 6.0.9. Written 2010–2013 by Apprentice Alf and others. CFBundleIconFile DeDRM CFBundleIdentifier com.apple.ScriptEditor.id.707CCCD5-0C6C-4BEB-B67C-B6E866ADE85A CFBundleInfoDictionaryVersion - 6.0.8 + 6.0.9 CFBundleName DeDRM CFBundlePackageType APPL CFBundleShortVersionString - 6.0.8 + 6.0.9 CFBundleSignature dplt LSRequiresCarbon diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Help.htm b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Help.htm index 69edade..f0c51a8 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Help.htm +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Help.htm @@ -17,7 +17,7 @@ p {margin-top: 0} -

DeDRM Plugin (v6.0.0)

+

DeDRM Plugin (v6.0.9)

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.

diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/__init__.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/__init__.py index 37d4cb1..232b6bc 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/__init__.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/__init__.py @@ -35,13 +35,14 @@ __docformat__ = 'restructuredtext en' # 6.0.6 - Fix up an incorrect function call # 6.0.7 - Error handling for incomplete PDF metadata # 6.0.8 - Fixes a Wine key issue and topaz support +# 6.0.9 - Ported to work with newer versions of Calibre (moved to Qt5). Still supports older Qt4 versions. """ Decrypt DRMed ebooks. """ PLUGIN_NAME = u"DeDRM" -PLUGIN_VERSION_TUPLE = (6, 0, 8) +PLUGIN_VERSION_TUPLE = (6, 0, 9) 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' diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/config.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/config.py index 1e58476..b5c5300 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/config.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/config.py @@ -9,16 +9,24 @@ __license__ = 'GPL v3' import os, traceback, json # PyQT4 modules (part of calibre). -from PyQt4.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, +try: + from PyQt5.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, QGroupBox, QPushButton, QListWidget, QListWidgetItem, - QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl, QString) -from PyQt4 import QtGui - + QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl) +except ImportError: + from PyQt4.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, + QGroupBox, QPushButton, QListWidget, QListWidgetItem, + QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl) +try: + from PyQt5 import Qt as QtGui +except ImportError: + from PyQt4 import QtGui + from zipfile import ZipFile # calibre modules and constants. from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url, - choose_dir, choose_files) + choose_dir, choose_files, choose_save_file) from calibre.utils.config import dynamic, config_dir, JSONConfig from calibre.constants import iswindows, isosx @@ -267,7 +275,7 @@ class ManageKeysDialog(QDialog): def getwineprefix(self): if self.wineprefix is not None: - return unicode(self.wp_lineedit.text().toUtf8(), 'utf8').strip() + return unicode(self.wp_lineedit.text()).strip() return u"" def populate_list(self): @@ -316,7 +324,7 @@ class ManageKeysDialog(QDialog): if d.result() != d.Accepted: # rename cancelled or moot. return - keyname = unicode(self.listy.currentItem().text().toUtf8(),'utf8') + keyname = unicode(self.listy.currentItem().text()) if not question_dialog(self, "{0} {1}: Confirm Rename".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to rename the {2} named {0} to {1}?".format(keyname,d.key_name,self.key_type_name), show_copy_button=False, default_yes=False): return self.plugin_keys[d.key_name] = self.plugin_keys[keyname] @@ -328,7 +336,7 @@ class ManageKeysDialog(QDialog): def delete_key(self): if not self.listy.currentItem(): return - keyname = unicode(self.listy.currentItem().text().toUtf8(), 'utf8') + keyname = unicode(self.listy.currentItem().text()) if not question_dialog(self, "{0} {1}: Confirm Delete".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to delete the {1} {0}?".format(keyname, self.key_type_name), show_copy_button=False, default_yes=False): return if type(self.plugin_keys) == dict: @@ -352,9 +360,10 @@ class ManageKeysDialog(QDialog): open_url(QUrl(url)) def migrate_files(self): - dynamic[PLUGIN_NAME + u"config_dir"] = config_dir - files = choose_files(self, PLUGIN_NAME + u"config_dir", - u"Select {0} files to import".format(self.key_type_name), [(u"{0} files".format(self.key_type_name), [self.keyfile_ext])], False) + unique_dlg_name = PLUGIN_NAME + u"import {0} keys".format(self.key_type_name).replace(' ', '_') #takes care of automatically remembering last directory + caption = u"Select {0} files to import".format(self.key_type_name) + filters = [(u"{0} files".format(self.key_type_name), [self.keyfile_ext])] + files = choose_files(self, unique_dlg_name, caption, filters, all_files=False) counter = 0 skipped = 0 if files: @@ -408,17 +417,14 @@ class ManageKeysDialog(QDialog): r = error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), _(errmsg), show=True, show_copy_button=False) return - filter = QString(u"{0} Files (*.{1})".format(self.key_type_name, self.keyfile_ext)) - keyname = unicode(self.listy.currentItem().text().toUtf8(), 'utf8') - if dynamic.get(PLUGIN_NAME + 'save_dir'): - defaultname = os.path.join(dynamic.get(PLUGIN_NAME + 'save_dir'), u"{0}.{1}".format(keyname , self.keyfile_ext)) - else: - defaultname = os.path.join(os.path.expanduser('~'), u"{0}.{1}".format(keyname , self.keyfile_ext)) - filename = unicode(QtGui.QFileDialog.getSaveFileName(self, u"Save {0} File as...".format(self.key_type_name), defaultname, - u"{0} Files (*.{1})".format(self.key_type_name,self.keyfile_ext), filter)) + keyname = unicode(self.listy.currentItem().text()) + unique_dlg_name = PLUGIN_NAME + u"export {0} keys".format(self.key_type_name).replace(' ', '_') #takes care of automatically remembering last directory + caption = u"Save {0} File as...".format(self.key_type_name) + filters = [(u"{0} Files".format(self.key_type_name), [u"{0}".format(self.keyfile_ext)])] + defaultname = u"{0}.{1}".format(keyname, self.keyfile_ext) + filename = choose_save_file(self, unique_dlg_name, caption, filters, all_files=False, initial_filename=defaultname) if filename: - dynamic[PLUGIN_NAME + 'save_dir'] = os.path.split(filename)[0] - with file(filename, 'w') as fname: + with file(filename, 'wb') as fname: if self.binary_file: fname.write(self.plugin_keys[keyname].decode('hex')) elif self.json_file: @@ -458,7 +464,7 @@ class RenameKeyDialog(QDialog): self.resize(self.sizeHint()) def accept(self): - if self.key_ledit.text().isEmpty() or unicode(self.key_ledit.text()).isspace(): + if not unicode(self.key_ledit.text()) or unicode(self.key_ledit.text()).isspace(): errmsg = u"Key name field cannot be empty!" return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), _(errmsg), show=True, show_copy_button=False) @@ -479,7 +485,7 @@ class RenameKeyDialog(QDialog): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @@ -553,7 +559,7 @@ class AddBandNKeyDialog(QDialog): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): @@ -562,11 +568,11 @@ class AddBandNKeyDialog(QDialog): @property def user_name(self): - return unicode(self.name_ledit.text().toUtf8(), 'utf8').strip().lower().replace(' ','') + return unicode(self.name_ledit.text()).strip().lower().replace(' ','') @property def cc_number(self): - return unicode(self.cc_ledit.text().toUtf8(), 'utf8').strip().replace(' ', '').replace('-','') + return unicode(self.cc_ledit.text()).strip().replace(' ', '').replace('-','') def accept(self): @@ -634,7 +640,7 @@ class AddEReaderDialog(QDialog): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): @@ -643,11 +649,11 @@ class AddEReaderDialog(QDialog): @property def user_name(self): - return unicode(self.name_ledit.text().toUtf8(), 'utf8').strip().lower().replace(' ','') + return unicode(self.name_ledit.text()).strip().lower().replace(' ','') @property def cc_number(self): - return unicode(self.cc_ledit.text().toUtf8(), 'utf8').strip().replace(' ', '').replace('-','') + return unicode(self.cc_ledit.text()).strip().replace(' ', '').replace('-','') def accept(self): @@ -719,7 +725,7 @@ class AddAdeptDialog(QDialog): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): @@ -792,7 +798,7 @@ class AddKindleDialog(QDialog): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): @@ -841,11 +847,11 @@ class AddSerialDialog(QDialog): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() def accept(self): if len(self.key_name) == 0 or self.key_name.isspace(): @@ -889,11 +895,11 @@ class AddPIDDialog(QDialog): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() def accept(self): if len(self.key_name) == 0 or self.key_name.isspace(): diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Help.htm b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Help.htm index 69edade..f0c51a8 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Help.htm +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Help.htm @@ -17,7 +17,7 @@ p {margin-top: 0} -

DeDRM Plugin (v6.0.0)

+

DeDRM Plugin (v6.0.9)

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.

diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/__init__.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/__init__.py index 37d4cb1..232b6bc 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/__init__.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/__init__.py @@ -35,13 +35,14 @@ __docformat__ = 'restructuredtext en' # 6.0.6 - Fix up an incorrect function call # 6.0.7 - Error handling for incomplete PDF metadata # 6.0.8 - Fixes a Wine key issue and topaz support +# 6.0.9 - Ported to work with newer versions of Calibre (moved to Qt5). Still supports older Qt4 versions. """ Decrypt DRMed ebooks. """ PLUGIN_NAME = u"DeDRM" -PLUGIN_VERSION_TUPLE = (6, 0, 8) +PLUGIN_VERSION_TUPLE = (6, 0, 9) 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' diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/config.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/config.py index 1e58476..b5c5300 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/config.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/config.py @@ -9,16 +9,24 @@ __license__ = 'GPL v3' import os, traceback, json # PyQT4 modules (part of calibre). -from PyQt4.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, +try: + from PyQt5.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, QGroupBox, QPushButton, QListWidget, QListWidgetItem, - QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl, QString) -from PyQt4 import QtGui - + QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl) +except ImportError: + from PyQt4.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, + QGroupBox, QPushButton, QListWidget, QListWidgetItem, + QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl) +try: + from PyQt5 import Qt as QtGui +except ImportError: + from PyQt4 import QtGui + from zipfile import ZipFile # calibre modules and constants. from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url, - choose_dir, choose_files) + choose_dir, choose_files, choose_save_file) from calibre.utils.config import dynamic, config_dir, JSONConfig from calibre.constants import iswindows, isosx @@ -267,7 +275,7 @@ class ManageKeysDialog(QDialog): def getwineprefix(self): if self.wineprefix is not None: - return unicode(self.wp_lineedit.text().toUtf8(), 'utf8').strip() + return unicode(self.wp_lineedit.text()).strip() return u"" def populate_list(self): @@ -316,7 +324,7 @@ class ManageKeysDialog(QDialog): if d.result() != d.Accepted: # rename cancelled or moot. return - keyname = unicode(self.listy.currentItem().text().toUtf8(),'utf8') + keyname = unicode(self.listy.currentItem().text()) if not question_dialog(self, "{0} {1}: Confirm Rename".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to rename the {2} named {0} to {1}?".format(keyname,d.key_name,self.key_type_name), show_copy_button=False, default_yes=False): return self.plugin_keys[d.key_name] = self.plugin_keys[keyname] @@ -328,7 +336,7 @@ class ManageKeysDialog(QDialog): def delete_key(self): if not self.listy.currentItem(): return - keyname = unicode(self.listy.currentItem().text().toUtf8(), 'utf8') + keyname = unicode(self.listy.currentItem().text()) if not question_dialog(self, "{0} {1}: Confirm Delete".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to delete the {1} {0}?".format(keyname, self.key_type_name), show_copy_button=False, default_yes=False): return if type(self.plugin_keys) == dict: @@ -352,9 +360,10 @@ class ManageKeysDialog(QDialog): open_url(QUrl(url)) def migrate_files(self): - dynamic[PLUGIN_NAME + u"config_dir"] = config_dir - files = choose_files(self, PLUGIN_NAME + u"config_dir", - u"Select {0} files to import".format(self.key_type_name), [(u"{0} files".format(self.key_type_name), [self.keyfile_ext])], False) + unique_dlg_name = PLUGIN_NAME + u"import {0} keys".format(self.key_type_name).replace(' ', '_') #takes care of automatically remembering last directory + caption = u"Select {0} files to import".format(self.key_type_name) + filters = [(u"{0} files".format(self.key_type_name), [self.keyfile_ext])] + files = choose_files(self, unique_dlg_name, caption, filters, all_files=False) counter = 0 skipped = 0 if files: @@ -408,17 +417,14 @@ class ManageKeysDialog(QDialog): r = error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), _(errmsg), show=True, show_copy_button=False) return - filter = QString(u"{0} Files (*.{1})".format(self.key_type_name, self.keyfile_ext)) - keyname = unicode(self.listy.currentItem().text().toUtf8(), 'utf8') - if dynamic.get(PLUGIN_NAME + 'save_dir'): - defaultname = os.path.join(dynamic.get(PLUGIN_NAME + 'save_dir'), u"{0}.{1}".format(keyname , self.keyfile_ext)) - else: - defaultname = os.path.join(os.path.expanduser('~'), u"{0}.{1}".format(keyname , self.keyfile_ext)) - filename = unicode(QtGui.QFileDialog.getSaveFileName(self, u"Save {0} File as...".format(self.key_type_name), defaultname, - u"{0} Files (*.{1})".format(self.key_type_name,self.keyfile_ext), filter)) + keyname = unicode(self.listy.currentItem().text()) + unique_dlg_name = PLUGIN_NAME + u"export {0} keys".format(self.key_type_name).replace(' ', '_') #takes care of automatically remembering last directory + caption = u"Save {0} File as...".format(self.key_type_name) + filters = [(u"{0} Files".format(self.key_type_name), [u"{0}".format(self.keyfile_ext)])] + defaultname = u"{0}.{1}".format(keyname, self.keyfile_ext) + filename = choose_save_file(self, unique_dlg_name, caption, filters, all_files=False, initial_filename=defaultname) if filename: - dynamic[PLUGIN_NAME + 'save_dir'] = os.path.split(filename)[0] - with file(filename, 'w') as fname: + with file(filename, 'wb') as fname: if self.binary_file: fname.write(self.plugin_keys[keyname].decode('hex')) elif self.json_file: @@ -458,7 +464,7 @@ class RenameKeyDialog(QDialog): self.resize(self.sizeHint()) def accept(self): - if self.key_ledit.text().isEmpty() or unicode(self.key_ledit.text()).isspace(): + if not unicode(self.key_ledit.text()) or unicode(self.key_ledit.text()).isspace(): errmsg = u"Key name field cannot be empty!" return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), _(errmsg), show=True, show_copy_button=False) @@ -479,7 +485,7 @@ class RenameKeyDialog(QDialog): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @@ -553,7 +559,7 @@ class AddBandNKeyDialog(QDialog): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): @@ -562,11 +568,11 @@ class AddBandNKeyDialog(QDialog): @property def user_name(self): - return unicode(self.name_ledit.text().toUtf8(), 'utf8').strip().lower().replace(' ','') + return unicode(self.name_ledit.text()).strip().lower().replace(' ','') @property def cc_number(self): - return unicode(self.cc_ledit.text().toUtf8(), 'utf8').strip().replace(' ', '').replace('-','') + return unicode(self.cc_ledit.text()).strip().replace(' ', '').replace('-','') def accept(self): @@ -634,7 +640,7 @@ class AddEReaderDialog(QDialog): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): @@ -643,11 +649,11 @@ class AddEReaderDialog(QDialog): @property def user_name(self): - return unicode(self.name_ledit.text().toUtf8(), 'utf8').strip().lower().replace(' ','') + return unicode(self.name_ledit.text()).strip().lower().replace(' ','') @property def cc_number(self): - return unicode(self.cc_ledit.text().toUtf8(), 'utf8').strip().replace(' ', '').replace('-','') + return unicode(self.cc_ledit.text()).strip().replace(' ', '').replace('-','') def accept(self): @@ -719,7 +725,7 @@ class AddAdeptDialog(QDialog): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): @@ -792,7 +798,7 @@ class AddKindleDialog(QDialog): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): @@ -841,11 +847,11 @@ class AddSerialDialog(QDialog): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() def accept(self): if len(self.key_name) == 0 or self.key_name.isspace(): @@ -889,11 +895,11 @@ class AddPIDDialog(QDialog): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() def accept(self): if len(self.key_name) == 0 or self.key_name.isspace(): diff --git a/DeDRM_calibre_plugin/DeDRM_plugin.zip b/DeDRM_calibre_plugin/DeDRM_plugin.zip index 58d8174..b314cb0 100644 Binary files a/DeDRM_calibre_plugin/DeDRM_plugin.zip and b/DeDRM_calibre_plugin/DeDRM_plugin.zip differ diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/DeDRM_Help.htm b/DeDRM_calibre_plugin/DeDRM_plugin/DeDRM_Help.htm index 69edade..f0c51a8 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/DeDRM_Help.htm +++ b/DeDRM_calibre_plugin/DeDRM_plugin/DeDRM_Help.htm @@ -17,7 +17,7 @@ p {margin-top: 0} -

DeDRM Plugin (v6.0.0)

+

DeDRM Plugin (v6.0.9)

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.

diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/__init__.py b/DeDRM_calibre_plugin/DeDRM_plugin/__init__.py index 37d4cb1..232b6bc 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/__init__.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/__init__.py @@ -35,13 +35,14 @@ __docformat__ = 'restructuredtext en' # 6.0.6 - Fix up an incorrect function call # 6.0.7 - Error handling for incomplete PDF metadata # 6.0.8 - Fixes a Wine key issue and topaz support +# 6.0.9 - Ported to work with newer versions of Calibre (moved to Qt5). Still supports older Qt4 versions. """ Decrypt DRMed ebooks. """ PLUGIN_NAME = u"DeDRM" -PLUGIN_VERSION_TUPLE = (6, 0, 8) +PLUGIN_VERSION_TUPLE = (6, 0, 9) 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' diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/config.py b/DeDRM_calibre_plugin/DeDRM_plugin/config.py index 1e58476..b5c5300 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/config.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/config.py @@ -9,16 +9,24 @@ __license__ = 'GPL v3' import os, traceback, json # PyQT4 modules (part of calibre). -from PyQt4.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, +try: + from PyQt5.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, QGroupBox, QPushButton, QListWidget, QListWidgetItem, - QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl, QString) -from PyQt4 import QtGui - + QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl) +except ImportError: + from PyQt4.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, + QGroupBox, QPushButton, QListWidget, QListWidgetItem, + QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl) +try: + from PyQt5 import Qt as QtGui +except ImportError: + from PyQt4 import QtGui + from zipfile import ZipFile # calibre modules and constants. from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url, - choose_dir, choose_files) + choose_dir, choose_files, choose_save_file) from calibre.utils.config import dynamic, config_dir, JSONConfig from calibre.constants import iswindows, isosx @@ -267,7 +275,7 @@ class ManageKeysDialog(QDialog): def getwineprefix(self): if self.wineprefix is not None: - return unicode(self.wp_lineedit.text().toUtf8(), 'utf8').strip() + return unicode(self.wp_lineedit.text()).strip() return u"" def populate_list(self): @@ -316,7 +324,7 @@ class ManageKeysDialog(QDialog): if d.result() != d.Accepted: # rename cancelled or moot. return - keyname = unicode(self.listy.currentItem().text().toUtf8(),'utf8') + keyname = unicode(self.listy.currentItem().text()) if not question_dialog(self, "{0} {1}: Confirm Rename".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to rename the {2} named {0} to {1}?".format(keyname,d.key_name,self.key_type_name), show_copy_button=False, default_yes=False): return self.plugin_keys[d.key_name] = self.plugin_keys[keyname] @@ -328,7 +336,7 @@ class ManageKeysDialog(QDialog): def delete_key(self): if not self.listy.currentItem(): return - keyname = unicode(self.listy.currentItem().text().toUtf8(), 'utf8') + keyname = unicode(self.listy.currentItem().text()) if not question_dialog(self, "{0} {1}: Confirm Delete".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to delete the {1} {0}?".format(keyname, self.key_type_name), show_copy_button=False, default_yes=False): return if type(self.plugin_keys) == dict: @@ -352,9 +360,10 @@ class ManageKeysDialog(QDialog): open_url(QUrl(url)) def migrate_files(self): - dynamic[PLUGIN_NAME + u"config_dir"] = config_dir - files = choose_files(self, PLUGIN_NAME + u"config_dir", - u"Select {0} files to import".format(self.key_type_name), [(u"{0} files".format(self.key_type_name), [self.keyfile_ext])], False) + unique_dlg_name = PLUGIN_NAME + u"import {0} keys".format(self.key_type_name).replace(' ', '_') #takes care of automatically remembering last directory + caption = u"Select {0} files to import".format(self.key_type_name) + filters = [(u"{0} files".format(self.key_type_name), [self.keyfile_ext])] + files = choose_files(self, unique_dlg_name, caption, filters, all_files=False) counter = 0 skipped = 0 if files: @@ -408,17 +417,14 @@ class ManageKeysDialog(QDialog): r = error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), _(errmsg), show=True, show_copy_button=False) return - filter = QString(u"{0} Files (*.{1})".format(self.key_type_name, self.keyfile_ext)) - keyname = unicode(self.listy.currentItem().text().toUtf8(), 'utf8') - if dynamic.get(PLUGIN_NAME + 'save_dir'): - defaultname = os.path.join(dynamic.get(PLUGIN_NAME + 'save_dir'), u"{0}.{1}".format(keyname , self.keyfile_ext)) - else: - defaultname = os.path.join(os.path.expanduser('~'), u"{0}.{1}".format(keyname , self.keyfile_ext)) - filename = unicode(QtGui.QFileDialog.getSaveFileName(self, u"Save {0} File as...".format(self.key_type_name), defaultname, - u"{0} Files (*.{1})".format(self.key_type_name,self.keyfile_ext), filter)) + keyname = unicode(self.listy.currentItem().text()) + unique_dlg_name = PLUGIN_NAME + u"export {0} keys".format(self.key_type_name).replace(' ', '_') #takes care of automatically remembering last directory + caption = u"Save {0} File as...".format(self.key_type_name) + filters = [(u"{0} Files".format(self.key_type_name), [u"{0}".format(self.keyfile_ext)])] + defaultname = u"{0}.{1}".format(keyname, self.keyfile_ext) + filename = choose_save_file(self, unique_dlg_name, caption, filters, all_files=False, initial_filename=defaultname) if filename: - dynamic[PLUGIN_NAME + 'save_dir'] = os.path.split(filename)[0] - with file(filename, 'w') as fname: + with file(filename, 'wb') as fname: if self.binary_file: fname.write(self.plugin_keys[keyname].decode('hex')) elif self.json_file: @@ -458,7 +464,7 @@ class RenameKeyDialog(QDialog): self.resize(self.sizeHint()) def accept(self): - if self.key_ledit.text().isEmpty() or unicode(self.key_ledit.text()).isspace(): + if not unicode(self.key_ledit.text()) or unicode(self.key_ledit.text()).isspace(): errmsg = u"Key name field cannot be empty!" return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), _(errmsg), show=True, show_copy_button=False) @@ -479,7 +485,7 @@ class RenameKeyDialog(QDialog): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @@ -553,7 +559,7 @@ class AddBandNKeyDialog(QDialog): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): @@ -562,11 +568,11 @@ class AddBandNKeyDialog(QDialog): @property def user_name(self): - return unicode(self.name_ledit.text().toUtf8(), 'utf8').strip().lower().replace(' ','') + return unicode(self.name_ledit.text()).strip().lower().replace(' ','') @property def cc_number(self): - return unicode(self.cc_ledit.text().toUtf8(), 'utf8').strip().replace(' ', '').replace('-','') + return unicode(self.cc_ledit.text()).strip().replace(' ', '').replace('-','') def accept(self): @@ -634,7 +640,7 @@ class AddEReaderDialog(QDialog): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): @@ -643,11 +649,11 @@ class AddEReaderDialog(QDialog): @property def user_name(self): - return unicode(self.name_ledit.text().toUtf8(), 'utf8').strip().lower().replace(' ','') + return unicode(self.name_ledit.text()).strip().lower().replace(' ','') @property def cc_number(self): - return unicode(self.cc_ledit.text().toUtf8(), 'utf8').strip().replace(' ', '').replace('-','') + return unicode(self.cc_ledit.text()).strip().replace(' ', '').replace('-','') def accept(self): @@ -719,7 +725,7 @@ class AddAdeptDialog(QDialog): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): @@ -792,7 +798,7 @@ class AddKindleDialog(QDialog): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): @@ -841,11 +847,11 @@ class AddSerialDialog(QDialog): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() def accept(self): if len(self.key_name) == 0 or self.key_name.isspace(): @@ -889,11 +895,11 @@ class AddPIDDialog(QDialog): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() def accept(self): if len(self.key_name) == 0 or self.key_name.isspace(): diff --git a/Other_Tools/Kindle_for_Android_Patches/kindle_version_4.0.2.1/kindle4.0.2.1.patch b/Other_Tools/Kindle_for_Android_Patches/kindle_version_4.0.2.1/kindle4.0.2.1.patch new file mode 100644 index 0000000..010f68f --- /dev/null +++ b/Other_Tools/Kindle_for_Android_Patches/kindle_version_4.0.2.1/kindle4.0.2.1.patch @@ -0,0 +1,238 @@ +Only in kindle4.0.2.1: build +diff -r -u10 kindle4.0.2.1_orig/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali kindle4.0.2.1/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali +--- kindle4.0.2.1_orig/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali 2013-05-22 18:39:03.000000000 -0500 ++++ kindle4.0.2.1/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali 2013-05-23 16:54:53.000000000 -0500 +@@ -36,20 +36,22 @@ + .field private maxCpuSpeed:J + + .field private maxMemory:J + + .field private minCpuSpeed:J + + .field private resources:Landroid/content/res/Resources; + + .field private security:Lcom/mobipocket/android/library/reader/AndroidSecurity; + ++.field private pidList:Ljava/lang/String; ++ + .field private totalMemory:J + + + # direct methods + .method static constructor ()V + .locals 1 + + .prologue + .line 30 + const-class v0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider; +@@ -72,20 +74,24 @@ + .prologue + .line 130 + invoke-direct {p0}, Ljava/lang/Object;->()V + + .line 131 + iput-object p1, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->security:Lcom/mobipocket/android/library/reader/AndroidSecurity; + + .line 132 + iput-object p2, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->deviceType:Lcom/amazon/kcp/application/AmazonDeviceType; + ++ const-string v0, "Open DRMed book to show PID list." ++ ++ iput-object v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String; ++ + .line 133 + sget-object v0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->TAG:Ljava/lang/String; + + new-instance v0, Ljava/lang/StringBuilder; + + invoke-direct {v0}, Ljava/lang/StringBuilder;->()V + + const-string v1, "Device Type is set to \"" + + invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; +@@ -1235,10 +1241,33 @@ + move-result-wide v0 + + iput-wide v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->totalMemory:J + + .line 308 + :cond_0 + iget-wide v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->totalMemory:J + + return-wide v0 + .end method ++ ++.method public getPidList()Ljava/lang/String; ++ .locals 1 ++ ++ .prologue ++ .line 15 ++ iget-object v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String; ++ ++ return-object v0 ++.end method ++ ++.method public setPidList(Ljava/lang/String;)V ++ .locals 0 ++ .parameter "value" ++ ++ .prologue ++ .line 11 ++ iput-object p1, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String; ++ ++ .line 12 ++ return-void ++.end method ++ +diff -r -u10 kindle4.0.2.1_orig/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali kindle4.0.2.1/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali +--- kindle4.0.2.1_orig/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali 2013-05-22 18:39:03.000000000 -0500 ++++ kindle4.0.2.1/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali 2013-05-23 16:55:58.000000000 -0500 +@@ -23,10 +23,16 @@ + .end method + + .method public abstract getDeviceTypeId()Ljava/lang/String; + .end method + + .method public abstract getOsVersion()Ljava/lang/String; + .end method + + .method public abstract getPid()Ljava/lang/String; + .end method ++ ++.method public abstract getPidList()Ljava/lang/String; ++.end method ++ ++.method public abstract setPidList(Ljava/lang/String;)V ++.end method +diff -r -u10 kindle4.0.2.1_orig/smali/com/amazon/kcp/info/AboutActivity.smali kindle4.0.2.1/smali/com/amazon/kcp/info/AboutActivity.smali +--- kindle4.0.2.1_orig/smali/com/amazon/kcp/info/AboutActivity.smali 2013-05-22 18:39:03.000000000 -0500 ++++ kindle4.0.2.1/smali/com/amazon/kcp/info/AboutActivity.smali 2013-05-23 17:18:14.000000000 -0500 +@@ -486,20 +486,71 @@ + .end local v2 #screenDpi:Ljava/lang/String; + :cond_0 + iget-object v5, p0, Lcom/amazon/kcp/info/AboutActivity;->detailItemList:Ljava/util/List; + + invoke-interface {v5, v0}, Ljava/util/List;->add(Ljava/lang/Object;)Z + + .line 317 + return-void + .end method + ++.method private populatePIDList()V ++ .locals 7 ++ ++ .prologue ++ .line 313 ++ invoke-static {}, Lcom/amazon/kcp/application/DeviceInformationProviderFactory;->getProvider()Lcom/amazon/kcp/application/IDeviceInformationProvider; ++ ++ move-result-object v0 ++ ++ invoke-interface {v0}, Lcom/amazon/kcp/application/IDeviceInformationProvider;->getPidList()Ljava/lang/String; ++ ++ move-result-object v1 ++ ++ .line 314 ++ .local v1, PidList:Ljava/lang/String; ++ iget-object v3, p0, Lcom/amazon/kcp/info/AboutActivity;->groupItemList:Ljava/util/List; ++ ++ new-instance v4, Lcom/amazon/kcp/info/AboutActivity$GroupItem; ++ ++ const-string v5, "PID List" ++ ++ const v6, 0x1 ++ ++ invoke-direct {v4, p0, v5, v6}, Lcom/amazon/kcp/info/AboutActivity$GroupItem;->(Lcom/amazon/kcp/info/AboutActivity;Ljava/lang/String;Z)V ++ ++ invoke-interface {v3, v4}, Ljava/util/List;->add(Ljava/lang/Object;)Z ++ ++ .line 315 ++ new-instance v2, Ljava/util/ArrayList; ++ ++ invoke-direct {v2}, Ljava/util/ArrayList;->()V ++ ++ .line 316 ++ .local v2, children:Ljava/util/List;,"Ljava/util/List;" ++ new-instance v3, Lcom/amazon/kcp/info/AboutActivity$DetailItem; ++ ++ const-string v4, "PIDs" ++ ++ invoke-direct {v3, p0, v4, v1}, Lcom/amazon/kcp/info/AboutActivity$DetailItem;->(Lcom/amazon/kcp/info/AboutActivity;Ljava/lang/String;Ljava/lang/String;)V ++ ++ invoke-interface {v2, v3}, Ljava/util/List;->add(Ljava/lang/Object;)Z ++ ++ .line 317 ++ iget-object v3, p0, Lcom/amazon/kcp/info/AboutActivity;->detailItemList:Ljava/util/List; ++ ++ invoke-interface {v3, v2}, Ljava/util/List;->add(Ljava/lang/Object;)Z ++ ++ .line 318 ++ return-void ++.end method ++ + .method private populateDisplayItems()V + .locals 1 + + .prologue + .line 171 + iget-object v0, p0, Lcom/amazon/kcp/info/AboutActivity;->groupItemList:Ljava/util/List; + + if-nez v0, :cond_0 + + .line 173 +@@ -531,20 +582,22 @@ + + .line 192 + invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populateRamInformation()V + + .line 193 + invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populateStorageInformation()V + + .line 194 + invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populateDisplayInformation()V + ++ invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populatePIDList()V ++ + .line 195 + return-void + + .line 177 + :cond_0 + iget-object v0, p0, Lcom/amazon/kcp/info/AboutActivity;->groupItemList:Ljava/util/List; + + invoke-interface {v0}, Ljava/util/List;->clear()V + + goto :goto_0 +diff -r -u10 kindle4.0.2.1_orig/smali/com/amazon/system/security/Security.smali kindle4.0.2.1/smali/com/amazon/system/security/Security.smali +--- kindle4.0.2.1_orig/smali/com/amazon/system/security/Security.smali 2013-05-22 18:39:04.000000000 -0500 ++++ kindle4.0.2.1/smali/com/amazon/system/security/Security.smali 2013-05-23 17:19:05.000000000 -0500 +@@ -920,20 +920,30 @@ + + .line 350 + :cond_2 + add-int/lit8 v8, v8, 0x1 + + .line 351 + sget-object v0, Lcom/amazon/system/security/Security;->CUSTOM_PID_FOR_BUNDLED_DICTIONARY_DRM:Ljava/lang/String; + + aput-object v0, v6, v8 + ++ invoke-static {}, Lcom/amazon/kcp/application/DeviceInformationProviderFactory;->getProvider()Lcom/amazon/kcp/application/IDeviceInformationProvider; ++ ++ move-result-object v5 ++ ++ invoke-static {v6}, Ljava/util/Arrays;->toString([Ljava/lang/Object;)Ljava/lang/String; ++ ++ move-result-object v2 ++ ++ invoke-interface {v5, v2}, Lcom/amazon/kcp/application/IDeviceInformationProvider;->setPidList(Ljava/lang/String;)V ++ + .line 353 + return-object v6 + .end method + + + # virtual methods + .method public customDrmOnly()I + .locals 1 + + .prologue diff --git a/Other_Tools/Kobo/obok_2.01.py b/Other_Tools/Kobo/obok_2.01.py new file mode 100644 index 0000000..3ed7cbd --- /dev/null +++ b/Other_Tools/Kobo/obok_2.01.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python +# +# Updated September 2013 by Anon +# Version 2.01 +# Incorporated minor fixes posted at Apprentice Alf's. +# +# Updates July 2012 by Michael Newton +# PWSD ID is no longer a MAC address, but should always +# be stored in the registry. Script now works with OS X +# and checks plist for values instead of registry. Must +# have biplist installed for OS X support. +# +########################################################## +# KOBO DRM CRACK BY # +# PHYSISTICATED # +########################################################## +# This app was made for Python 2.7 on Windows 32-bit +# +# This app needs pycrypto - get from here: +# http://www.voidspace.org.uk/python/modules.shtml +# +# Usage: obok.py +# Choose the book you want to decrypt +# +# Shouts to my krew - you know who you are - and one in +# particular who gave me a lot of help with this - thank +# you so much! +# +# Kopimi /K\ +# Keep sharing, keep copying, but remember that nothing is +# for free - make sure you compensate your favorite +# authors - and cut out the middle man whenever possible +# ;) ;) ;) +# +# DRM AUTOPSY +# The Kobo DRM was incredibly easy to crack, but it took +# me months to get around to making this. Here's the +# basics of how it works: +# 1: Get MAC address of first NIC in ipconfig (sometimes +# stored in registry as pwsdid) +# 2: Get user ID (stored in tons of places, this gets it +# from HKEY_CURRENT_USER\Software\Kobo\Kobo Desktop +# Edition\Browser\cookies) +# 3: Concatenate and SHA256, take the second half - this +# is your master key +# 4: Open %LOCALAPPDATA%\Kobo Desktop Editions\Kobo.sqlite +# and dump content_keys +# 5: Unbase64 the keys, then decode these with the master +# key - these are your page keys +# 6: Unzip EPUB of your choice, decrypt each page with its +# page key, then zip back up again +# +# WHY USE THIS WHEN INEPT WORKS FINE? (adobe DRM stripper) +# Inept works very well, but authors on Kobo can choose +# what DRM they want to use - and some have chosen not to +# let people download them with Adobe Digital Editions - +# they would rather lock you into a single platform. +# +# With Obok, you can sync Kobo Desktop, decrypt all your +# ebooks, and then use them on whatever device you want +# - you bought them, you own them, you can do what you +# like with them. +# +# Obok is Kobo backwards, but it is also means "next to" +# in Polish. +# When you buy a real book, it is right next to you. You +# can read it at home, at work, on a train, you can lend +# it to a friend, you can scribble on it, and add your own +# explanations/translations. +# +# Obok gives you this power over your ebooks - no longer +# are you restricted to one device. This allows you to +# embed foreign fonts into your books, as older Kobo's +# can't display them properly. You can read your books +# on your phones, in different PC readers, and different +# ereader devices. You can share them with your friends +# too, if you like - you can do that with a real book +# after all. +# +""" +Decrypt Kobo encrypted EPUB books. +""" + +import os +import sys +if sys.platform.startswith('win'): + import _winreg +elif sys.platform.startswith('darwin'): + from biplist import readPlist +import re +import string +import hashlib +import sqlite3 +import base64 +import binascii +import zipfile +from Crypto.Cipher import AES + +def SHA256(raw): + return hashlib.sha256(raw).hexdigest() + +def RemoveAESPadding(contents): + lastchar = binascii.b2a_hex(contents[-1:]) + strlen = int(lastchar, 16) + padding = strlen + if(strlen == 1): + return contents[:-1] + if(strlen < 16): + for i in range(strlen): + testchar = binascii.b2a_hex(contents[-strlen:-(strlen-1)]) + if(testchar != lastchar): + padding = 0 + if(padding > 0): + contents = contents[:-padding] + return contents + +def GetVolumeKeys(dbase, enc): + volumekeys = {} + for row in dbase.execute("SELECT * from content_keys"): + if(row[0] not in volumekeys): + volumekeys[row[0]] = {} + volumekeys[row[0]][row[1]] = {} + volumekeys[row[0]][row[1]]["encryptedkey"] = base64.b64decode(row[2]) + volumekeys[row[0]][row[1]]["decryptedkey"] = enc.decrypt(volumekeys[row[0]][row[1]]["encryptedkey"]) + # get book name + for key in volumekeys.keys(): + volumekeys[key]["title"] = dbase.execute("SELECT Title from content where ContentID = '%s'" % (key)).fetchone()[0] + return volumekeys + +def ByteArrayToString(bytearr): + wincheck = re.match("@ByteArray\\((.+)\\)", bytearr) + if wincheck: + return wincheck.group(1) + return bytearr + +def GetUserHexKey(prefs = ""): + "find wsuid and pwsdid" + wsuid = "" + pwsdid = "" + if sys.platform.startswith('win'): + regkey_browser = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, "Software\\Kobo\\Kobo Desktop Edition\\Browser") + cookies = _winreg.QueryValueEx(regkey_browser, "cookies") + bytearrays = cookies[0] + elif sys.platform.startswith('darwin'): + cookies = readPlist(prefs) + bytearrays = cookies["Browser.cookies"] + for bytearr in bytearrays: + cookie = ByteArrayToString(bytearr) + print cookie + wsuidcheck = re.match("^wsuid=([0-9a-f-]+)", cookie) + if(wsuidcheck): + wsuid = wsuidcheck.group(1) + pwsdidcheck = re.match("^pwsdid=([0-9a-f-]+)", cookie) + if (pwsdidcheck): + pwsdid = pwsdidcheck.group(1) + + if(wsuid == "" or pwsdid == ""): + print "wsuid or pwsdid key not found :/" + exit() + preuserkey = string.join((pwsdid, wsuid), "") + print SHA256(pwsdid) + userkey = SHA256(preuserkey) + return userkey[32:] + +# get dirs +if sys.platform.startswith('win'): + delim = "\\" + if (sys.getwindowsversion().major > 5): + kobodir = string.join((os.environ['LOCALAPPDATA'], "Kobo\\Kobo Desktop Edition"), delim) + else: + kobodir = string.join((os.environ['USERPROFILE'], "Local Settings\\Application Data\\Kobo\\Kobo Desktop Edition"), delim) + prefs = "" +elif sys.platform.startswith('darwin'): + delim = "/" + kobodir = string.join((os.environ['HOME'], "Library/Application Support/Kobo/Kobo Desktop Edition"), delim) + prefs = string.join((os.environ['HOME'], "Library/Preferences/com.kobo.Kobo Desktop Edition.plist"), delim) +sqlitefile = string.join((kobodir, "Kobo.sqlite"), delim) +bookdir = string.join((kobodir, "kepub"), delim) + +# get key +userkeyhex = GetUserHexKey(prefs) +# load into AES +userkey = binascii.a2b_hex(userkeyhex) +enc = AES.new(userkey, AES.MODE_ECB) + +# open sqlite +conn = sqlite3.connect(sqlitefile) +dbcursor = conn.cursor() +# get volume keys +volumekeys = GetVolumeKeys(dbcursor, enc) + +# choose a volumeID + +volumeid = "" +print "Choose a book to decrypt:" +i = 1 +for key in volumekeys.keys(): + print "%d: %s" % (i, volumekeys[key]["title"]) + i += 1 + +num = input("...") + +i = 1 +for key in volumekeys.keys(): + if(i == num): + volumeid = key + i += 1 + +if(volumeid == ""): + exit() + +zippath = string.join((bookdir, volumeid), delim) + +z = zipfile.ZipFile(zippath, "r") +# make filename out of Unicode alphanumeric and whitespace equivalents from title +outname = "%s.epub" % (re.sub("[^\s\w]", "", volumekeys[volumeid]["title"], 0, re.UNICODE)) +zout = zipfile.ZipFile(outname, "w", zipfile.ZIP_DEFLATED) +for filename in z.namelist(): + #print filename + # read in and decrypt + if(filename in volumekeys[volumeid]): + # do decrypted version + pagekey = volumekeys[volumeid][filename]["decryptedkey"] + penc = AES.new(pagekey, AES.MODE_ECB) + contents = RemoveAESPadding(penc.decrypt(z.read(filename))) + # need to fix padding + zout.writestr(filename, contents) + else: + zout.writestr(filename, z.read(filename)) + +print "Book saved as %s%s%s" % (os.getcwd(), delim, outname) \ No newline at end of file