parent
748bd2d471
commit
20bc936e99
@ -1,274 +0,0 @@
|
||||
#!/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 The Dark Reverser for the original mobidedrm script.
|
||||
# Thanks to all those who've worked on the scripts since 2008 to improve
|
||||
# the support for formats and sources.
|
||||
#
|
||||
# Revision history:
|
||||
# 0.4.8 - Major code change to use unaltered k4mobidedrm.py 4.8 and later
|
||||
# 0.4.9 - typo fix
|
||||
# 0.4.10 - Another Topaz Fix (class added to page and group and region)
|
||||
# 0.4.11 - Fixed Linux support of K4PC
|
||||
# 0.4.12 - More Linux Wine fixes
|
||||
# 0.4.13 - Ancient Mobipocket files fix
|
||||
# 0.4.14 - Error on invalid character in book names fix
|
||||
# 0.4.15 - Another Topaz fix
|
||||
# 0.4.16 - Yet another Topaz fix
|
||||
# 0.4.17 - Manage to include the actual fix.
|
||||
# 0.4.18 - More Topaz fixes
|
||||
# 0.4.19 - MobiDeDRM PalmDoc fix
|
||||
|
||||
"""
|
||||
Decrypt Amazon Kindle and Mobipocket encrypted ebooks.
|
||||
"""
|
||||
|
||||
PLUGIN_NAME = u"Kindle and Mobipocket DeDRM"
|
||||
PLUGIN_VERSION_TUPLE = (0, 4, 19)
|
||||
PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE])
|
||||
|
||||
import sys, os, re
|
||||
import time
|
||||
from zipfile import ZipFile
|
||||
|
||||
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
|
||||
# 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 K4DeDRM(FileTypePlugin):
|
||||
name = PLUGIN_NAME
|
||||
description = u"Removes DRM from eInk Kindle, Kindle 4 Mac and Kindle 4 PC ebooks, and from Mobipocket ebooks. Provided by the work of many including The Dark Reverser, DiapDealer, SomeUpdates, i♥cabbages, CMBDTC, Skindle, mdlnx, ApprenticeAlf, and probably others."
|
||||
supported_platforms = ['osx', 'windows', 'linux'] # Platforms this plugin will run on
|
||||
author = u"DiapDealer, SomeUpdates, mdlnx, Apprentice Alf and The Dark Reverser"
|
||||
version = PLUGIN_VERSION_TUPLE
|
||||
file_types = set(['prc','mobi','azw','azw1','azw3','azw4','tpz']) # The file types that this plugin will be applied to
|
||||
on_import = True # Run this plugin during the import
|
||||
priority = 521 # run this plugin before earlier versions
|
||||
minimum_calibre_version = (0, 7, 55)
|
||||
|
||||
def initialize(self):
|
||||
"""
|
||||
Dynamic modules can't be imported/loaded from a zipfile... so this routine
|
||||
runs whenever the plugin gets initialized. This will extract the appropriate
|
||||
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.
|
||||
"""
|
||||
if iswindows:
|
||||
names = [u"alfcrypto.dll",u"alfcrypto64.dll"]
|
||||
elif isosx:
|
||||
names = [u"libalfcrypto.dylib"]
|
||||
else:
|
||||
names = [u"libalfcrypto32.so",u"libalfcrypto64.so",u"alfcrypto.py",u"alfcrypto.dll",u"alfcrypto64.dll",u"getk4pcpids.py",u"k4mobidedrm.py",u"mobidedrm.py",u"kgenpids.py",u"k4pcutils.py",u"topazextract.py"]
|
||||
lib_dict = self.load_resources(names)
|
||||
self.alfdir = os.path.join(config_dir,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)
|
||||
|
||||
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)
|
||||
|
||||
starttime = time.time()
|
||||
print u"{0} v{1}: Trying to decrypt {2}.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(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 these imports here so the custom libs can be
|
||||
# extracted to the appropriate places beforehand these routines
|
||||
# look for them.
|
||||
from calibre_plugins.k4mobidedrm import k4mobidedrm
|
||||
|
||||
k4 = True
|
||||
pids = []
|
||||
serials = []
|
||||
kInfoFiles = []
|
||||
|
||||
self.config()
|
||||
|
||||
# Get supplied list of PIDs to try from plugin customization.
|
||||
pidstringlistt = self.pids_string.split(',')
|
||||
for pid in pidstringlistt:
|
||||
pid = str(pid).strip()
|
||||
if len(pid) == 10 or len(pid) == 8:
|
||||
pids.append(pid)
|
||||
else:
|
||||
if len(pid) > 0:
|
||||
print u"{0} v{1}: \'{2}\' is not a valid Mobipocket PID.".format(PLUGIN_NAME, PLUGIN_VERSION, pid)
|
||||
|
||||
# For linux, get PIDs by calling the right routines under WINE
|
||||
if sys.platform.startswith('linux'):
|
||||
k4 = False
|
||||
pids.extend(self.WINEgetPIDs(path_to_ebook))
|
||||
|
||||
# Get supplied list of Kindle serial numbers to try from plugin customization.
|
||||
serialstringlistt = self.serials_string.split(',')
|
||||
for serial in serialstringlistt:
|
||||
serial = str(serial).replace(" ","")
|
||||
if len(serial) == 16 and serial[0] in ['B','9']:
|
||||
serials.append(serial)
|
||||
else:
|
||||
if len(serial) > 0:
|
||||
print u"{0} v{1}: \'{2}\' is not a valid eInk Kindle serial number.".format(PLUGIN_NAME, PLUGIN_VERSION, serial)
|
||||
|
||||
# Load any kindle info files (*.info) included Calibre's config directory.
|
||||
try:
|
||||
print u"{0} v{1}: Calibre configuration directory is {2}".format(PLUGIN_NAME, PLUGIN_VERSION, config_dir)
|
||||
files = os.listdir(config_dir)
|
||||
filefilter = re.compile("\.info$|\.kinf$", re.IGNORECASE)
|
||||
files = filter(filefilter.search, files)
|
||||
if files:
|
||||
for filename in files:
|
||||
fpath = os.path.join(config_dir, filename)
|
||||
kInfoFiles.append(fpath)
|
||||
print u"{0} v{1}: Kindle info/kinf file {2} found in config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, filename)
|
||||
except IOError, e:
|
||||
print u"{0} v{1}: Error \'{2}\' reading kindle info/kinf files from config directory.".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0])
|
||||
pass
|
||||
|
||||
try:
|
||||
book = k4mobidedrm.GetDecryptedBook(path_to_ebook,kInfoFiles,serials,pids,starttime)
|
||||
except Exception, e:
|
||||
#if you reached here then no luck raise and exception
|
||||
if is_ok_to_use_qt():
|
||||
from PyQt4.Qt import QMessageBox
|
||||
d = QMessageBox(QMessageBox.Warning, u"{0} v{1}".format(PLUGIN_NAME, PLUGIN_VERSION), u"Error after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime))
|
||||
d.show()
|
||||
d.raise_()
|
||||
d.exec_()
|
||||
raise Exception(u"{0} v{1}: Error after {3:.1f} seconds: {2}".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0],time.time()-starttime))
|
||||
|
||||
|
||||
print u"{0} v{1}: Successfully decrypted book after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-starttime)
|
||||
|
||||
of = self.temporary_file(u"decrypted_ebook.{0}".format(book.getBookExtension()))
|
||||
book.getFile(of.name)
|
||||
book.cleanup()
|
||||
return of.name
|
||||
|
||||
def WINEgetPIDs(self, infile):
|
||||
|
||||
import subprocess
|
||||
from subprocess import Popen, PIPE, STDOUT
|
||||
|
||||
import subasyncio
|
||||
from subasyncio import Process
|
||||
|
||||
print u" Getting PIDs from Wine"
|
||||
|
||||
outfile = os.path.join(self.alfdir + u"winepids.txt")
|
||||
# Remove any previous winepids.txt file.
|
||||
if os.path.exists(outfile):
|
||||
os.remove(outfile)
|
||||
|
||||
cmdline = u"wine python.exe \"{0}/getk4pcpids.py\" \"{1}\" \"{2}\"".format(self.alfdir,infile,outfile)
|
||||
env = os.environ
|
||||
|
||||
print u"wine_prefix from tweaks is \'{0}\'".format(self.wine_prefix)
|
||||
|
||||
if ("WINEPREFIX" in env):
|
||||
print u"Using WINEPREFIX from the environment instead: \'{0}\'".format(env["WINEPREFIX"])
|
||||
elif (self.wine_prefix is not None):
|
||||
env["WINEPREFIX"] = self.wine_prefix
|
||||
print u"Using WINEPREFIX from tweaks \'{0}\'".format(self.wine_prefix)
|
||||
else:
|
||||
print u"No wine prefix used."
|
||||
|
||||
print u"Trying command: {0}".format(cmdline)
|
||||
|
||||
try:
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p2 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=sys.stdout, stderr=STDOUT, close_fds=False)
|
||||
result = p2.wait("wait")
|
||||
except Exception, e:
|
||||
print u"WINE subprocess error: {0}".format(e.args[0])
|
||||
return []
|
||||
print u"WINE subprocess returned {0}".format(result)
|
||||
|
||||
WINEpids = []
|
||||
if os.path.exists(outfile):
|
||||
try:
|
||||
customvalues = file(outfile, 'r').readline().split(',')
|
||||
for customvalue in customvalues:
|
||||
customvalue = str(customvalue)
|
||||
customvalue = customvalue.strip()
|
||||
if len(customvalue) == 10 or len(customvalue) == 8:
|
||||
WINEpids.append(customvalue)
|
||||
print u"Found PID '{0}'".format(customvalue)
|
||||
else:
|
||||
print u"'{0}' is not a valid PID.".format(customvalue)
|
||||
except Exception, e:
|
||||
print u"Error parsing winepids.txt: {0}".format(e.args[0])
|
||||
return []
|
||||
if len(WINEpids) == 0:
|
||||
print u"No PIDs generated by Wine Python subprocess."
|
||||
return WINEpids
|
||||
|
||||
def is_customizable(self):
|
||||
# return true to allow customization via the Plugin->Preferences.
|
||||
return True
|
||||
|
||||
def config_widget(self):
|
||||
# It is important to put this import statement here rather than at the
|
||||
# top of the module as importing the config class will also cause the
|
||||
# GUI libraries to be loaded, which we do not want when using calibre
|
||||
# from the command line
|
||||
from calibre_plugins.k4mobidedrm.config import ConfigWidget
|
||||
return config.ConfigWidget()
|
||||
|
||||
def config(self):
|
||||
from calibre_plugins.k4mobidedrm.config import prefs
|
||||
|
||||
self.pids_string = prefs['pids']
|
||||
self.serials_string = prefs['serials']
|
||||
self.wine_prefix = prefs['WINEPREFIX']
|
||||
|
||||
def save_settings(self, config_widget):
|
||||
'''
|
||||
Save the settings specified by the user with config_widget.
|
||||
'''
|
||||
config_widget.save_settings()
|
||||
self.config()
|
||||
|
||||
def load_resources(self, names):
|
||||
ans = {}
|
||||
with ZipFile(self.plugin_path, 'r') as zf:
|
||||
for candidate in zf.namelist():
|
||||
if candidate in names:
|
||||
ans[candidate] = zf.read(candidate)
|
||||
return ans
|
@ -1,62 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from PyQt4.Qt import QWidget, QVBoxLayout, QLabel, QLineEdit
|
||||
|
||||
from calibre.utils.config import JSONConfig
|
||||
|
||||
# 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')
|
||||
|
||||
# Set defaults
|
||||
prefs.defaults['pids'] = ""
|
||||
prefs.defaults['serials'] = ""
|
||||
prefs.defaults['WINEPREFIX'] = None
|
||||
|
||||
|
||||
class ConfigWidget(QWidget):
|
||||
|
||||
def __init__(self):
|
||||
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.serials = QLineEdit(self)
|
||||
self.serials.setText(prefs['serials'])
|
||||
self.l.addWidget(self.serials)
|
||||
self.serialLabel.setBuddy(self.serials)
|
||||
|
||||
self.pidLabel = QLabel('Mobipocket PIDs (8 or 10 characters, use commas if more than one)')
|
||||
self.l.addWidget(self.pidLabel)
|
||||
|
||||
self.pids = QLineEdit(self)
|
||||
self.pids.setText(prefs['pids'])
|
||||
self.l.addWidget(self.pids)
|
||||
self.pidLabel.setBuddy(self.serials)
|
||||
|
||||
self.wpLabel = QLabel('For Linux only: WINEPREFIX (enter absolute path)')
|
||||
self.l.addWidget(self.wpLabel)
|
||||
|
||||
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)
|
||||
|
||||
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
|
@ -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())
|
@ -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)
|
@ -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 <bellman@lysator.liu.se>"
|
||||
# available under GPL version 3 or Later
|
||||
|
||||
# create an asynchronous subprocess whose output can be collected in
|
||||
# a non-blocking manner
|
||||
|
||||
# What a mess! Have to use threads just to get non-blocking io
|
||||
# in a cross-platform manner
|
||||
|
||||
# luckily all thread use is hidden within this class
|
||||
|
||||
class Process(object):
|
||||
def __init__(self, *params, **kwparams):
|
||||
if len(params) <= 3:
|
||||
kwparams.setdefault('stdin', subprocess.PIPE)
|
||||
if len(params) <= 4:
|
||||
kwparams.setdefault('stdout', subprocess.PIPE)
|
||||
if len(params) <= 5:
|
||||
kwparams.setdefault('stderr', subprocess.PIPE)
|
||||
self.__pending_input = []
|
||||
self.__collected_outdata = []
|
||||
self.__collected_errdata = []
|
||||
self.__exitstatus = None
|
||||
self.__lock = threading.Lock()
|
||||
self.__inputsem = threading.Semaphore(0)
|
||||
self.__quit = False
|
||||
|
||||
self.__process = subprocess.Popen(*params, **kwparams)
|
||||
|
||||
if self.__process.stdin:
|
||||
self.__stdin_thread = threading.Thread(
|
||||
name="stdin-thread",
|
||||
target=self.__feeder, args=(self.__pending_input,
|
||||
self.__process.stdin))
|
||||
self.__stdin_thread.setDaemon(True)
|
||||
self.__stdin_thread.start()
|
||||
|
||||
if self.__process.stdout:
|
||||
self.__stdout_thread = threading.Thread(
|
||||
name="stdout-thread",
|
||||
target=self.__reader, args=(self.__collected_outdata,
|
||||
self.__process.stdout))
|
||||
self.__stdout_thread.setDaemon(True)
|
||||
self.__stdout_thread.start()
|
||||
|
||||
if self.__process.stderr:
|
||||
self.__stderr_thread = threading.Thread(
|
||||
name="stderr-thread",
|
||||
target=self.__reader, args=(self.__collected_errdata,
|
||||
self.__process.stderr))
|
||||
self.__stderr_thread.setDaemon(True)
|
||||
self.__stderr_thread.start()
|
||||
|
||||
def pid(self):
|
||||
return self.__process.pid
|
||||
|
||||
def kill(self, signal):
|
||||
self.__process.send_signal(signal)
|
||||
|
||||
# check on subprocess (pass in 'nowait') to act like poll
|
||||
def wait(self, flag):
|
||||
if flag.lower() == 'nowait':
|
||||
rc = self.__process.poll()
|
||||
else:
|
||||
rc = self.__process.wait()
|
||||
if rc != None:
|
||||
if self.__process.stdin:
|
||||
self.closeinput()
|
||||
if self.__process.stdout:
|
||||
self.__stdout_thread.join()
|
||||
if self.__process.stderr:
|
||||
self.__stderr_thread.join()
|
||||
return self.__process.returncode
|
||||
|
||||
def terminate(self):
|
||||
if self.__process.stdin:
|
||||
self.closeinput()
|
||||
self.__process.terminate()
|
||||
|
||||
# thread gets data from subprocess stdout
|
||||
def __reader(self, collector, source):
|
||||
while True:
|
||||
data = os.read(source.fileno(), 65536)
|
||||
self.__lock.acquire()
|
||||
collector.append(data)
|
||||
self.__lock.release()
|
||||
if data == "":
|
||||
source.close()
|
||||
break
|
||||
return
|
||||
|
||||
# thread feeds data to subprocess stdin
|
||||
def __feeder(self, pending, drain):
|
||||
while True:
|
||||
self.__inputsem.acquire()
|
||||
self.__lock.acquire()
|
||||
if not pending and self.__quit:
|
||||
drain.close()
|
||||
self.__lock.release()
|
||||
break
|
||||
data = pending.pop(0)
|
||||
self.__lock.release()
|
||||
drain.write(data)
|
||||
|
||||
# non-blocking read of data from subprocess stdout
|
||||
def read(self):
|
||||
self.__lock.acquire()
|
||||
outdata = "".join(self.__collected_outdata)
|
||||
del self.__collected_outdata[:]
|
||||
self.__lock.release()
|
||||
return outdata
|
||||
|
||||
# non-blocking read of data from subprocess stderr
|
||||
def readerr(self):
|
||||
self.__lock.acquire()
|
||||
errdata = "".join(self.__collected_errdata)
|
||||
del self.__collected_errdata[:]
|
||||
self.__lock.release()
|
||||
return errdata
|
||||
|
||||
# non-blocking write to stdin of subprocess
|
||||
def write(self, data):
|
||||
if self.__process.stdin is None:
|
||||
raise ValueError("Writing to process with stdin not a pipe")
|
||||
self.__lock.acquire()
|
||||
self.__pending_input.append(data)
|
||||
self.__inputsem.release()
|
||||
self.__lock.release()
|
||||
|
||||
# close stdinput of subprocess
|
||||
def closeinput(self):
|
||||
self.__lock.acquire()
|
||||
self.__quit = True
|
||||
self.__inputsem.release()
|
||||
self.__lock.release()
|
Binary file not shown.
@ -1,124 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# eReaderPDB2PML_plugin.py
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
#
|
||||
# All credit given to The Dark Reverser for the original standalone script.
|
||||
# I had the much easier job of converting it to Calibre a plugin.
|
||||
#
|
||||
# This plugin is meant to convert secure Ereader files (PDB) to unsecured PMLZ files.
|
||||
# Calibre can then convert it to whatever format you desire.
|
||||
# It is meant to function without having to install any dependencies...
|
||||
# other than having Calibre installed, of course.
|
||||
#
|
||||
# Installation:
|
||||
# Go to Calibre's Preferences page... click on the Plugins button. Use the file
|
||||
# dialog button to select the plugin's zip file (eReaderPDB2PML_vXX_plugin.zip) and
|
||||
# click the 'Add' button. You're done.
|
||||
#
|
||||
# Configuration:
|
||||
# Highlight the plugin (eReader PDB 2 PML) and click the
|
||||
# "Customize Plugin" button on Calibre's Preferences->Plugins page.
|
||||
# Enter your name and the last 8 digits of the credit card number separated by
|
||||
# a comma: Your Name,12341234
|
||||
#
|
||||
# If you've purchased books with more than one credit card, separate the info with
|
||||
# a colon: Your Name,12341234:Other Name,23452345
|
||||
# NOTE: Do NOT put quotes around your name like you do with the original script!!
|
||||
#
|
||||
# Revision history:
|
||||
# 0.0.1 - Initial release
|
||||
# 0.0.2 - updated to distinguish it from earlier non-openssl version
|
||||
# 0.0.3 - removed added psyco code as it is not supported under Calibre's Python 2.7
|
||||
# 0.0.4 - minor typos fixed
|
||||
# 0.0.5 - updated to the new calibre plugin interface
|
||||
# 0.0.6 - unknown changes
|
||||
# 0.0.7 - improved config dialog processing and fix possible output/unicode problem
|
||||
# 0.0.8 - Proper fix for unicode problems, separate out erdr2pml from plugin
|
||||
|
||||
PLUGIN_NAME = u"eReader PDB 2 PML"
|
||||
PLUGIN_VERSION_TUPLE = (0, 0, 8)
|
||||
PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE])
|
||||
|
||||
import sys, os
|
||||
|
||||
from calibre.customize import FileTypePlugin
|
||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||
from calibre.constants import iswindows, isosx
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
|
||||
class eRdrDeDRM(FileTypePlugin):
|
||||
name = PLUGIN_NAME
|
||||
description = u"Removes DRM from secure pdb files. Credit given to The Dark Reverser for the original standalone script."
|
||||
supported_platforms = ['linux', 'osx', 'windows'] # Platforms this plugin will run on
|
||||
author = u"DiapDealer, Apprentice Alf and The Dark Reverser"
|
||||
version = PLUGIN_VERSION_TUPLE
|
||||
file_types = set(['pdb']) # The file types that this plugin will be applied to
|
||||
on_import = True # Run this plugin during the import
|
||||
minimum_calibre_version = (0, 7, 55)
|
||||
priority = 100
|
||||
|
||||
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))
|
||||
|
||||
infile = path_to_ebook
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
outdir = PersistentTemporaryDirectory()
|
||||
pmlzfile = self.temporary_file(bookname + '.pmlz')
|
||||
|
||||
if self.site_customization:
|
||||
from calibre_plugins.erdrpdb2pml import erdr2pml
|
||||
|
||||
keydata = self.site_customization
|
||||
ar = keydata.split(':')
|
||||
for i in ar:
|
||||
try:
|
||||
name, cc = i.split(',')
|
||||
user_key = erdr2pml.getuser_key(name,cc)
|
||||
except ValueError:
|
||||
print u"{0} v{1}: Error parsing user supplied data.".format(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
return path_to_ebook
|
||||
|
||||
try:
|
||||
print u"{0} v{1}: Processing...".format(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
import time
|
||||
start_time = time.time()
|
||||
if erdr2pml.decryptBook(infile,pmlzfile.name,True,user_key) == 0:
|
||||
print u"{0} v{1}: Elapsed time: {2:.2f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-start_time)
|
||||
return pmlzfile.name
|
||||
else:
|
||||
raise ValueError(u"{0} v{1}: Error Creating PML file.".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
except ValueError, e:
|
||||
print u"{0} v{1}: Error: {2}".format(PLUGIN_NAME, PLUGIN_VERSION,e.args[0])
|
||||
pass
|
||||
raise Exception(u"{0} v{1}: Couldn\'t decrypt pdb file. See Apprentice Alf's blog for help.".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
else:
|
||||
raise Exception(u"{0} v{1}: No name and CC# provided.".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
|
||||
|
||||
def customization_help(self, gui=False):
|
||||
return u"Enter Account Name & Last 8 digits of Credit Card number (separate with a comma, multiple pairs with a colon)"
|
Binary file not shown.
Binary file not shown.
@ -1,188 +0,0 @@
|
||||
#!/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 for the original standalone scripts.
|
||||
# I had the much easier job of converting them to a calibre plugin.
|
||||
#
|
||||
# This plugin is meant to decrypt Barnes & Noble Epubs that are protected
|
||||
# with a version of Adobe's Adept encryption. It is meant to function without having to
|
||||
# install any dependencies... other than having calibre installed, of course. It will still
|
||||
# work if you have Python and PyCrypto already installed, but they aren't necessary.
|
||||
#
|
||||
# Configuration:
|
||||
# Check out the plugin's configuration settings by clicking the "Customize plugin"
|
||||
# button when you have the "BnN ePub 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:
|
||||
# 0.1.0 - Initial release
|
||||
# 0.1.1 - Allow Windows users to make use of openssl if they have it installed.
|
||||
# - Incorporated SomeUpdates zipfix routine.
|
||||
# 0.1.2 - bug fix for non-ascii file names in encryption.xml
|
||||
# 0.1.3 - Try PyCrypto on Windows first
|
||||
# 0.1.4 - update zipfix to deal with mimetype not in correct place
|
||||
# 0.1.5 - update zipfix to deal with completely missing mimetype files
|
||||
# 0.1.6 - update for the new calibre plugin interface
|
||||
# 0.1.7 - Fix for potential problem with PyCrypto
|
||||
# 0.1.8 - an updated/modified zipfix.py and included zipfilerugged.py
|
||||
# 0.2.0 - Completely overhauled plugin configuration dialog and key management/storage
|
||||
# 0.2.1 - added zipfix.py and included zipfilerugged.py from 0.1.8
|
||||
# 0.2.2 - added in potential fixes from 0.1.7 that had been missed.
|
||||
# 0.2.3 - fixed possible output/unicode problem
|
||||
# 0.2.4 - ditched nearly hopeless caselessStrCmp method in favor of uStrCmp.
|
||||
# - added ability to rename existing keys.
|
||||
# 0.2.5 - Major code change to use unaltered ignobleepub.py 3.6 and
|
||||
# - ignoblekeygen 2.4 and later.
|
||||
# 0.2.6 - Tweaked to eliminate issue with both ignoble and inept calibre plugins installed/enabled at once
|
||||
|
||||
"""
|
||||
Decrypt Barnes & Noble ADEPT encrypted EPUB books.
|
||||
"""
|
||||
|
||||
PLUGIN_NAME = u"Ignoble Epub DeDRM"
|
||||
PLUGIN_VERSION_TUPLE = (0, 2, 6)
|
||||
PLUGIN_VERSION = '.'.join([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 zipfile
|
||||
from zipfile import ZipFile
|
||||
|
||||
class IGNOBLEError(Exception):
|
||||
pass
|
||||
|
||||
from calibre.customize import FileTypePlugin
|
||||
from calibre.constants import iswindows, isosx
|
||||
from calibre.gui2 import is_ok_to_use_qt
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
|
||||
class IgnobleDeDRM(FileTypePlugin):
|
||||
name = PLUGIN_NAME
|
||||
description = u"Removes DRM from secure Barnes & Noble epub files. Credit given to i♥cabbages for the original stand-alone scripts."
|
||||
supported_platforms = ['linux', 'osx', 'windows']
|
||||
author = u"DiapDealer, Apprentice Alf 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'])
|
||||
on_import = True
|
||||
priority = 101
|
||||
|
||||
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))
|
||||
|
||||
# First time use or first time after upgrade to new key-handling/storage method
|
||||
# or no keys configured. Give a visual prompt to configure.
|
||||
import calibre_plugins.ignobleepub.config as cfg
|
||||
if not cfg.prefs['configured']:
|
||||
titlemsg = '%s v%s' % (PLUGIN_NAME, PLUGIN_VERSION)
|
||||
errmsg = titlemsg + ' not (properly) configured!\n' + \
|
||||
'\nThis may be the first time you\'ve used this plugin' + \
|
||||
' (or the first time since upgrading this plugin).' + \
|
||||
' You\'ll need to open the customization dialog (Preferences->Plugins->File type plugins)' + \
|
||||
' and follow the instructions there.\n' + \
|
||||
'\nIf you don\'t use the ' + PLUGIN_NAME + ' plugin, you should disable or uninstall it.'
|
||||
if is_ok_to_use_qt():
|
||||
from PyQt4.Qt import QMessageBox
|
||||
d = QMessageBox(QMessageBox.Warning, titlemsg, errmsg )
|
||||
d.show()
|
||||
d.raise_()
|
||||
d.exec_()
|
||||
raise Exception('%s Plugin v%s: Plugin not configured.' % (PLUGIN_NAME, PLUGIN_VERSION))
|
||||
|
||||
# Create a TemporaryPersistent file to work with.
|
||||
# Check original epub archive for zip errors.
|
||||
from calibre_plugins.ignobleepub import 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)
|
||||
return
|
||||
|
||||
#check the book
|
||||
from calibre_plugins.ignobleepub import ignobleepub
|
||||
if not ignobleepub.ignobleBook(inf.name):
|
||||
raise IGNOBLEError(u"{0} v{1}: {2} is not 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 cfg.prefs['keys'].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()
|
||||
|
||||
# Decryption was successful return the modified PersistentTemporary
|
||||
# file to Calibre's import process.
|
||||
if result == 0:
|
||||
print u"{0} v{1}: Encryption successfully removed.".format(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
return of.name
|
||||
break
|
||||
|
||||
print u"{0} v{1}: Encryption key incorrect.".format(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
|
||||
# Something went wrong with decryption.
|
||||
# Import the original unmolested epub.
|
||||
raise IGNOBLEError(u"{0} v{1}: Ultimately failed to decrypt".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
return
|
||||
|
||||
def is_customizable(self):
|
||||
# return true to allow customization via the Plugin->Preferences.
|
||||
return True
|
||||
|
||||
def config_widget(self):
|
||||
from calibre_plugins.ignobleepub.config import ConfigWidget
|
||||
# Extract the helpfile contents from in the plugin's zipfile.
|
||||
# The helpfile must be named <plugin name variable> + '_Help.htm'
|
||||
return ConfigWidget(self.load_resources(RESOURCE_NAME)[RESOURCE_NAME])
|
||||
|
||||
def load_resources(self, names):
|
||||
ans = {}
|
||||
with ZipFile(self.plugin_path, 'r') as zf:
|
||||
for candidate in zf.namelist():
|
||||
if candidate in names:
|
||||
ans[candidate] = zf.read(candidate)
|
||||
return ans
|
||||
|
||||
def save_settings(self, config_widget):
|
||||
config_widget.save_settings()
|
@ -1,305 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
__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
|
||||
|
||||
# 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
|
||||
|
||||
# modules from this plugin's zipfile.
|
||||
from calibre_plugins.ignobleepub.__init__ import PLUGIN_NAME, PLUGIN_VERSION
|
||||
from calibre_plugins.ignobleepub.__init__ import RESOURCE_NAME as help_file_name
|
||||
from calibre_plugins.ignobleepub.utilities import (uStrCmp, DETAILED_MESSAGE, parseCustString)
|
||||
from calibre_plugins.ignobleepub.dialogs import AddKeyDialog, RenameKeyDialog
|
||||
from calibre_plugins.ignobleepub.ignoblekeygen import generate_key
|
||||
|
||||
JSON_NAME = PLUGIN_NAME.strip().lower().replace(' ', '_')
|
||||
JSON_PATH = 'plugins/' + JSON_NAME + '.json'
|
||||
|
||||
# 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(JSON_PATH)
|
||||
|
||||
# Set defaults
|
||||
prefs.defaults['keys'] = {}
|
||||
prefs.defaults['configured'] = False
|
||||
|
||||
class ConfigWidget(QWidget):
|
||||
def __init__(self, help_file_data):
|
||||
QWidget.__init__(self)
|
||||
|
||||
self.help_file_data = help_file_data
|
||||
self.plugin_keys = prefs['keys']
|
||||
|
||||
# Handle the old plugin's customization string by either converting the
|
||||
# old string to stored keys or by saving the string to a text file of the
|
||||
# user's choice. Either way... get that personal data out of plain sight.
|
||||
from calibre.customize.ui import config
|
||||
sc = config['plugin_customization']
|
||||
val = sc.get(PLUGIN_NAME, None)
|
||||
if val is not None:
|
||||
title = 'Convert existing customization data?'
|
||||
msg = '<p>Convert your existing insecure customization data? (Please '+ \
|
||||
'read the detailed message)'
|
||||
det_msg = DETAILED_MESSAGE
|
||||
|
||||
# Offer to convert the old string to the new format
|
||||
if question_dialog(self, _(title), _(msg), det_msg, True, True):
|
||||
userkeys = parseCustString(str(val))
|
||||
if userkeys:
|
||||
counter = 0
|
||||
# Yay! We found valid customization data... add it to the new plugin
|
||||
for k in userkeys:
|
||||
counter += 1
|
||||
self.plugin_keys['Converted Old Plugin Key - ' + str(counter)] = k
|
||||
msg = '<p><b>' + str(counter) + '</b> User key(s) configured from old plugin customization string'
|
||||
inf = info_dialog(None, _(PLUGIN_NAME + 'info_dlg'), _(msg), show=True)
|
||||
val = sc.pop(PLUGIN_NAME, None)
|
||||
if val is not None:
|
||||
config['plugin_customization'] = sc
|
||||
else:
|
||||
# The existing customization string was invalid and wouldn't have
|
||||
# worked anyway. Offer to save it as a text file and get rid of it.
|
||||
errmsg = '<p>Unknown Error converting user supplied-customization string'
|
||||
r = error_dialog(None, PLUGIN_NAME,
|
||||
_(errmsg), show=True, show_copy_button=False)
|
||||
self.saveOldCustomizationData(str(val))
|
||||
val = sc.pop(PLUGIN_NAME, None)
|
||||
if val is not None:
|
||||
config['plugin_customization'] = sc
|
||||
# If they don't want to convert the old string to keys then
|
||||
# offer to save the old string to a text file and delete the
|
||||
# the old customization string.
|
||||
else:
|
||||
self.saveOldCustomizationData(str(val))
|
||||
val = sc.pop(PLUGIN_NAME, None)
|
||||
if val is not None:
|
||||
config['plugin_customization'] = sc
|
||||
|
||||
# First time run since upgrading to new key storage method, or 0 keys configured.
|
||||
# Prompt to import pre-existing key files.
|
||||
if not prefs['configured']:
|
||||
title = 'Import existing key files?'
|
||||
msg = '<p>This plugin no longer uses *.b64 keyfiles stored in calibre\'s configuration '+ \
|
||||
'directory. Do you have any exsiting key files there (or anywhere) that you\'d '+ \
|
||||
'like to migrate into the new plugin preferences method?'
|
||||
if question_dialog(self, _(title), _(msg)):
|
||||
self.migrate_files()
|
||||
|
||||
# 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/">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)
|
||||
|
||||
keys_group_box = QGroupBox(_('Configured Ignoble Keys:'), 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(_('<p>Stored Ignoble keys that will be used for decryption'))
|
||||
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(_('Create new key'))
|
||||
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(_('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)
|
||||
|
||||
self._rename_key_button = QtGui.QToolButton(self)
|
||||
self._rename_key_button.setToolTip(_('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(_('Export highlighted key'))
|
||||
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(20)
|
||||
migrate_layout = QHBoxLayout()
|
||||
layout.addLayout(migrate_layout)
|
||||
self.migrate_btn = QPushButton(_('Import Existing Keyfiles'), self)
|
||||
self.migrate_btn.setToolTip(_('<p>Import *.b64 keyfiles (used by older versions of the plugin).'))
|
||||
self.migrate_btn.clicked.connect(self.migrate_wrapper)
|
||||
migrate_layout.setAlignment(Qt.AlignLeft)
|
||||
migrate_layout.addWidget(self.migrate_btn)
|
||||
|
||||
self.resize(self.sizeHint())
|
||||
|
||||
def populate_list(self):
|
||||
for key in self.plugin_keys.keys():
|
||||
self.listy.addItem(QListWidgetItem(key))
|
||||
|
||||
def add_key(self):
|
||||
d = AddKeyDialog(self)
|
||||
d.exec_()
|
||||
|
||||
if d.result() != d.Accepted:
|
||||
# New key generation cancelled.
|
||||
return
|
||||
self.plugin_keys[d.key_name] = generate_key(d.user_name, d.cc_number)
|
||||
|
||||
self.listy.clear()
|
||||
self.populate_list()
|
||||
|
||||
def rename_key(self):
|
||||
if not self.listy.currentItem():
|
||||
errmsg = '<p>No keyfile selected to export. Highlight a keyfile first.'
|
||||
r = error_dialog(None, PLUGIN_NAME,
|
||||
_(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, _('Are you sure?'), _('<p>'+
|
||||
'Do you really want to rename the Ignoble key named <strong>%s</strong> to <strong>%s</strong>?') % (keyname, d.key_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, _('Are you sure?'), _('<p>'+
|
||||
'Do you really want to delete the Ignoble key named <strong>%s</strong>?') % keyname,
|
||||
show_copy_button=False, default_yes=False):
|
||||
return
|
||||
del self.plugin_keys[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.
|
||||
file_path = os.path.join(config_dir, 'plugins', help_file_name)
|
||||
with open(file_path,'w') as f:
|
||||
f.write(self.help_file_data)
|
||||
return file_path
|
||||
url = 'file:///' + get_help_file_resource()
|
||||
open_url(QUrl(url))
|
||||
|
||||
def save_settings(self):
|
||||
prefs['keys'] = self.plugin_keys
|
||||
if prefs['keys']:
|
||||
prefs['configured'] = True
|
||||
else:
|
||||
prefs['configured'] = False
|
||||
|
||||
def migrate_files(self):
|
||||
dynamic[PLUGIN_NAME + 'config_dir'] = config_dir
|
||||
files = choose_files(self, PLUGIN_NAME + 'config_dir',
|
||||
_('Select Ignoble keyfiles to import'), [('Ignoble Keyfiles', ['b64'])], False)
|
||||
if files:
|
||||
counter = 0
|
||||
skipped = 0
|
||||
for filename in files:
|
||||
fpath = os.path.join(config_dir, filename)
|
||||
new_key_name = os.path.splitext(os.path.basename(filename))[0]
|
||||
match = False
|
||||
for key in self.plugin_keys.keys():
|
||||
if uStrCmp(new_key_name, key, True):
|
||||
skipped += 1
|
||||
msg = '<p>A key with the name <strong>' + new_key_name + '</strong> already exists! </p>' + \
|
||||
'<p>Skipping key file named <strong>' + filename + '</strong>.</p>' + \
|
||||
'<p>Either delete the existing key and re-migrate, or ' + \
|
||||
'create that key manually with a different name.'
|
||||
inf = info_dialog(None, _(PLUGIN_NAME + 'info_dlg'),
|
||||
_(msg), show=True)
|
||||
match = True
|
||||
break
|
||||
if not match:
|
||||
with open(fpath, 'rb') as f:
|
||||
counter += 1
|
||||
self.plugin_keys[unicode(new_key_name)] = f.read()
|
||||
|
||||
msg = '<p>Done migrating <strong>' + str(counter) + '</strong> ' + \
|
||||
'key files...</p><p>Skipped <strong>' + str(skipped) + '</strong> key files.'
|
||||
inf = info_dialog(None, _(PLUGIN_NAME + 'info_dlg'),
|
||||
_(msg), show=True)
|
||||
return 1
|
||||
return 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 = '<p>No keyfile selected to export. Highlight a keyfile first.'
|
||||
r = error_dialog(None, PLUGIN_NAME,
|
||||
_(errmsg), show=True, show_copy_button=False)
|
||||
return
|
||||
filter = QString('Ignoble Key Files (*.b64)')
|
||||
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'), keyname + '.b64')
|
||||
else:
|
||||
defaultname = os.path.join(os.path.expanduser('~'), keyname + '.b64')
|
||||
filename = unicode(QtGui.QFileDialog.getSaveFileName(self, "Save Ignoble Key File as...", defaultname,
|
||||
"Ignoble Key Files (*.b64)", filter))
|
||||
if filename:
|
||||
dynamic[PLUGIN_NAME + 'save_dir'] = os.path.split(filename)[0]
|
||||
fname = open(filename, 'w')
|
||||
fname.write(self.plugin_keys[keyname])
|
||||
fname.close()
|
||||
|
||||
def saveOldCustomizationData(self, strdata):
|
||||
filter = QString('Text files (*.txt)')
|
||||
default_basefilename = PLUGIN_NAME + ' old customization data.txt'
|
||||
defaultname = os.path.join(os.path.expanduser('~'), default_basefilename)
|
||||
filename = unicode(QtGui.QFileDialog.getSaveFileName(self, "Save old plugin style customization data as...", defaultname,
|
||||
"Text Files (*.txt)", filter))
|
||||
if filename:
|
||||
fname = open(filename, 'w')
|
||||
fname.write(strdata)
|
||||
fname.close()
|
@ -1,160 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
from __future__ import with_statement
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
from PyQt4.Qt import (Qt, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit,
|
||||
QGroupBox, QDialog, QDialogButtonBox)
|
||||
from calibre.gui2 import error_dialog
|
||||
|
||||
from calibre_plugins.ignobleepub.__init__ import PLUGIN_NAME, PLUGIN_VERSION
|
||||
from calibre_plugins.ignobleepub.utilities import uStrCmp
|
||||
|
||||
class AddKeyDialog(QDialog):
|
||||
def __init__(self, parent=None,):
|
||||
QDialog.__init__(self, parent)
|
||||
self.parent = parent
|
||||
self.setWindowTitle('Create New Ignoble Key')
|
||||
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)
|
||||
|
||||
key_group = QHBoxLayout()
|
||||
data_group_box_layout.addLayout(key_group)
|
||||
key_group.addWidget(QLabel('Unique Key Name:', self))
|
||||
self.key_ledit = QLineEdit('', self)
|
||||
self.key_ledit.setToolTip(_('<p>Enter an identifying name for this new Ignoble key.</p>' +
|
||||
'<p>It 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('Your Name:', self))
|
||||
self.name_ledit = QLineEdit('', self)
|
||||
self.name_ledit.setToolTip(_('<p>Enter your name as it appears in your B&N ' +
|
||||
'account and/or on your credit card.</p>' +
|
||||
'<p>It will only be used to generate this ' +
|
||||
'one-time key and won\'t be stored anywhere ' +
|
||||
'in calibre or on your computer.</p>' +
|
||||
'<p>(ex: Jonathan Smith)'))
|
||||
name_group.addWidget(self.name_ledit)
|
||||
name_disclaimer_label = QLabel(_('Will not be stored/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('Credit Card#:', self))
|
||||
self.cc_ledit = QLineEdit('', self)
|
||||
self.cc_ledit.setToolTip(_('<p>Enter the full credit card number on record ' +
|
||||
'in your B&N account.</p>' +
|
||||
'<p>No spaces or dashes... just the numbers. ' +
|
||||
'This CC# 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 stored/saved in configuration data:'), self)
|
||||
ccn_disclaimer_label.setAlignment(Qt.AlignHCenter)
|
||||
data_group_box_layout.addWidget(ccn_disclaimer_label)
|
||||
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.parent.sizeHint())
|
||||
|
||||
def accept(self):
|
||||
if (self.key_ledit.text().isEmpty() or self.name_ledit.text().isEmpty()
|
||||
or self.cc_ledit.text().isEmpty()):
|
||||
errmsg = '<p>All fields are required!'
|
||||
return error_dialog(None, PLUGIN_NAME,
|
||||
_(errmsg), show=True, show_copy_button=False)
|
||||
if (unicode(self.key_ledit.text()).isspace() or unicode(self.name_ledit.text()).isspace()
|
||||
or unicode(self.cc_ledit.text()).isspace()):
|
||||
errmsg = '<p>All fields are required!'
|
||||
return error_dialog(None, PLUGIN_NAME,
|
||||
_(errmsg), show=True, show_copy_button=False)
|
||||
if not unicode(self.cc_ledit.text()).isdigit():
|
||||
errmsg = '<p>Numbers only in the credit card number field!'
|
||||
return error_dialog(None, PLUGIN_NAME,
|
||||
_(errmsg), show=True, show_copy_button=False)
|
||||
if len(self.key_ledit.text()) < 4:
|
||||
errmsg = '<p>Key name must be at <i>least</i> 4 characters long!'
|
||||
return error_dialog(None, PLUGIN_NAME,
|
||||
_(errmsg), show=True, show_copy_button=False)
|
||||
for k in self.parent.plugin_keys.keys():
|
||||
if uStrCmp(self.key_ledit.text(), k, True):
|
||||
errmsg = '<p>The key name <strong>%s</strong> is already being used.' % self.key_ledit.text()
|
||||
return error_dialog(None, PLUGIN_NAME,
|
||||
_(errmsg), show=True, show_copy_button=False)
|
||||
QDialog.accept(self)
|
||||
|
||||
@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('-','')
|
||||
@property
|
||||
def key_name(self):
|
||||
return unicode(self.key_ledit.text().toUtf8(), 'utf8')
|
||||
|
||||
class RenameKeyDialog(QDialog):
|
||||
def __init__(self, parent=None,):
|
||||
QDialog.__init__(self, parent)
|
||||
self.parent = parent
|
||||
self.setWindowTitle('Rename Ignoble Key')
|
||||
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('Key Name:', self))
|
||||
self.key_ledit = QLineEdit(self.parent.listy.currentItem().text(), self)
|
||||
self.key_ledit.setToolTip(_('<p>Enter a new name for this existing Ignoble key.'))
|
||||
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)
|
||||
|
||||
def accept(self):
|
||||
if self.key_ledit.text().isEmpty() or unicode(self.key_ledit.text()).isspace():
|
||||
errmsg = '<p>Key name field cannot be empty!'
|
||||
return error_dialog(None, PLUGIN_NAME,
|
||||
_(errmsg), show=True, show_copy_button=False)
|
||||
if len(self.key_ledit.text()) < 4:
|
||||
errmsg = '<p>Key name must be at <i>least</i> 4 characters long!'
|
||||
return error_dialog(None, PLUGIN_NAME,
|
||||
_(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 = '<p>The key name <strong>%s</strong> is already being used.' % self.key_ledit.text()
|
||||
return error_dialog(None, PLUGIN_NAME,
|
||||
_(errmsg), show=True, show_copy_button=False)
|
||||
QDialog.accept(self)
|
||||
|
||||
@property
|
||||
def key_name(self):
|
||||
return unicode(self.key_ledit.text().toUtf8(), 'utf8')
|
@ -1,98 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
|
||||
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
||||
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
|
||||
<head>
|
||||
<title>Ignoble Epub DeDRM Plugin Configuration</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<h1>Ignoble Epub DeDRM Plugin</h1>
|
||||
<h3>(version 0.2.6)</h3>
|
||||
<h3> For additional help read the <a href="http://apprenticealf.wordpress.com/2011/01/17/frequently-asked-questions-about-the-drm-removal-tools/" target="_blank">FAQ</a> on <a href="http://apprenticealf.wordpress.com" target="_blank">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/" target="_blank">first post</a>.</h3>
|
||||
|
||||
<p>All credit given to I ♥ Cabbages for the original standalone scripts (I had the much easier job of converting them to a calibre plugin).</p>
|
||||
|
||||
<p>This plugin is meant to decrypt Barnes & Noble ePubs that are protected with Adobe's Adept encryption. It is meant to function without having to install any dependencies... other than having calibre installed, of course. It will still work if you have Python and PyCrypto already installed, but they aren't necessary.</p>
|
||||
|
||||
<p>This help file is always available from within the plugin's customization dialog in calibre (when installed, of course). The "Plugin Help" link can be found in the upper-right portion of the customization dialog.</p>
|
||||
|
||||
<h3>Installation:</h3>
|
||||
|
||||
<p>Go to calibre's Preferences page. Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior". Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (ignobleepub_v02.3_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog. <b><u>Now restart calibre</u></b>.</p>
|
||||
|
||||
|
||||
<h3>Configuration:</h3>
|
||||
|
||||
<p>Upon first installing the plugin (or upgrading from a version earlier than 0.2.0), the plugin will be unconfigured. Until you create at least one B&N key—or migrate your existing key(s)/data from an earlier version of the plugin—the plugin will not function. When unconfigured (no saved keys)... an error message will occur whenever ePubs are imported to calibre. To eliminate the error message, open the plugin's customization dialog and create/import/migrate a key (or disable/uninstall the plugin). You can get to the plugin's customization dialog by opening calibre's Preferences dialog, and clicking Plugins (under the Advanced section). Once in the Plugin Preferences, expand the "File type plugins" section and look for the "Ignoble Epub DeDRM" plugin. Highlight that plugin and click the "Customize plugin" button.</p>
|
||||
|
||||
<p>If you are upgrading from an earlier version of this plugin and have provided your name(s) and credit card number(s) as part of the old plugin's customization string, you will be prompted to migrate this data to the plugin's new, more secure, key storage method when you open the customization dialog for the first time. If you choose NOT to migrate that data, you will be prompted to save that data as a text file in a location of your choosing. Either way, this plugin will no longer be storing names and credit card numbers in plain sight (or anywhere for that matter) on your computer or in calibre. If you don't choose to migrate OR save the data, that data will be lost. You have been warned!!</p>
|
||||
|
||||
<p>Upon configuring for the first time, you may also be asked if you wish to import your existing *.b64 keyfiles (if you use them) to the plugin's new key storage method. The new plugin no longer looks for keyfiles in calibre's configuration directory, so it's highly recommended that you import any existing keyfiles when prompted ... but you <i>always</i> have the ability to import existing keyfiles anytime you might need/want to.</p>
|
||||
|
||||
<p>If you have upgraded from an earlier version of the plugin, the above instructions may be all you need to do to get the new plugin up and running. Continue reading for new-key generation and existing-key management instructions.</p>
|
||||
|
||||
<h4 style="margin-left: 1.0em;"><u>Creating New Keys:</u></h4>
|
||||
|
||||
<p style="margin-left: 1.0em">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 style="margin-left: 2.0em;">
|
||||
<li><b>Unique Key Name:</b> this is a unique name you choose to help you identify the key after it's created. 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.</i>
|
||||
<li style="margin-top: 0.5em;"><b>Your Name:</b> Your name as set in your Barnes & Noble account, My Account page, directly under PERSONAL INFORMATION. It is usually just your first name and last name separated by a space. 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.</i>
|
||||
<li style="margin-top: 0.5em;"><b>Credit Card#:</b> this is the default credit card number that was on file with Barnes & Noble at the time of download of the ebook to be de-DRMed. Nothing fancy here; no dashes or spaces ... just the 16 (15 for American Express) digits. Again... 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.</i>
|
||||
</ul>
|
||||
|
||||
<p style="margin-left: 1.0em;">Click the 'OK" button to create and store the generated key. Or Cancel if you didn't want to create a key.</p>
|
||||
|
||||
<h4 style="margin-left: 1.0em;"><u>Deleting Keys:</u></h4>
|
||||
|
||||
<p style="margin-left: 1.0em;">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>
|
||||
|
||||
<h4 style="margin-left: 1.0em;"><u>Exporting Keys:</u></h4>
|
||||
|
||||
<p style="margin-left: 1.0em;">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 (*.b64). 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>
|
||||
|
||||
<h4 style="margin-left: 1.0em;"><u>Importing Existing Keyfiles:</u></h4>
|
||||
|
||||
<p style="margin-left: 1.0em;">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 keyfiles. Used for migrating keyfiles from older versions of the plugin (or keys generated with the original I <3 Cabbages script), or moving keyfiles from computer to computer, or restoring a backup. Some very basic validation is done to try to avoid overwriting already configured keys with incoming, imported keyfiles with the same base file name, but I'm sure that could be broken if someone tried hard. Just take care when importing.</p>
|
||||
|
||||
<p>Once done creating/importing/exporting/deleting decryption keys; click "OK" to exit the customization dialogue (the cancel button will actually work the same way here ... at this point all data/changes are committed already, so take your pick).</p>
|
||||
|
||||
<h3>Troubleshooting:</h3>
|
||||
|
||||
<p style="margin-top: 0.5em;">If you find that it's not working for you (imported Barnes & Noble epubs still have DRM), you can save a lot of time and trouble by trying to add the epub to Calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might as well get used to it. ;)</p>
|
||||
|
||||
<p>Open a command prompt (terminal) and change to the directory where the ebook you're trying to import resides. Then type the command "calibredb add your_ebook.epub" **. Don't type the quotes and obviously change the 'your_ebook.epub' to whatever the filename of your book is. Copy the resulting output and paste it into any online help request you make.</p>
|
||||
|
||||
<p>Another way to debug (perhaps easier if you're not all that comfortable with command-line stuff) is to launch calibre in debug mode. Open a command prompt (terminal) and type "calibre-debug -g" (again without the quotes). Calibre will launch, and you can can add the problem book(s) using the normal gui method. The debug info will be output to the original command prompt (terminal window). Copy the resulting output and paste it into any online help request you make.</p>
|
||||
<p> </p>
|
||||
<p>** Note: 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 see the option to install the command line tools.</p>
|
||||
|
||||
<p> </p>
|
||||
<h4>Revision history:</h4>
|
||||
<pre>
|
||||
0.1.0 - Initial release
|
||||
0.1.1 - Allow Windows users to make use of openssl if they have it installed.
|
||||
- Incorporated SomeUpdates zipfix routine.
|
||||
0.1.2 - bug fix for non-ascii file names in encryption.xml
|
||||
0.1.3 - Try PyCrypto on Windows first
|
||||
0.1.4 - update zipfix to deal with mimetype not in correct place
|
||||
0.1.5 - update zipfix to deal with completely missing mimetype files
|
||||
0.1.6 - update to the new calibre plugin interface
|
||||
0.1.7 - Fix for potential problem with PyCrypto
|
||||
0.1.8 - an updated/modified zipfix.py and included zipfilerugged.py
|
||||
0.2.0 - Completely overhauled plugin configuration dialog and key management/storage
|
||||
0.2.1 - an updated/modified zipfix.py and included zipfilerugged.py
|
||||
0.2.2 - added in potential fixes from 0.1.7 that had been missed.
|
||||
0.2.3 - fixed possible output/unicode problem
|
||||
0.2.4 - ditched nearly hopeless caselessStrCmp method in favor of uStrCmp.
|
||||
- added ability to rename existing keys.
|
||||
0.2.5 - Major code change to use unaltered ignobleepub.py 3.6 and
|
||||
- ignoblekeygen 2.4 and later.
|
||||
0.2.6 - Modified to alleviate the issue with having both the ignoble and inept epub plugins installed/enabled
|
||||
</pre>
|
||||
</body>
|
||||
|
||||
</html>
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,205 +0,0 @@
|
||||
#!/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 for the original standalone scripts.
|
||||
# I had the much easier job of converting them to a calibre plugin.
|
||||
#
|
||||
# This plugin is meant to decrypt Adobe Digital Edition Epubs that are protected
|
||||
# with Adobe's Adept encryption. It is meant to function without having to install
|
||||
# any dependencies... other than having calibre installed, of course. It will still
|
||||
# work if you have Python and PyCrypto already installed, but they aren't necessary.
|
||||
#
|
||||
# Configuration:
|
||||
# When first run, the plugin will attempt to find your Adobe Digital Editions installation
|
||||
# (on Windows and Mac OS's). If successful, it will create one or more
|
||||
# 'calibre-adeptkey<n>.der' files and save them in calibre's configuration directory.
|
||||
# It will use those files on subsequent runs. If there is already a 'calibre-adeptkey*.der'
|
||||
# file in the directory, the plugin won't attempt to find the ADE installation.
|
||||
# So if you have ADE installed on the same machine as calibre you are ready to go.
|
||||
#
|
||||
# If you already have keyfiles generated with i♥cabbages' ineptkey.pyw script,
|
||||
# you can put those keyfiles in Calibre's configuration directory. The easiest
|
||||
# way to find the correct directory is to go to Calibre's Preferences page... click
|
||||
# on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre
|
||||
# configuration directory' button. Copy your keyfiles in there. Just make sure that
|
||||
# they have different names and are saved with the '.der' extension (like the ineptkey
|
||||
# script produces). This directory isn't touched when upgrading Calibre, so it's quite
|
||||
# safe to leave them there.
|
||||
#
|
||||
# Since there is no Linux version of Adobe Digital Editions, Linux users will have to
|
||||
# obtain a keyfile through other methods and put the file in Calibre's configuration directory.
|
||||
#
|
||||
# All keyfiles with a '.der' extension found in Calibre's configuration directory will
|
||||
# be used to attempt to decrypt a book.
|
||||
#
|
||||
# ** NOTE ** There is no plugin customization data for the Inept Epub DeDRM plugin.
|
||||
#
|
||||
# Revision history:
|
||||
# 0.1 - Initial release
|
||||
# 0.1.1 - Allow Windows users to make use of openssl if they have it installed.
|
||||
# - Incorporated SomeUpdates zipfix routine.
|
||||
# 0.1.2 - Removed Carbon dependency for Mac users. Fixes an issue that was a
|
||||
# result of Calibre changing to python 2.7.
|
||||
# 0.1.3 - bug fix for epubs with non-ascii chars in file names
|
||||
# 0.1.4 - default to try PyCrypto first on Windows
|
||||
# 0.1.5 - update zipfix to handle out of position mimetypes
|
||||
# 0.1.6 - update zipfix to handle completely missing mimetype files
|
||||
# 0.1.7 - update to new calibre plugin interface
|
||||
# 0.1.8 - Fix for potential problem with PyCrypto
|
||||
# 0.1.9 - Fix for potential problem with ADE keys and fix possible output/unicode problem
|
||||
# 0.2.0 - Major code change to use unaltered ineptepub.py file 5.8 or later.
|
||||
# 0.2.1 - Tweaked to eliminate issue with both ignoble and inept calibre plugins installed/enabled at once
|
||||
|
||||
|
||||
PLUGIN_NAME = u"Inept Epub DeDRM"
|
||||
PLUGIN_VERSION_TUPLE = (0, 2, 1)
|
||||
PLUGIN_VERSION = u'.'.join([str(x) for x in PLUGIN_VERSION_TUPLE])
|
||||
|
||||
import sys, os, re
|
||||
|
||||
class ADEPTError(Exception):
|
||||
pass
|
||||
|
||||
from calibre.customize import FileTypePlugin
|
||||
from calibre.constants import iswindows, isosx
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
|
||||
class IneptDeDRM(FileTypePlugin):
|
||||
name = PLUGIN_NAME
|
||||
description = u"Removes DRM from secure Adobe epub files. Credit given to i♥cabbages for the original stand-alone scripts."
|
||||
supported_platforms = ['linux', 'osx', 'windows']
|
||||
author = u"DiapDealer, Apprentice Alf 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'])
|
||||
on_import = True
|
||||
priority = 100
|
||||
|
||||
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))
|
||||
|
||||
# Create a TemporaryPersistent file to work with.
|
||||
# Check original epub archive for zip errors.
|
||||
from calibre_plugins.ineptepub import 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)
|
||||
return
|
||||
|
||||
#check the book
|
||||
from calibre_plugins.ineptepub import ineptepub
|
||||
if not ineptepub.adeptBook(inf.name):
|
||||
raise ADEPTError(u"{0} v{1}: {2} is not a secure Adobe Adept ePub.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
|
||||
|
||||
# Load any keyfiles (*.der) included Calibre's config directory.
|
||||
userkeys = []
|
||||
# Find Calibre's configuration directory.
|
||||
# self.plugin_path is passed in unicode because we defined our name in unicode
|
||||
confpath = os.path.split(os.path.split(self.plugin_path)[0])[0]
|
||||
print u"{0} v{1}: Calibre configuration directory = {2}".format(PLUGIN_NAME, PLUGIN_VERSION, confpath)
|
||||
files = os.listdir(confpath)
|
||||
filefilter = re.compile(u"\.der$", re.IGNORECASE)
|
||||
files = filter(filefilter.search, files)
|
||||
foundDefault = False
|
||||
if files:
|
||||
try:
|
||||
for filename in files:
|
||||
if filename[:16] == u"calibre-adeptkey":
|
||||
foundDefault = True
|
||||
fpath = os.path.join(confpath, filename)
|
||||
with open(fpath, 'rb') as f:
|
||||
userkeys.append([f.read(), filename])
|
||||
print u"{0} v{1}: Keyfile {2} found in config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, filename)
|
||||
except IOError:
|
||||
print u"{0} v{1}: Error reading keyfiles from config directory.".format(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
pass
|
||||
|
||||
if not foundDefault:
|
||||
# Try to find key from ADE install and save the key in
|
||||
# Calibre's configuration directory for future use.
|
||||
if iswindows or isosx:
|
||||
#ignore annoying future warning from key generation
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore', category=FutureWarning)
|
||||
|
||||
# ADE key retrieval script included in respective OS folder.
|
||||
from calibre_plugins.ineptepub.ineptkey import retrieve_keys
|
||||
try:
|
||||
keys = retrieve_keys()
|
||||
for i,key in enumerate(keys):
|
||||
keyname = u"calibre-adeptkey{0:d}.der".format(i)
|
||||
userkeys.append([key,keyname])
|
||||
keypath = os.path.join(confpath, keyname)
|
||||
open(keypath, 'wb').write(key)
|
||||
print u"{0} v{1}: Created keyfile {2} from ADE install.".format(PLUGIN_NAME, PLUGIN_VERSION, keyname)
|
||||
except:
|
||||
print u"{0} v{1}: Couldn\'t Retrieve key from ADE install.".format(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
pass
|
||||
|
||||
if not userkeys:
|
||||
# No user keys found... bail out.
|
||||
raise ADEPTError(u"{0} v{1}: No keys found. Check keyfile(s)/ADE install".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
return
|
||||
|
||||
# Attempt to decrypt epub with each encryption key found.
|
||||
for userkeyinfo in userkeys:
|
||||
userkey,keyname = userkeyinfo
|
||||
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.
|
||||
result = ineptepub.decryptBook(userkey, inf.name, of.name)
|
||||
|
||||
of.close()
|
||||
|
||||
# Decryption was successful return the modified PersistentTemporary
|
||||
# file to Calibre's import process.
|
||||
if result == 0:
|
||||
print u"{0} v{1}: Encryption successfully removed.".format(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
return of.name
|
||||
break
|
||||
|
||||
print u"{0} v{1}: Encryption key incorrect.".format(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
|
||||
# Something went wrong with decryption.
|
||||
# Import the original unmolested epub.
|
||||
raise ADEPTError(u"{0} v{1}: Ultimately failed to decrypt".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
return
|
||||
|
Binary file not shown.
@ -1,196 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# PLEASE DO NOT PIRATE EBOOKS!
|
||||
|
||||
# We want all authors and publishers, and eBook stores to live
|
||||
# long and prosperous lives but at the same time we just want to
|
||||
# be able to read OUR books on whatever device we want and to keep
|
||||
# readable for a long, long time
|
||||
|
||||
# Requires Calibre version 0.7.55 or higher.
|
||||
#
|
||||
# All credit given to i♥cabbages for the original standalone scripts.
|
||||
# I had the much easier job of converting them to a Calibre plugin.
|
||||
#
|
||||
# This plugin is meant to decrypt Adobe Digital Edition PDFs that are protected
|
||||
# with Adobe's Adept encryption. It is meant to function without having to install
|
||||
# any dependencies... other than having Calibre installed, of course. It will still
|
||||
# work if you have Python and PyCrypto already installed, but they aren't necessary.
|
||||
#
|
||||
# Configuration:
|
||||
# When first run, the plugin will attempt to find your Adobe Digital Editions installation
|
||||
# (on Windows and Mac OS's). If successful, it will create one or more
|
||||
# 'calibre-adeptkey<n>.der' files and save them in calibre's configuration directory.
|
||||
# It will use those files on subsequent runs. If there is already a 'calibre-adeptkey*.der'
|
||||
# file in the directory, the plugin won't attempt to find the ADE installation.
|
||||
# So if you have ADE installed on the same machine as calibre you are ready to go.
|
||||
#
|
||||
# If you already have keyfiles generated with i♥cabbages' ineptkey.pyw script,
|
||||
# you can put those keyfiles in Calibre's configuration directory. The easiest
|
||||
# way to find the correct directory is to go to Calibre's Preferences page... click
|
||||
# on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre
|
||||
# configuration directory' button. Paste your keyfiles in there. Just make sure that
|
||||
# they have different names and are saved with the '.der' extension (like the ineptkey
|
||||
# script produces). This directory isn't touched when upgrading Calibre, so it's quite
|
||||
# safe to leave them there.
|
||||
#
|
||||
# Since there is no Linux version of Adobe Digital Editions, Linux users will have to
|
||||
# obtain a keyfile through other methods and put the file in Calibre's configuration directory.
|
||||
#
|
||||
# All keyfiles with a '.der' extension found in Calibre's configuration directory will
|
||||
# be used to attempt to decrypt a book.
|
||||
#
|
||||
# ** NOTE ** There is no plugin customization data for the Inept PDF DeDRM plugin.
|
||||
#
|
||||
# Revision history:
|
||||
# 0.1 - Initial release
|
||||
# 0.1.1 - back port ineptpdf 8.4.X support for increased number of encryption methods
|
||||
# 0.1.2 - back port ineptpdf 8.4.X bug fixes
|
||||
# 0.1.3 - add in fix for improper rejection of session bookkeys with len(bookkey) = length + 1
|
||||
# 0.1.4 - update to the new calibre plugin interface
|
||||
# 0.1.5 - synced to ineptpdf 7.11
|
||||
# 0.1.6 - Fix for potential problem with PyCrypto
|
||||
# 0.1.7 - Fix for potential problem with ADE keys and fix possible output/unicode problem
|
||||
# 0.1.8 - Fix for code copying error
|
||||
# 0.1.9 - Major code change to use unaltered ineptpdf.py
|
||||
# 0.2.0 - Fix erroneous dependency on ineptepub plugin
|
||||
|
||||
"""
|
||||
Decrypts Adobe ADEPT-encrypted PDF files.
|
||||
"""
|
||||
|
||||
PLUGIN_NAME = u"Inept PDF DeDRM"
|
||||
PLUGIN_VERSION_TUPLE = (0, 2, 0)
|
||||
PLUGIN_VERSION = u'.'.join([str(x) for x in PLUGIN_VERSION_TUPLE])
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
|
||||
class ADEPTError(Exception):
|
||||
pass
|
||||
|
||||
from calibre.customize import FileTypePlugin
|
||||
from calibre.constants import iswindows, isosx
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
|
||||
class ADEPTError(Exception):
|
||||
pass
|
||||
|
||||
class IneptPDFDeDRM(FileTypePlugin):
|
||||
name = PLUGIN_NAME
|
||||
description = u"Removes DRM from secure Adobe pdf files. Credit given to i♥cabbages for the original stand-alone scripts."
|
||||
supported_platforms = ['linux', 'osx', 'windows']
|
||||
author = u"DiapDealer, Apprentice Alf and i♥cabbages"
|
||||
version = PLUGIN_VERSION_TUPLE
|
||||
minimum_calibre_version = (0, 7, 55) # for the new plugin interface
|
||||
file_types = set(['pdf'])
|
||||
on_import = True
|
||||
priority = 100
|
||||
|
||||
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))
|
||||
|
||||
# Load any keyfiles (*.der) included Calibre's config directory.
|
||||
userkeys = []
|
||||
# Find Calibre's configuration directory.
|
||||
# self.plugin_path is passed in unicode because we defined our name in unicode
|
||||
confpath = os.path.split(os.path.split(self.plugin_path)[0])[0]
|
||||
print u"{0} v{1}: Calibre configuration directory = {2}".format(PLUGIN_NAME, PLUGIN_VERSION, confpath)
|
||||
files = os.listdir(confpath)
|
||||
filefilter = re.compile(u"\.der$", re.IGNORECASE)
|
||||
files = filter(filefilter.search, files)
|
||||
foundDefault = False
|
||||
if files:
|
||||
try:
|
||||
for filename in files:
|
||||
if filename[:16] == u"calibre-adeptkey":
|
||||
foundDefault = True
|
||||
fpath = os.path.join(confpath, filename)
|
||||
with open(fpath, 'rb') as f:
|
||||
userkeys.append([f.read(), filename])
|
||||
print u"{0} v{1}: Keyfile {2} found in config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, filename)
|
||||
except IOError:
|
||||
print u"{0} v{1}: Error reading keyfiles from config directory.".format(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
pass
|
||||
|
||||
if not foundDefault:
|
||||
# Try to find key from ADE install and save the key in
|
||||
# Calibre's configuration directory for future use.
|
||||
if iswindows or isosx:
|
||||
#ignore annoying future warning from key generation
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore', category=FutureWarning)
|
||||
|
||||
# ADE key retrieval script included in respective OS folder.
|
||||
from calibre_plugins.ineptpdf.ineptkey import retrieve_keys
|
||||
try:
|
||||
keys = retrieve_keys()
|
||||
for i,key in enumerate(keys):
|
||||
keyname = u"calibre-adeptkey{0:d}.der".format(i)
|
||||
userkeys.append([key,keyname])
|
||||
keypath = os.path.join(confpath, keyname)
|
||||
open(keypath, 'wb').write(key)
|
||||
print u"{0} v{1}: Created keyfile {2} from ADE install.".format(PLUGIN_NAME, PLUGIN_VERSION, keyname)
|
||||
except:
|
||||
print u"{0} v{1}: Couldn\'t Retrieve key from ADE install.".format(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
pass
|
||||
|
||||
if not userkeys:
|
||||
# No user keys found... bail out.
|
||||
raise ADEPTError(u"{0} v{1}: No keys found. Check keyfile(s)/ADE install".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
return
|
||||
|
||||
# Attempt to decrypt pdf with each encryption key found.
|
||||
from calibre_plugins.ineptpdf import ineptpdf
|
||||
for userkeyinfo in userkeys:
|
||||
print u"{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, userkeyinfo[1])
|
||||
# Create a TemporaryPersistent file to work with.
|
||||
of = self.temporary_file('.pdf')
|
||||
|
||||
# Give the user keyfile, ebook and TemporaryPersistent file to the decryptBook function.
|
||||
result = ineptpdf.decryptBook(userkeyinfo[0], path_to_ebook, of.name)
|
||||
|
||||
# Decryption was successful return the modified PersistentTemporary
|
||||
# file to Calibre's import process.
|
||||
if result == 0:
|
||||
print u"{0} v{1}: Encryption successfully removed.".format(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
of.close()
|
||||
return of.name
|
||||
break
|
||||
|
||||
print u"{0} v{1}: Encryption key incorrect.".format(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
of.close()
|
||||
|
||||
# Something went wrong with decryption.
|
||||
raise ADEPTError(u"{0} v{1}: Ultimately failed to decrypt".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
return
|
Binary file not shown.
@ -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
|
@ -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
|
Binary file not shown.
Binary file not shown.
@ -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()
|
@ -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)
|
||||
|
@ -1,4 +1,4 @@
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
|
||||
{\fonttbl}
|
||||
{\colortbl;\red255\green255\blue255;}
|
||||
}
|
@ -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)
|
||||
|
||||
|
@ -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())
|
@ -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
|
@ -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
|
File diff suppressed because it is too large
Load Diff
@ -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)
|
@ -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 <bellman@lysator.liu.se>"
|
||||
# available under GPL version 3 or Later
|
||||
|
||||
# create an asynchronous subprocess whose output can be collected in
|
||||
# a non-blocking manner
|
||||
|
||||
# What a mess! Have to use threads just to get non-blocking io
|
||||
# in a cross-platform manner
|
||||
|
||||
# luckily all thread use is hidden within this class
|
||||
|
||||
class Process(object):
|
||||
def __init__(self, *params, **kwparams):
|
||||
if len(params) <= 3:
|
||||
kwparams.setdefault('stdin', subprocess.PIPE)
|
||||
if len(params) <= 4:
|
||||
kwparams.setdefault('stdout', subprocess.PIPE)
|
||||
if len(params) <= 5:
|
||||
kwparams.setdefault('stderr', subprocess.PIPE)
|
||||
self.__pending_input = []
|
||||
self.__collected_outdata = []
|
||||
self.__collected_errdata = []
|
||||
self.__exitstatus = None
|
||||
self.__lock = threading.Lock()
|
||||
self.__inputsem = threading.Semaphore(0)
|
||||
self.__quit = False
|
||||
|
||||
self.__process = subprocess.Popen(*params, **kwparams)
|
||||
|
||||
if self.__process.stdin:
|
||||
self.__stdin_thread = threading.Thread(
|
||||
name="stdin-thread",
|
||||
target=self.__feeder, args=(self.__pending_input,
|
||||
self.__process.stdin))
|
||||
self.__stdin_thread.setDaemon(True)
|
||||
self.__stdin_thread.start()
|
||||
|
||||
if self.__process.stdout:
|
||||
self.__stdout_thread = threading.Thread(
|
||||
name="stdout-thread",
|
||||
target=self.__reader, args=(self.__collected_outdata,
|
||||
self.__process.stdout))
|
||||
self.__stdout_thread.setDaemon(True)
|
||||
self.__stdout_thread.start()
|
||||
|
||||
if self.__process.stderr:
|
||||
self.__stderr_thread = threading.Thread(
|
||||
name="stderr-thread",
|
||||
target=self.__reader, args=(self.__collected_errdata,
|
||||
self.__process.stderr))
|
||||
self.__stderr_thread.setDaemon(True)
|
||||
self.__stderr_thread.start()
|
||||
|
||||
def pid(self):
|
||||
return self.__process.pid
|
||||
|
||||
def kill(self, signal):
|
||||
self.__process.send_signal(signal)
|
||||
|
||||
# check on subprocess (pass in 'nowait') to act like poll
|
||||
def wait(self, flag):
|
||||
if flag.lower() == 'nowait':
|
||||
rc = self.__process.poll()
|
||||
else:
|
||||
rc = self.__process.wait()
|
||||
if rc != None:
|
||||
if self.__process.stdin:
|
||||
self.closeinput()
|
||||
if self.__process.stdout:
|
||||
self.__stdout_thread.join()
|
||||
if self.__process.stderr:
|
||||
self.__stderr_thread.join()
|
||||
return self.__process.returncode
|
||||
|
||||
def terminate(self):
|
||||
if self.__process.stdin:
|
||||
self.closeinput()
|
||||
self.__process.terminate()
|
||||
|
||||
# thread gets data from subprocess stdout
|
||||
def __reader(self, collector, source):
|
||||
while True:
|
||||
data = os.read(source.fileno(), 65536)
|
||||
self.__lock.acquire()
|
||||
collector.append(data)
|
||||
self.__lock.release()
|
||||
if data == "":
|
||||
source.close()
|
||||
break
|
||||
return
|
||||
|
||||
# thread feeds data to subprocess stdin
|
||||
def __feeder(self, pending, drain):
|
||||
while True:
|
||||
self.__inputsem.acquire()
|
||||
self.__lock.acquire()
|
||||
if not pending and self.__quit:
|
||||
drain.close()
|
||||
self.__lock.release()
|
||||
break
|
||||
data = pending.pop(0)
|
||||
self.__lock.release()
|
||||
drain.write(data)
|
||||
|
||||
# non-blocking read of data from subprocess stdout
|
||||
def read(self):
|
||||
self.__lock.acquire()
|
||||
outdata = "".join(self.__collected_outdata)
|
||||
del self.__collected_outdata[:]
|
||||
self.__lock.release()
|
||||
return outdata
|
||||
|
||||
# non-blocking read of data from subprocess stderr
|
||||
def readerr(self):
|
||||
self.__lock.acquire()
|
||||
errdata = "".join(self.__collected_errdata)
|
||||
del self.__collected_errdata[:]
|
||||
self.__lock.release()
|
||||
return errdata
|
||||
|
||||
# non-blocking write to stdin of subprocess
|
||||
def write(self, data):
|
||||
if self.__process.stdin is None:
|
||||
raise ValueError("Writing to process with stdin not a pipe")
|
||||
self.__lock.acquire()
|
||||
self.__pending_input.append(data)
|
||||
self.__inputsem.release()
|
||||
self.__lock.release()
|
||||
|
||||
# close stdinput of subprocess
|
||||
def closeinput(self):
|
||||
self.__lock.acquire()
|
||||
self.__quit = True
|
||||
self.__inputsem.release()
|
||||
self.__lock.release()
|
@ -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()
|
@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
|
||||
# to work around tk_chooseDirectory not properly returning unicode paths on Windows
|
||||
# need to use a dialog that can be hacked up to actually return full unicode paths
|
||||
# originally based on AskFolder from EasyDialogs for Windows but modified to fix it
|
||||
# to actually use unicode for path
|
||||
|
||||
# The original license for EasyDialogs is as follows
|
||||
#
|
||||
# Copyright (c) 2003-2005 Jimmy Retzlaff
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a
|
||||
# copy of this software and associated documentation files (the "Software"),
|
||||
# to deal in the Software without restriction, including without limitation
|
||||
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
# and/or sell copies of the Software, and to permit persons to whom the
|
||||
# Software is furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
# DEALINGS IN THE SOFTWARE.
|
||||
|
||||
"""
|
||||
AskFolder(...) -- Ask the user to select a folder Windows specific
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import ctypes
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
import ctypes.wintypes as wintypes
|
||||
|
||||
|
||||
__all__ = ['AskFolder']
|
||||
|
||||
# Load required Windows DLLs
|
||||
ole32 = ctypes.windll.ole32
|
||||
shell32 = ctypes.windll.shell32
|
||||
user32 = ctypes.windll.user32
|
||||
|
||||
|
||||
# Windows Constants
|
||||
BFFM_INITIALIZED = 1
|
||||
BFFM_SETOKTEXT = 1129
|
||||
BFFM_SETSELECTIONA = 1126
|
||||
BFFM_SETSELECTIONW = 1127
|
||||
BIF_EDITBOX = 16
|
||||
BS_DEFPUSHBUTTON = 1
|
||||
CB_ADDSTRING = 323
|
||||
CB_GETCURSEL = 327
|
||||
CB_SETCURSEL = 334
|
||||
CDM_SETCONTROLTEXT = 1128
|
||||
EM_GETLINECOUNT = 186
|
||||
EM_GETMARGINS = 212
|
||||
EM_POSFROMCHAR = 214
|
||||
EM_SETSEL = 177
|
||||
GWL_STYLE = -16
|
||||
IDC_STATIC = -1
|
||||
IDCANCEL = 2
|
||||
IDNO = 7
|
||||
IDOK = 1
|
||||
IDYES = 6
|
||||
MAX_PATH = 260
|
||||
OFN_ALLOWMULTISELECT = 512
|
||||
OFN_ENABLEHOOK = 32
|
||||
OFN_ENABLESIZING = 8388608
|
||||
OFN_ENABLETEMPLATEHANDLE = 128
|
||||
OFN_EXPLORER = 524288
|
||||
OFN_OVERWRITEPROMPT = 2
|
||||
OPENFILENAME_SIZE_VERSION_400 = 76
|
||||
PBM_GETPOS = 1032
|
||||
PBM_SETMARQUEE = 1034
|
||||
PBM_SETPOS = 1026
|
||||
PBM_SETRANGE = 1025
|
||||
PBM_SETRANGE32 = 1030
|
||||
PBS_MARQUEE = 8
|
||||
PM_REMOVE = 1
|
||||
SW_HIDE = 0
|
||||
SW_SHOW = 5
|
||||
SW_SHOWNORMAL = 1
|
||||
SWP_NOACTIVATE = 16
|
||||
SWP_NOMOVE = 2
|
||||
SWP_NOSIZE = 1
|
||||
SWP_NOZORDER = 4
|
||||
VER_PLATFORM_WIN32_NT = 2
|
||||
WM_COMMAND = 273
|
||||
WM_GETTEXT = 13
|
||||
WM_GETTEXTLENGTH = 14
|
||||
WM_INITDIALOG = 272
|
||||
WM_NOTIFY = 78
|
||||
|
||||
# Windows function prototypes
|
||||
BrowseCallbackProc = ctypes.WINFUNCTYPE(ctypes.c_int, wintypes.HWND, ctypes.c_uint, wintypes.LPARAM, wintypes.LPARAM)
|
||||
|
||||
# Windows types
|
||||
LPCTSTR = ctypes.c_char_p
|
||||
LPTSTR = ctypes.c_char_p
|
||||
LPVOID = ctypes.c_voidp
|
||||
TCHAR = ctypes.c_char
|
||||
|
||||
class BROWSEINFO(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("hwndOwner", wintypes.HWND),
|
||||
("pidlRoot", LPVOID),
|
||||
("pszDisplayName", LPTSTR),
|
||||
("lpszTitle", LPCTSTR),
|
||||
("ulFlags", ctypes.c_uint),
|
||||
("lpfn", BrowseCallbackProc),
|
||||
("lParam", wintypes.LPARAM),
|
||||
("iImage", ctypes.c_int)
|
||||
]
|
||||
|
||||
|
||||
# Utilities
|
||||
def CenterWindow(hwnd):
|
||||
desktopRect = GetWindowRect(user32.GetDesktopWindow())
|
||||
myRect = GetWindowRect(hwnd)
|
||||
x = width(desktopRect) // 2 - width(myRect) // 2
|
||||
y = height(desktopRect) // 2 - height(myRect) // 2
|
||||
user32.SetWindowPos(hwnd, 0,
|
||||
desktopRect.left + x,
|
||||
desktopRect.top + y,
|
||||
0, 0,
|
||||
SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER
|
||||
)
|
||||
|
||||
|
||||
def GetWindowRect(hwnd):
|
||||
rect = wintypes.RECT()
|
||||
user32.GetWindowRect(hwnd, ctypes.byref(rect))
|
||||
return rect
|
||||
|
||||
def width(rect):
|
||||
return rect.right-rect.left
|
||||
|
||||
def height(rect):
|
||||
return rect.bottom-rect.top
|
||||
|
||||
|
||||
def AskFolder(
|
||||
message=None,
|
||||
version=None,
|
||||
defaultLocation=None,
|
||||
location=None,
|
||||
windowTitle=None,
|
||||
actionButtonLabel=None,
|
||||
cancelButtonLabel=None,
|
||||
multiple=None):
|
||||
"""Display a dialog asking the user for select a folder.
|
||||
modified to use unicode strings as much as possible
|
||||
returns unicode path
|
||||
"""
|
||||
|
||||
def BrowseCallback(hwnd, uMsg, lParam, lpData):
|
||||
if uMsg == BFFM_INITIALIZED:
|
||||
if actionButtonLabel:
|
||||
label = unicode(actionButtonLabel, errors='replace')
|
||||
user32.SendMessageW(hwnd, BFFM_SETOKTEXT, 0, label)
|
||||
if cancelButtonLabel:
|
||||
label = unicode(cancelButtonLabel, errors='replace')
|
||||
cancelButton = user32.GetDlgItem(hwnd, IDCANCEL)
|
||||
if cancelButton:
|
||||
user32.SetWindowTextW(cancelButton, label)
|
||||
if windowTitle:
|
||||
title = unicode(windowTitle, erros='replace')
|
||||
user32.SetWindowTextW(hwnd, title)
|
||||
if defaultLocation:
|
||||
user32.SendMessageW(hwnd, BFFM_SETSELECTIONW, 1, defaultLocation.replace('/', '\\'))
|
||||
if location:
|
||||
x, y = location
|
||||
desktopRect = wintypes.RECT()
|
||||
user32.GetWindowRect(0, ctypes.byref(desktopRect))
|
||||
user32.SetWindowPos(hwnd, 0,
|
||||
desktopRect.left + x,
|
||||
desktopRect.top + y, 0, 0,
|
||||
SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER)
|
||||
else:
|
||||
CenterWindow(hwnd)
|
||||
return 0
|
||||
|
||||
# This next line is needed to prevent gc of the callback
|
||||
callback = BrowseCallbackProc(BrowseCallback)
|
||||
|
||||
browseInfo = BROWSEINFO()
|
||||
browseInfo.pszDisplayName = ctypes.c_char_p('\0' * (MAX_PATH+1))
|
||||
browseInfo.lpszTitle = message
|
||||
browseInfo.lpfn = callback
|
||||
|
||||
pidl = shell32.SHBrowseForFolder(ctypes.byref(browseInfo))
|
||||
if not pidl:
|
||||
result = None
|
||||
else:
|
||||
path = LPCWSTR(u" " * (MAX_PATH+1))
|
||||
shell32.SHGetPathFromIDListW(pidl, path)
|
||||
ole32.CoTaskMemFree(pidl)
|
||||
result = path.value
|
||||
return result
|
||||
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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
|
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue