Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
602ff30b3a | ||
|
|
c010e3f77a | ||
|
|
0c03820db7 | ||
|
|
9fda194391 | ||
|
|
b661a6cdc5 | ||
|
|
0dcd18d524 | ||
|
|
0028027f71 | ||
|
|
4b3618246b | ||
|
|
07ea87edf4 | ||
|
|
899fd419ae | ||
|
|
f3f02adc98 | ||
|
|
0812438b9d | ||
|
|
26d9f7bd20 | ||
|
|
2c95633fcd | ||
|
|
07e532f59c | ||
|
|
882edb6c69 | ||
|
|
93f02c625a | ||
|
|
e95ed1a8ed | ||
|
|
ba5927a20d | ||
|
|
297a9ddc66 | ||
|
|
4f34a9a196 | ||
|
|
529dd3f160 | ||
|
|
4163d5ccf4 | ||
|
|
867ac35b45 | ||
|
|
427137b0fe | ||
|
|
ac9cdb1e98 | ||
|
|
2bedd75005 | ||
|
|
8b632e309f | ||
|
|
bc968f8eca | ||
|
|
00ac669f76 | ||
|
|
694dfafd39 | ||
|
|
a7856f5c32 | ||
|
|
38eabe7612 | ||
|
|
9162698f89 | ||
|
|
506d97d5f0 | ||
|
|
a76ba56cd8 | ||
|
|
8e73edc012 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,9 +2,6 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
# Distribution / packaging
|
||||||
.Python
|
.Python
|
||||||
env/
|
env/
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
From Apprentice Alf's Blog
|
|
||||||
|
|
||||||
Adobe Adept ePub and PDF, .epub, .pdf
|
|
||||||
|
|
||||||
This directory includes modified versions of the I♥CABBAGES Adobe Adept inept scripts for epubs. These scripts have been modified to work with OpenSSL on Windows as well as Linux and Mac OS X. His original scripts can be found in the clearly labelled folder. If a Windows User has OpenSSL installed, these scripts will make use of it in place of PyCrypto.
|
|
||||||
|
|
||||||
The wonderful I♥CABBAGES has produced scripts that will remove the DRM from ePubs and PDFs encryped with Adobe’s DRM. These scripts require installation of the PyCrypto python package *or* the OpenSSL library on Windows. For Mac OS X and Linux boxes, these scripts use the already installed OpenSSL libcrypto so there is no additional requirements for these platforms.
|
|
||||||
|
|
||||||
For more info, see the author's blog:
|
|
||||||
http://i-u2665-cabbages.blogspot.com/2009_02_01_archive.html
|
|
||||||
|
|
||||||
There are two scripts:
|
|
||||||
|
|
||||||
The first is called ineptkey_v5.1.pyw. Simply double-click to launch it and it will create a key file that is needed later to actually remove the DRM. This script need only be run once unless you change your ADE account information.
|
|
||||||
|
|
||||||
The second is called in ineptepub_v5.3.pyw. Simply double-click to launch it. It will ask for your previously generated key file and the path to the book you want to remove the DRM from.
|
|
||||||
|
|
||||||
Both of these scripts are gui python programs. Python 2.X (32 bit) is already installed in Mac OSX. We recommend ActiveState's Active Python Version 2.X (32 bit) for Windows users.
|
|
||||||
|
|
||||||
The latest version of ineptpdf to use is version 8.4.42, which improves support for some PDF files.
|
|
||||||
|
|
||||||
ineptpdf version 8.4.42 can be found here:
|
|
||||||
|
|
||||||
http://pastebin.com/kuKMXXsC
|
|
||||||
|
|
||||||
It is not included in the tools archive.
|
|
||||||
|
|
||||||
If that link is down, please check out the following website for some of the latest releases of these tools:
|
|
||||||
|
|
||||||
http://ainept.freewebspace.com/
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
Readme.txt
|
|
||||||
|
|
||||||
Barnes and Noble EPUB ebooks use a form of Social DRM which requires information on your Credit Card Number and the Name on the Credit card used to purchase the book to actually unencrypt the book.
|
|
||||||
|
|
||||||
For more info, see the author's blog:
|
|
||||||
http://i-u2665-cabbages.blogspot.com/2009_12_01_archive.html
|
|
||||||
|
|
||||||
The original scripts by IHeartCabbages are available here as well. These scripts have been modified to allow the use of OpenSSL in place of PyCrypto to make them easier to run on Linux and Mac OS X.
|
|
||||||
|
|
||||||
There are 2 scripts:
|
|
||||||
|
|
||||||
The first is ignoblekeygen_v2.pyw. Double-click to launch it and provide the required information, and this program will generate a key file needed to remove the DRM from the books. This key file need only be generated once unless either you change your credit card number or your name on the credit card (or if you use a different credit card to purchase your book).
|
|
||||||
|
|
||||||
The second is ignobleepub_v3.pyw. Double-click it and it will ask for your key file and the path to the book to remove the DRM from.
|
|
||||||
|
|
||||||
All of these scripts are gui python programs. Python 2.X (32 bit) is already installed in Mac OSX. We recommend ActiveState's Active Python Version 2.X (32 bit) for Windows users.
|
|
||||||
|
|
||||||
These scripts are based on the IHeartCabbages original scripts that allow the replacement of the requirement for PyCrypto with OpenSSL's libcrypto which is already installed on all Mac OS X machines and Linux Boxes. Window's Users will still have to install PyCrypto or OpenSSL to get these scripts to work properly.
|
|
||||||
|
|
||||||
@@ -1,323 +0,0 @@
|
|||||||
#! /usr/bin/python
|
|
||||||
|
|
||||||
# ignobleepub.pyw, version 3
|
|
||||||
|
|
||||||
# To run this program install Python 2.6 from <http://www.python.org/download/>
|
|
||||||
# and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
|
|
||||||
# (make sure to install the version for Python 2.6). Save this script file as
|
|
||||||
# ignobleepub.pyw and double-click on it to run it.
|
|
||||||
|
|
||||||
# Revision history:
|
|
||||||
# 1 - Initial release
|
|
||||||
# 2 - Added OS X support by using OpenSSL when available
|
|
||||||
# 3 - screen out improper key lengths to prevent segfaults on Linux
|
|
||||||
# 3.1 - Allow Windows versions of libcrypto to be found
|
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import zlib
|
|
||||||
import zipfile
|
|
||||||
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
|
||||||
from contextlib import closing
|
|
||||||
import xml.etree.ElementTree as etree
|
|
||||||
import Tkinter
|
|
||||||
import Tkconstants
|
|
||||||
import tkFileDialog
|
|
||||||
import tkMessageBox
|
|
||||||
|
|
||||||
class IGNOBLEError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _load_crypto_libcrypto():
|
|
||||||
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
|
||||||
Structure, c_ulong, create_string_buffer, cast
|
|
||||||
from ctypes.util import find_library
|
|
||||||
|
|
||||||
if sys.platform.startswith('win'):
|
|
||||||
libcrypto = find_library('libeay32')
|
|
||||||
else:
|
|
||||||
libcrypto = find_library('crypto')
|
|
||||||
if libcrypto is None:
|
|
||||||
raise IGNOBLEError('libcrypto not found')
|
|
||||||
libcrypto = CDLL(libcrypto)
|
|
||||||
|
|
||||||
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])
|
|
||||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
|
||||||
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
|
||||||
c_int])
|
|
||||||
|
|
||||||
class AES(object):
|
|
||||||
def __init__(self, userkey):
|
|
||||||
self._blocksize = len(userkey)
|
|
||||||
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
|
||||||
raise IGNOBLEError('AES improper key used')
|
|
||||||
return
|
|
||||||
key = self._key = AES_KEY()
|
|
||||||
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
|
|
||||||
if rv < 0:
|
|
||||||
raise IGNOBLEError('Failed to initialize AES key')
|
|
||||||
|
|
||||||
def decrypt(self, data):
|
|
||||||
out = create_string_buffer(len(data))
|
|
||||||
iv = ("\x00" * self._blocksize)
|
|
||||||
rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
|
|
||||||
if rv == 0:
|
|
||||||
raise IGNOBLEError('AES decryption failed')
|
|
||||||
return out.raw
|
|
||||||
|
|
||||||
return AES
|
|
||||||
|
|
||||||
def _load_crypto_pycrypto():
|
|
||||||
from Crypto.Cipher import AES as _AES
|
|
||||||
|
|
||||||
class AES(object):
|
|
||||||
def __init__(self, key):
|
|
||||||
self._aes = _AES.new(key, _AES.MODE_CBC)
|
|
||||||
|
|
||||||
def decrypt(self, data):
|
|
||||||
return self._aes.decrypt(data)
|
|
||||||
|
|
||||||
return AES
|
|
||||||
|
|
||||||
def _load_crypto():
|
|
||||||
AES = None
|
|
||||||
for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto):
|
|
||||||
try:
|
|
||||||
AES = loader()
|
|
||||||
break
|
|
||||||
except (ImportError, IGNOBLEError):
|
|
||||||
pass
|
|
||||||
return AES
|
|
||||||
AES = _load_crypto()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
Decrypt Barnes & Noble ADEPT encrypted EPUB books.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
|
|
||||||
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
|
||||||
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
|
||||||
|
|
||||||
class ZipInfo(zipfile.ZipInfo):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
if 'compress_type' in kwargs:
|
|
||||||
compress_type = kwargs.pop('compress_type')
|
|
||||||
super(ZipInfo, self).__init__(*args, **kwargs)
|
|
||||||
self.compress_type = compress_type
|
|
||||||
|
|
||||||
class Decryptor(object):
|
|
||||||
def __init__(self, bookkey, encryption):
|
|
||||||
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
|
|
||||||
# self._aes = AES.new(bookkey, AES.MODE_CBC)
|
|
||||||
self._aes = AES(bookkey)
|
|
||||||
encryption = etree.fromstring(encryption)
|
|
||||||
self._encrypted = encrypted = set()
|
|
||||||
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
|
|
||||||
enc('CipherReference'))
|
|
||||||
for elem in encryption.findall(expr):
|
|
||||||
path = elem.get('URI', None)
|
|
||||||
if path is not None:
|
|
||||||
encrypted.add(path)
|
|
||||||
|
|
||||||
def decompress(self, bytes):
|
|
||||||
dc = zlib.decompressobj(-15)
|
|
||||||
bytes = dc.decompress(bytes)
|
|
||||||
ex = dc.decompress('Z') + dc.flush()
|
|
||||||
if ex:
|
|
||||||
bytes = bytes + ex
|
|
||||||
return bytes
|
|
||||||
|
|
||||||
def decrypt(self, path, data):
|
|
||||||
if path in self._encrypted:
|
|
||||||
data = self._aes.decrypt(data)[16:]
|
|
||||||
data = data[:-ord(data[-1])]
|
|
||||||
data = self.decompress(data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def cli_main(argv=sys.argv):
|
|
||||||
progname = os.path.basename(argv[0])
|
|
||||||
if AES is None:
|
|
||||||
print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \
|
|
||||||
"separately. Read the top-of-script comment for details." % \
|
|
||||||
(progname,)
|
|
||||||
return 1
|
|
||||||
if len(argv) != 4:
|
|
||||||
print "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,)
|
|
||||||
return 1
|
|
||||||
keypath, inpath, outpath = argv[1:]
|
|
||||||
with open(keypath, 'rb') as f:
|
|
||||||
keyb64 = f.read()
|
|
||||||
key = keyb64.decode('base64')[:16]
|
|
||||||
# aes = AES.new(key, AES.MODE_CBC)
|
|
||||||
aes = AES(key)
|
|
||||||
|
|
||||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
|
||||||
namelist = set(inf.namelist())
|
|
||||||
if 'META-INF/rights.xml' not in namelist or \
|
|
||||||
'META-INF/encryption.xml' not in namelist:
|
|
||||||
raise IGNOBLEError('%s: not an B&N ADEPT EPUB' % (inpath,))
|
|
||||||
for name in META_NAMES:
|
|
||||||
namelist.remove(name)
|
|
||||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
|
||||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
|
||||||
expr = './/%s' % (adept('encryptedKey'),)
|
|
||||||
bookkey = ''.join(rights.findtext(expr))
|
|
||||||
bookkey = aes.decrypt(bookkey.decode('base64'))
|
|
||||||
bookkey = bookkey[:-ord(bookkey[-1])]
|
|
||||||
encryption = inf.read('META-INF/encryption.xml')
|
|
||||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
|
||||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
|
||||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
|
||||||
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
|
||||||
outf.writestr(zi, inf.read('mimetype'))
|
|
||||||
for path in namelist:
|
|
||||||
data = inf.read(path)
|
|
||||||
outf.writestr(path, decryptor.decrypt(path, data))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
class DecryptionDialog(Tkinter.Frame):
|
|
||||||
def __init__(self, root):
|
|
||||||
Tkinter.Frame.__init__(self, root, border=5)
|
|
||||||
self.status = Tkinter.Label(self, text='Select files for decryption')
|
|
||||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
|
||||||
body = Tkinter.Frame(self)
|
|
||||||
body.pack(fill=Tkconstants.X, expand=1)
|
|
||||||
sticky = Tkconstants.E + Tkconstants.W
|
|
||||||
body.grid_columnconfigure(1, weight=2)
|
|
||||||
Tkinter.Label(body, text='Key file').grid(row=0)
|
|
||||||
self.keypath = Tkinter.Entry(body, width=30)
|
|
||||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
|
||||||
if os.path.exists('bnepubkey.b64'):
|
|
||||||
self.keypath.insert(0, 'bnepubkey.b64')
|
|
||||||
button = Tkinter.Button(body, text="...", command=self.get_keypath)
|
|
||||||
button.grid(row=0, column=2)
|
|
||||||
Tkinter.Label(body, text='Input file').grid(row=1)
|
|
||||||
self.inpath = Tkinter.Entry(body, width=30)
|
|
||||||
self.inpath.grid(row=1, column=1, sticky=sticky)
|
|
||||||
button = Tkinter.Button(body, text="...", command=self.get_inpath)
|
|
||||||
button.grid(row=1, column=2)
|
|
||||||
Tkinter.Label(body, text='Output file').grid(row=2)
|
|
||||||
self.outpath = Tkinter.Entry(body, width=30)
|
|
||||||
self.outpath.grid(row=2, column=1, sticky=sticky)
|
|
||||||
button = Tkinter.Button(body, text="...", command=self.get_outpath)
|
|
||||||
button.grid(row=2, column=2)
|
|
||||||
buttons = Tkinter.Frame(self)
|
|
||||||
buttons.pack()
|
|
||||||
botton = Tkinter.Button(
|
|
||||||
buttons, text="Decrypt", width=10, command=self.decrypt)
|
|
||||||
botton.pack(side=Tkconstants.LEFT)
|
|
||||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
|
||||||
button = Tkinter.Button(
|
|
||||||
buttons, text="Quit", width=10, command=self.quit)
|
|
||||||
button.pack(side=Tkconstants.RIGHT)
|
|
||||||
|
|
||||||
def get_keypath(self):
|
|
||||||
keypath = tkFileDialog.askopenfilename(
|
|
||||||
parent=None, title='Select B&N EPUB key file',
|
|
||||||
defaultextension='.b64',
|
|
||||||
filetypes=[('base64-encoded files', '.b64'),
|
|
||||||
('All Files', '.*')])
|
|
||||||
if keypath:
|
|
||||||
keypath = os.path.normpath(keypath)
|
|
||||||
self.keypath.delete(0, Tkconstants.END)
|
|
||||||
self.keypath.insert(0, keypath)
|
|
||||||
return
|
|
||||||
|
|
||||||
def get_inpath(self):
|
|
||||||
inpath = tkFileDialog.askopenfilename(
|
|
||||||
parent=None, title='Select B&N-encrypted EPUB file to decrypt',
|
|
||||||
defaultextension='.epub', filetypes=[('EPUB files', '.epub'),
|
|
||||||
('All files', '.*')])
|
|
||||||
if inpath:
|
|
||||||
inpath = os.path.normpath(inpath)
|
|
||||||
self.inpath.delete(0, Tkconstants.END)
|
|
||||||
self.inpath.insert(0, inpath)
|
|
||||||
return
|
|
||||||
|
|
||||||
def get_outpath(self):
|
|
||||||
outpath = tkFileDialog.asksaveasfilename(
|
|
||||||
parent=None, title='Select unencrypted EPUB file to produce',
|
|
||||||
defaultextension='.epub', filetypes=[('EPUB files', '.epub'),
|
|
||||||
('All files', '.*')])
|
|
||||||
if outpath:
|
|
||||||
outpath = os.path.normpath(outpath)
|
|
||||||
self.outpath.delete(0, Tkconstants.END)
|
|
||||||
self.outpath.insert(0, outpath)
|
|
||||||
return
|
|
||||||
|
|
||||||
def decrypt(self):
|
|
||||||
keypath = self.keypath.get()
|
|
||||||
inpath = self.inpath.get()
|
|
||||||
outpath = self.outpath.get()
|
|
||||||
if not keypath or not os.path.exists(keypath):
|
|
||||||
self.status['text'] = 'Specified key file does not exist'
|
|
||||||
return
|
|
||||||
if not inpath or not os.path.exists(inpath):
|
|
||||||
self.status['text'] = 'Specified input file does not exist'
|
|
||||||
return
|
|
||||||
if not outpath:
|
|
||||||
self.status['text'] = 'Output file not specified'
|
|
||||||
return
|
|
||||||
if inpath == outpath:
|
|
||||||
self.status['text'] = 'Must have different input and output files'
|
|
||||||
return
|
|
||||||
argv = [sys.argv[0], keypath, inpath, outpath]
|
|
||||||
self.status['text'] = 'Decrypting...'
|
|
||||||
try:
|
|
||||||
cli_main(argv)
|
|
||||||
except Exception, e:
|
|
||||||
self.status['text'] = 'Error: ' + str(e)
|
|
||||||
return
|
|
||||||
self.status['text'] = 'File successfully decrypted'
|
|
||||||
|
|
||||||
def gui_main():
|
|
||||||
root = Tkinter.Tk()
|
|
||||||
if AES is None:
|
|
||||||
root.withdraw()
|
|
||||||
tkMessageBox.showerror(
|
|
||||||
"Ignoble EPUB Decrypter",
|
|
||||||
"This script requires OpenSSL or PyCrypto, which must be installed "
|
|
||||||
"separately. Read the top-of-script comment for details.")
|
|
||||||
return 1
|
|
||||||
root.title('Ignoble EPUB Decrypter')
|
|
||||||
root.resizable(True, False)
|
|
||||||
root.minsize(300, 0)
|
|
||||||
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
|
||||||
root.mainloop()
|
|
||||||
return 0
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if len(sys.argv) > 1:
|
|
||||||
sys.exit(cli_main())
|
|
||||||
sys.exit(gui_main())
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
#! /usr/bin/python
|
|
||||||
|
|
||||||
# ignoblekey.pyw, version 2
|
|
||||||
|
|
||||||
# To run this program install Python 2.6 from <http://www.python.org/download/>
|
|
||||||
# Save this script file as ignoblekey.pyw and double-click on it to run it.
|
|
||||||
|
|
||||||
# Revision history:
|
|
||||||
# 1 - Initial release
|
|
||||||
# 2 - Add some missing code
|
|
||||||
|
|
||||||
"""
|
|
||||||
Retrieve B&N DesktopReader EPUB user AES key.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import binascii
|
|
||||||
import glob
|
|
||||||
import Tkinter
|
|
||||||
import Tkconstants
|
|
||||||
import tkMessageBox
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
BN_KEY_KEY = 'uhk00000000'
|
|
||||||
BN_APPDATA_DIR = r'Barnes & Noble\DesktopReader'
|
|
||||||
|
|
||||||
class IgnobleError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def retrieve_key(inpath, outpath):
|
|
||||||
# The B&N DesktopReader 'ClientAPI' file is just a sqlite3 DB. Requiring
|
|
||||||
# users to install sqlite3 and bindings seems like overkill for retrieving
|
|
||||||
# one value, so we go in hot and dirty.
|
|
||||||
with open(inpath, 'rb') as f:
|
|
||||||
data = f.read()
|
|
||||||
if BN_KEY_KEY not in data:
|
|
||||||
raise IgnobleError('B&N user key not found; unexpected DB format?')
|
|
||||||
index = data.rindex(BN_KEY_KEY) + len(BN_KEY_KEY) + 1
|
|
||||||
data = data[index:index + 40]
|
|
||||||
for i in xrange(20, len(data)):
|
|
||||||
try:
|
|
||||||
keyb64 = data[:i]
|
|
||||||
if len(keyb64.decode('base64')) == 20:
|
|
||||||
break
|
|
||||||
except binascii.Error:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
raise IgnobleError('Problem decoding key; unexpected DB format?')
|
|
||||||
with open(outpath, 'wb') as f:
|
|
||||||
f.write(keyb64 + '\n')
|
|
||||||
|
|
||||||
def cli_main(argv=sys.argv):
|
|
||||||
progname = os.path.basename(argv[0])
|
|
||||||
args = argv[1:]
|
|
||||||
if len(args) != 2:
|
|
||||||
sys.stderr.write("USAGE: %s CLIENTDB KEYFILE" % (progname,))
|
|
||||||
return 1
|
|
||||||
inpath, outpath = args
|
|
||||||
retrieve_key(inpath, outpath)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def find_bnclientdb_path():
|
|
||||||
appdata = os.environ['APPDATA']
|
|
||||||
bndir = os.path.join(appdata, BN_APPDATA_DIR)
|
|
||||||
if not os.path.isdir(bndir):
|
|
||||||
raise IgnobleError('Could not locate B&N Reader installation')
|
|
||||||
dbpath = glob.glob(os.path.join(bndir, 'ClientAPI_*.db'))
|
|
||||||
if len(dbpath) == 0:
|
|
||||||
raise IgnobleError('Problem locating B&N Reader DB')
|
|
||||||
return sorted(dbpath)[-1]
|
|
||||||
|
|
||||||
class ExceptionDialog(Tkinter.Frame):
|
|
||||||
def __init__(self, root, text):
|
|
||||||
Tkinter.Frame.__init__(self, root, border=5)
|
|
||||||
label = Tkinter.Label(self, text="Unexpected error:",
|
|
||||||
anchor=Tkconstants.W, justify=Tkconstants.LEFT)
|
|
||||||
label.pack(fill=Tkconstants.X, expand=0)
|
|
||||||
self.text = Tkinter.Text(self)
|
|
||||||
self.text.pack(fill=Tkconstants.BOTH, expand=1)
|
|
||||||
self.text.insert(Tkconstants.END, text)
|
|
||||||
|
|
||||||
def gui_main(argv=sys.argv):
|
|
||||||
root = Tkinter.Tk()
|
|
||||||
root.withdraw()
|
|
||||||
progname = os.path.basename(argv[0])
|
|
||||||
keypath = 'bnepubkey.b64'
|
|
||||||
try:
|
|
||||||
dbpath = find_bnclientdb_path()
|
|
||||||
retrieve_key(dbpath, keypath)
|
|
||||||
except IgnobleError, e:
|
|
||||||
tkMessageBox.showerror("Ignoble Key", "Error: " + str(e))
|
|
||||||
return 1
|
|
||||||
except Exception:
|
|
||||||
root.wm_state('normal')
|
|
||||||
root.title('Ignoble Key')
|
|
||||||
text = traceback.format_exc()
|
|
||||||
ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1)
|
|
||||||
root.mainloop()
|
|
||||||
return 1
|
|
||||||
tkMessageBox.showinfo(
|
|
||||||
"Ignoble Key", "Key successfully retrieved to %s" % (keypath))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if len(sys.argv) > 1:
|
|
||||||
sys.exit(cli_main())
|
|
||||||
sys.exit(gui_main())
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
#! /usr/bin/python
|
|
||||||
|
|
||||||
# ignoblekeygen.pyw, version 2
|
|
||||||
|
|
||||||
# To run this program install Python 2.6 from <http://www.python.org/download/>
|
|
||||||
# and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
|
|
||||||
# (make sure to install the version for Python 2.6). Save this script file as
|
|
||||||
# ignoblekeygen.pyw and double-click on it to run it.
|
|
||||||
|
|
||||||
# Revision history:
|
|
||||||
# 1 - Initial release
|
|
||||||
# 2 - Add OS X support by using OpenSSL when available (taken/modified from ineptepub v5)
|
|
||||||
# 2.1 - Allow Windows versions of libcrypto to be found
|
|
||||||
|
|
||||||
"""
|
|
||||||
Generate Barnes & Noble EPUB user key from name and credit card number.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import hashlib
|
|
||||||
import Tkinter
|
|
||||||
import Tkconstants
|
|
||||||
import tkFileDialog
|
|
||||||
import tkMessageBox
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# use openssl's libcrypt if it exists in place of pycrypto
|
|
||||||
# code extracted from the Adobe Adept DRM removal code also by I HeartCabbages
|
|
||||||
class IGNOBLEError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _load_crypto_libcrypto():
|
|
||||||
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
|
||||||
Structure, c_ulong, create_string_buffer, cast
|
|
||||||
from ctypes.util import find_library
|
|
||||||
|
|
||||||
if sys.platform.startswith('win'):
|
|
||||||
libcrypto = find_library('libeay32')
|
|
||||||
else:
|
|
||||||
libcrypto = find_library('crypto')
|
|
||||||
if libcrypto is None:
|
|
||||||
print 'libcrypto not found'
|
|
||||||
raise IGNOBLEError('libcrypto not found')
|
|
||||||
libcrypto = CDLL(libcrypto)
|
|
||||||
|
|
||||||
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_set_encrypt_key = F(c_int, 'AES_set_encrypt_key',
|
|
||||||
[c_char_p, c_int, AES_KEY_p])
|
|
||||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
|
||||||
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
|
||||||
c_int])
|
|
||||||
class AES(object):
|
|
||||||
def __init__(self, userkey, iv):
|
|
||||||
self._blocksize = len(userkey)
|
|
||||||
self._iv = iv
|
|
||||||
key = self._key = AES_KEY()
|
|
||||||
rv = AES_set_encrypt_key(userkey, len(userkey) * 8, key)
|
|
||||||
if rv < 0:
|
|
||||||
raise IGNOBLEError('Failed to initialize AES Encrypt key')
|
|
||||||
|
|
||||||
def encrypt(self, data):
|
|
||||||
out = create_string_buffer(len(data))
|
|
||||||
rv = AES_cbc_encrypt(data, out, len(data), self._key, self._iv, 1)
|
|
||||||
if rv == 0:
|
|
||||||
raise IGNOBLEError('AES encryption failed')
|
|
||||||
return out.raw
|
|
||||||
|
|
||||||
return AES
|
|
||||||
|
|
||||||
|
|
||||||
def _load_crypto_pycrypto():
|
|
||||||
from Crypto.Cipher import AES as _AES
|
|
||||||
|
|
||||||
class AES(object):
|
|
||||||
def __init__(self, key, iv):
|
|
||||||
self._aes = _AES.new(key, _AES.MODE_CBC, iv)
|
|
||||||
|
|
||||||
def encrypt(self, data):
|
|
||||||
return self._aes.encrypt(data)
|
|
||||||
|
|
||||||
return AES
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _load_crypto():
|
|
||||||
AES = None
|
|
||||||
for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto):
|
|
||||||
try:
|
|
||||||
AES = loader()
|
|
||||||
break
|
|
||||||
except (ImportError, IGNOBLEError):
|
|
||||||
pass
|
|
||||||
return AES
|
|
||||||
|
|
||||||
AES = _load_crypto()
|
|
||||||
|
|
||||||
def normalize_name(name):
|
|
||||||
return ''.join(x for x in name.lower() if x != ' ')
|
|
||||||
|
|
||||||
def generate_keyfile(name, ccn, outpath):
|
|
||||||
name = normalize_name(name) + '\x00'
|
|
||||||
ccn = ccn + '\x00'
|
|
||||||
name_sha = hashlib.sha1(name).digest()[:16]
|
|
||||||
ccn_sha = hashlib.sha1(ccn).digest()[:16]
|
|
||||||
both_sha = hashlib.sha1(name + ccn).digest()
|
|
||||||
aes = AES(ccn_sha, name_sha)
|
|
||||||
crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c))
|
|
||||||
userkey = hashlib.sha1(crypt).digest()
|
|
||||||
with open(outpath, 'wb') as f:
|
|
||||||
f.write(userkey.encode('base64'))
|
|
||||||
return userkey
|
|
||||||
|
|
||||||
def cli_main(argv=sys.argv):
|
|
||||||
progname = os.path.basename(argv[0])
|
|
||||||
if AES is None:
|
|
||||||
print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \
|
|
||||||
"separately. Read the top-of-script comment for details." % \
|
|
||||||
(progname,)
|
|
||||||
return 1
|
|
||||||
if len(argv) != 4:
|
|
||||||
print "usage: %s NAME CC# OUTFILE" % (progname,)
|
|
||||||
return 1
|
|
||||||
name, ccn, outpath = argv[1:]
|
|
||||||
generate_keyfile(name, ccn, outpath)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
class DecryptionDialog(Tkinter.Frame):
|
|
||||||
def __init__(self, root):
|
|
||||||
Tkinter.Frame.__init__(self, root, border=5)
|
|
||||||
self.status = Tkinter.Label(self, text='Enter parameters')
|
|
||||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
|
||||||
body = Tkinter.Frame(self)
|
|
||||||
body.pack(fill=Tkconstants.X, expand=1)
|
|
||||||
sticky = Tkconstants.E + Tkconstants.W
|
|
||||||
body.grid_columnconfigure(1, weight=2)
|
|
||||||
Tkinter.Label(body, text='Name').grid(row=1)
|
|
||||||
self.name = Tkinter.Entry(body, width=30)
|
|
||||||
self.name.grid(row=1, column=1, sticky=sticky)
|
|
||||||
Tkinter.Label(body, text='CC#').grid(row=2)
|
|
||||||
self.ccn = Tkinter.Entry(body, width=30)
|
|
||||||
self.ccn.grid(row=2, column=1, sticky=sticky)
|
|
||||||
Tkinter.Label(body, text='Output file').grid(row=0)
|
|
||||||
self.keypath = Tkinter.Entry(body, width=30)
|
|
||||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
|
||||||
self.keypath.insert(0, 'bnepubkey.b64')
|
|
||||||
button = Tkinter.Button(body, text="...", command=self.get_keypath)
|
|
||||||
button.grid(row=0, column=2)
|
|
||||||
buttons = Tkinter.Frame(self)
|
|
||||||
buttons.pack()
|
|
||||||
botton = Tkinter.Button(
|
|
||||||
buttons, text="Generate", width=10, command=self.generate)
|
|
||||||
botton.pack(side=Tkconstants.LEFT)
|
|
||||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
|
||||||
button = Tkinter.Button(
|
|
||||||
buttons, text="Quit", width=10, command=self.quit)
|
|
||||||
button.pack(side=Tkconstants.RIGHT)
|
|
||||||
|
|
||||||
def get_keypath(self):
|
|
||||||
keypath = tkFileDialog.asksaveasfilename(
|
|
||||||
parent=None, title='Select B&N EPUB key file to produce',
|
|
||||||
defaultextension='.b64',
|
|
||||||
filetypes=[('base64-encoded files', '.b64'),
|
|
||||||
('All Files', '.*')])
|
|
||||||
if keypath:
|
|
||||||
keypath = os.path.normpath(keypath)
|
|
||||||
self.keypath.delete(0, Tkconstants.END)
|
|
||||||
self.keypath.insert(0, keypath)
|
|
||||||
return
|
|
||||||
|
|
||||||
def generate(self):
|
|
||||||
name = self.name.get()
|
|
||||||
ccn = self.ccn.get()
|
|
||||||
keypath = self.keypath.get()
|
|
||||||
if not name:
|
|
||||||
self.status['text'] = 'Name not specified'
|
|
||||||
return
|
|
||||||
if not ccn:
|
|
||||||
self.status['text'] = 'Credit card number not specified'
|
|
||||||
return
|
|
||||||
if not keypath:
|
|
||||||
self.status['text'] = 'Output keyfile path not specified'
|
|
||||||
return
|
|
||||||
self.status['text'] = 'Generating...'
|
|
||||||
try:
|
|
||||||
generate_keyfile(name, ccn, keypath)
|
|
||||||
except Exception, e:
|
|
||||||
self.status['text'] = 'Error: ' + str(e)
|
|
||||||
return
|
|
||||||
self.status['text'] = 'Keyfile successfully generated'
|
|
||||||
|
|
||||||
def gui_main():
|
|
||||||
root = Tkinter.Tk()
|
|
||||||
if AES is None:
|
|
||||||
root.withdraw()
|
|
||||||
tkMessageBox.showerror(
|
|
||||||
"Ignoble EPUB Keyfile Generator",
|
|
||||||
"This script requires OpenSSL or PyCrypto, which must be installed "
|
|
||||||
"separately. Read the top-of-script comment for details.")
|
|
||||||
return 1
|
|
||||||
root.title('Ignoble EPUB Keyfile Generator')
|
|
||||||
root.resizable(True, False)
|
|
||||||
root.minsize(300, 0)
|
|
||||||
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
|
||||||
root.mainloop()
|
|
||||||
return 0
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if len(sys.argv) > 1:
|
|
||||||
sys.exit(cli_main())
|
|
||||||
sys.exit(gui_main())
|
|
||||||
70
Calibre_Plugins/Ignobleepub ReadMe.txt
Normal file
70
Calibre_Plugins/Ignobleepub ReadMe.txt
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
Ignoble Epub DeDRM - ignobleepub_v02.5_plugin.zip
|
||||||
|
=================================================
|
||||||
|
|
||||||
|
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 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.
|
||||||
|
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior" to go to Calibre's Preferences page. Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (ignobleepub_v02.5_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog.
|
||||||
|
|
||||||
|
|
||||||
|
Customization
|
||||||
|
-------------
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Upgrading from old keys
|
||||||
|
|
||||||
|
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!!
|
||||||
|
|
||||||
|
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 always have the ability to import existing keyfiles anytime you might need/want to.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Creating New Keys:
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
* Unique Key Name: 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.
|
||||||
|
* Your Name: 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.
|
||||||
|
* Credit Card number: this is the credit card number that was set as default with Barnes & Noble at the time of download. Nothing fancy here; no dashes or spaces ... just the 16 (15?) 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.
|
||||||
|
Click the 'OK" button to create and store the generated key. Or Cancel if you didn't want to create a key.
|
||||||
|
|
||||||
|
Deleting Keys:
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Exporting Keys:
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Importing Existing Keyfiles:
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
Troubleshooting
|
||||||
|
---------------
|
||||||
|
|
||||||
|
If you find that it's not working for you (imported ebooks still have DRM), you can save a lot of time and trouble by first deleting the DRMed ebook from calibre and then trying to add the ebook 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. ;)
|
||||||
|
|
||||||
|
On Macintosh only you must first run calibre, open Preferences, open Miscellaneous, and click on the “Install command line tools” button. (On Windows and Linux the command line tools are installed automatically.)
|
||||||
|
|
||||||
|
On Windows, open a terminal/command window. (Start/Run… and then type 'cmd' (without the 's) as the program to run).
|
||||||
|
On Macintosh, open the Terminal application (in your Utilities folder).
|
||||||
|
On Linux open a command window. Hopefully all Linux users know how to do this, as I do not.
|
||||||
|
|
||||||
|
You should now have a text-based command-line window open. Also have open the folder containing the ebook to be imported. Make sure that book isn’t already in calibre, and that calibre isn’t running.
|
||||||
|
|
||||||
|
Now type in "calibredb add " (without the " but don’t miss that final space) and now drag the book to be imported onto the window. The full path to the book should be inserted into the command line. Now press the return/enter key. The import routines will run and produce some logging information.
|
||||||
|
|
||||||
|
Now copy the output from the terminal window.
|
||||||
|
On Windows, you must use the window menu (little icon at left of window bar) to select all the text and then to copy it.
|
||||||
|
On Macintosh and Linux, just use the normal text select and copy commands.
|
||||||
|
|
||||||
|
Paste the information into a comment at my blog, http://apprenticealf.wordpress.com/ describing your problem.
|
||||||
115
Calibre_Plugins/Ineptepub ReadMe.txt
Normal file
115
Calibre_Plugins/Ineptepub ReadMe.txt
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
Inept Epub DeDRM - ineptepub_v02.0_plugin.zip
|
||||||
|
=============================================
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior" to go to Calibre's Preferences page. Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (ineptepub_v02.0_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Customization
|
||||||
|
-------------
|
||||||
|
|
||||||
|
When first run, the plugin will attempt to find your Adobe Digital Editions installation (on Windows and Mac OS). If successful, it will create 'calibre-adeptkey[number].der' file(s) and save them in Calibre's configuration directory. It will use those files and any other '*.der' files in any decryption attempts. If there is already at least one 'calibre-adept*.der' file in the directory, the plugin won't attempt to find the Adobe Digital Editions installation keys again.
|
||||||
|
|
||||||
|
So if you have Adobe Digital Editions installation installed on the same machine as Calibre... you are ready to go. If not... keep reading.
|
||||||
|
|
||||||
|
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 Epub DeDRM plugin.
|
||||||
|
|
||||||
|
|
||||||
|
Troubleshooting
|
||||||
|
---------------
|
||||||
|
|
||||||
|
If you find that it's not working for you (imported ebooks still have DRM), you can save a lot of time and trouble by first deleting the DRMed ebook from calibre and then trying to add the ebook 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. ;)
|
||||||
|
|
||||||
|
On Macintosh only you must first run calibre, open Preferences, open Miscellaneous, and click on the “Install command line tools” button. (On Windows and Linux the command line tools are installed automatically.)
|
||||||
|
|
||||||
|
On Windows, open a terminal/command window. (Start/Run… and then type ‘cmd’ (without the ‘s) as the program to run).
|
||||||
|
On Macintosh, open the Terminal application (in your Utilities folder).
|
||||||
|
On Linux open a command window. Hopefully all Linux users know how to do this, as I do not.
|
||||||
|
|
||||||
|
You should now have a text-based command-line window open. Also have open the folder containing the ebook to be imported. Make sure that book isn’t already in calibre, and that calibre isn’t running.
|
||||||
|
|
||||||
|
Now type in "calibredb add " (without the " but don’t miss that final space) and now drag the book to be imported onto the window. The full path to the book should be inserted into the command line. Now press the return/enter key. The import routines will run and produce some logging information.
|
||||||
|
|
||||||
|
Now copy the output from the terminal window.
|
||||||
|
On Windows, you must use the window menu (little icon at left of window bar) to select all the text and then to copy it.
|
||||||
|
On Macintosh and Linux, just use the normal text select and copy commands.
|
||||||
|
|
||||||
|
Paste the information into a comment at my blog, http://apprenticealf.wordpress.com/ describing your problem.
|
||||||
|
|
||||||
|
|
||||||
|
Linux and Adobe Digital Editions ePubs
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
Here are the instructions for using the tools with ePub books and Adobe Digital Editions on Linux under Wine. (Thank you mclien!)
|
||||||
|
|
||||||
|
|
||||||
|
1. download the most recent version of wine from winehq.org (1.3.29 in my case)
|
||||||
|
|
||||||
|
For debian users:
|
||||||
|
|
||||||
|
to get a recent version of wine I decited to use aptosid (2011-02, xfce)
|
||||||
|
(because I’m used to debian)
|
||||||
|
install aptosid and upgrade it (see aptosid site for detaild instructions)
|
||||||
|
|
||||||
|
|
||||||
|
2. properly install Wine (see the Wine site for details)
|
||||||
|
|
||||||
|
For debian users:
|
||||||
|
|
||||||
|
cd to this dir and install the packages as root:
|
||||||
|
‘dpkg -i *.deb’
|
||||||
|
you will get some error messages, which can be ignored.
|
||||||
|
again as root use
|
||||||
|
‘apt-get -f install’ to correct this errors
|
||||||
|
|
||||||
|
3. python 2.7 should already be installed on your system but you may need the following additional python package
|
||||||
|
|
||||||
|
'apt-get install python-tk’
|
||||||
|
|
||||||
|
4. all programms need to be installed as normal user. All these programm are installed the same way:
|
||||||
|
‘wine ‘
|
||||||
|
we need:
|
||||||
|
a) Adobe Digital Edition 1.7.2(from: http://kb2.adobe.com/cps/403/kb403051.html)
|
||||||
|
(there is a “can’t install ADE” site, where the setup.exe hides)
|
||||||
|
|
||||||
|
b) ActivePython-2.7.2.5-win32-x86.msi (from: http://www.activestate.com/activepython/downloads)
|
||||||
|
|
||||||
|
c) Win32OpenSSL_Light-0_9_8r.exe (from: http://www.slproweb.com/)
|
||||||
|
|
||||||
|
d) pycrypto-2.3.win32-py2.7.msi (from: http://www.voidspace.org.uk/python/modules.shtml)
|
||||||
|
|
||||||
|
5. now get and unpack the very latest tools_vX.X (from Apprentice Alf) in the users drive_c of wine
|
||||||
|
(~/.wine/drive_c/)
|
||||||
|
|
||||||
|
6. start ADE with:
|
||||||
|
‘wine digitaleditions.exe’ or from the start menue wine-adobe-digital..
|
||||||
|
|
||||||
|
7. register this instance of ADE with your adobeID and close it
|
||||||
|
change to the tools_vX.X dir:
|
||||||
|
cd ~/.wine/drive_c/tools_vX.X/Other_Tools/
|
||||||
|
|
||||||
|
8. create the adeptkey.der with:
|
||||||
|
‘wine python ineptkey.py’ (only need once!)
|
||||||
|
(key will be here: ~/.wine/drive_c/tools_vX.X/Other_Tools/adeptkey.der)
|
||||||
|
|
||||||
|
9. Use ADE running under Wine to dowload all of your purchased ePub ebooks
|
||||||
|
|
||||||
|
10. install the ineptepub and ineptpdf plugins from the tools as discribed in the readmes.
|
||||||
|
|
||||||
|
11. copy the adeptkey.der into the config dir of calibre (~/.config/calibre in debian). Your ADE books imported to calibre will automatically be freed from DRM.
|
||||||
|
|
||||||
114
Calibre_Plugins/Ineptpdf ReadMe.txt
Normal file
114
Calibre_Plugins/Ineptpdf ReadMe.txt
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
Inept PDF Plugin - ineptpdf_v01.9_plugin.zip
|
||||||
|
============================================
|
||||||
|
|
||||||
|
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, PyCrypto and/or OpenSSL already installed, but they aren't necessary.
|
||||||
|
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior" to go to Calibre's Preferences page. Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (ineptpdf_v01.9_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog.
|
||||||
|
|
||||||
|
|
||||||
|
Customization
|
||||||
|
-------------
|
||||||
|
|
||||||
|
When first run, the plugin will attempt to find your Adobe Digital Editions installation (on Windows and Mac OS). If successful, it will create 'calibre-adeptkey[number].der' file(s) and save them in Calibre's configuration directory. It will use those files and any other '*.der' files in any decryption attempts. If there is already at least one 'calibre-adept*.der' file in the directory, the plugin won't attempt to find the Adobe Digital Editions installation keys again.
|
||||||
|
|
||||||
|
So if you have Adobe Digital Editions installation installed on the same machine as Calibre... you are ready to go. If not... keep reading.
|
||||||
|
|
||||||
|
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 plugin.
|
||||||
|
|
||||||
|
|
||||||
|
Troubleshooting
|
||||||
|
---------------
|
||||||
|
|
||||||
|
If you find that it's not working for you (imported ebooks still have DRM), you can save a lot of time and trouble by first deleting the DRMed ebook from calibre and then trying to add the ebook 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. ;)
|
||||||
|
|
||||||
|
On Macintosh only you must first run calibre, open Preferences, open Miscellaneous, and click on the “Install command line tools” button. (On Windows and Linux the command line tools are installed automatically.)
|
||||||
|
|
||||||
|
On Windows, open a terminal/command window. (Start/Run… and then type ‘cmd’ (without the ‘s) as the program to run).
|
||||||
|
On Macintosh, open the Terminal application (in your Utilities folder).
|
||||||
|
On Linux open a command window. Hopefully all Linux users know how to do this, as I do not.
|
||||||
|
|
||||||
|
You should now have a text-based command-line window open. Also have open the folder containing the ebook to be imported. Make sure that book isn’t already in calibre, and that calibre isn’t running.
|
||||||
|
|
||||||
|
Now type in "calibredb add " (without the " but don’t miss that final space) and now drag the book to be imported onto the window. The full path to the book should be inserted into the command line. Now press the return/enter key. The import routines will run and produce some logging information.
|
||||||
|
|
||||||
|
Now copy the output from the terminal window.
|
||||||
|
On Windows, you must use the window menu (little icon at left of window bar) to select all the text and then to copy it.
|
||||||
|
On Macintosh and Linux, just use the normal text select and copy commands.
|
||||||
|
|
||||||
|
Paste the information into a comment at my blog, http://apprenticealf.wordpress.com/ describing your problem.
|
||||||
|
|
||||||
|
|
||||||
|
Linux and Adobe Digital Editions PDFs
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
Here are the instructions for using the tools with ePub books and Adobe Digital Editions on Linux under Wine. (Thank you mclien!)
|
||||||
|
|
||||||
|
|
||||||
|
1. download the most recent version of wine from winehq.org (1.3.29 in my case)
|
||||||
|
|
||||||
|
For debian users:
|
||||||
|
|
||||||
|
to get a recent version of wine I decited to use aptosid (2011-02, xfce)
|
||||||
|
(because I’m used to debian)
|
||||||
|
install aptosid and upgrade it (see aptosid site for detaild instructions)
|
||||||
|
|
||||||
|
|
||||||
|
2. properly install Wine (see the Wine site for details)
|
||||||
|
|
||||||
|
For debian users:
|
||||||
|
|
||||||
|
cd to this dir and install the packages as root:
|
||||||
|
‘dpkg -i *.deb’
|
||||||
|
you will get some error messages, which can be ignored.
|
||||||
|
again as root use
|
||||||
|
‘apt-get -f install’ to correct this errors
|
||||||
|
|
||||||
|
3. python 2.7 should already be installed on your system but you may need the following additional python package
|
||||||
|
|
||||||
|
'apt-get install python-tk’
|
||||||
|
|
||||||
|
4. all programms need to be installed as normal user. All these programm are installed the same way:
|
||||||
|
‘wine ‘
|
||||||
|
we need:
|
||||||
|
a) Adobe Digital Edition 1.7.2(from: http://kb2.adobe.com/cps/403/kb403051.html)
|
||||||
|
(there is a “can’t install ADE” site, where the setup.exe hides)
|
||||||
|
|
||||||
|
b) ActivePython-2.7.2.5-win32-x86.msi (from: http://www.activestate.com/activepython/downloads)
|
||||||
|
|
||||||
|
c) Win32OpenSSL_Light-0_9_8r.exe (from: http://www.slproweb.com/)
|
||||||
|
|
||||||
|
d) pycrypto-2.3.win32-py2.7.msi (from: http://www.voidspace.org.uk/python/modules.shtml)
|
||||||
|
|
||||||
|
5. now get and unpack the very latest tools_vX.X (from Apprentice Alf) in the users drive_c of wine
|
||||||
|
(~/.wine/drive_c/)
|
||||||
|
|
||||||
|
6. start ADE with:
|
||||||
|
‘wine digitaleditions.exe’ or from the start menue wine-adobe-digital..
|
||||||
|
|
||||||
|
7. register this instance of ADE with your adobeID and close it
|
||||||
|
change to the tools_vX.X dir:
|
||||||
|
cd ~/.wine/drive_c/tools_vX.X/Other_Tools/
|
||||||
|
|
||||||
|
8. create the adeptkey.der with:
|
||||||
|
‘wine python ineptkey.py’ (only need once!)
|
||||||
|
(key will be here: ~/.wine/drive_c/tools_vX.X/Other_Tools/adeptkey.der)
|
||||||
|
|
||||||
|
9. Use ADE running under Wine to dowload all of your purchased ePub ebooks
|
||||||
|
|
||||||
|
10. install the ineptepub and ineptpdf plugins from the tools as discribed in the readmes.
|
||||||
|
|
||||||
|
11. copy the adeptkey.der into the config dir of calibre (~/.config/calibre in debian). Your ADE books imported to calibre will automatically be freed from DRM.
|
||||||
|
|
||||||
122
Calibre_Plugins/K4MobiDeDRM ReadMe.txt
Normal file
122
Calibre_Plugins/K4MobiDeDRM ReadMe.txt
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
Kindle and Mobipocket Plugin - K4MobiDeDRM_v04.10_plugin.zip
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
Credit given to The Dark Reverser for the original standalone script. Credit also to the many people who have updated and expanded that script since then.
|
||||||
|
|
||||||
|
Plugin for K4PC, K4Mac, eInk Kindles and Mobipocket.
|
||||||
|
|
||||||
|
This plugin supersedes MobiDeDRM, K4DeDRM, and K4PCDeDRM and K4X plugins. If you install this plugin, those plugins should be removed, as should any earlier versions of this plugin.
|
||||||
|
|
||||||
|
This plugin is meant to remove the DRM from .prc, .mobi, .azw, .azw1, .azw3, .azw4 and .tpz ebooks. Calibre can then convert them to whatever format you desire. It is meant to function without having to install any dependencies except for Calibre being on your same machine and in the same account as your "Kindle for PC" or "Kindle for Mac" application if you are going to remove the DRM from books from those programs.
|
||||||
|
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior" to go to Calibre's Preferences page. Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (K4MobiDeDRM_v04.10_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog.
|
||||||
|
|
||||||
|
Make sure that you delete any old versions of the plugin. They might interfere with the operation of the new one.
|
||||||
|
|
||||||
|
|
||||||
|
Customization
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Highlight the plugin (K4MobiDeDRM under the "File type plugins" category) and click the "Customize Plugin" button on Calibre's Preferences->Plugins page.
|
||||||
|
|
||||||
|
If you have an eInk Kindle enter the 16 character serial number (these all begin a "B" or a "9") in the serial numbers field. The easiest way to make sure that you have the serial number right is to copy it from your Amazon account pages (the "Manage Your Devices" page). If you have more than one eInk Kindle, you can enter multiple serial numbers separated by commas.
|
||||||
|
|
||||||
|
If you have Mobipocket books, enter your 8 or 10 digit PID in the Mobipocket PIDs field. If you have more than one PID, separate them with commas.
|
||||||
|
|
||||||
|
These configuration steps are not needed if you only want to decode "Kindle for PC" or "Kindle for Mac" books.
|
||||||
|
|
||||||
|
|
||||||
|
Troubleshooting
|
||||||
|
---------------
|
||||||
|
|
||||||
|
If you find that it's not working for you (imported ebooks still have DRM), you can save a lot of time and trouble by first deleting the DRMed ebook from calibre and then trying to add the ebook 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. ;)
|
||||||
|
|
||||||
|
On Macintosh only you must first run calibre, open Preferences, open Miscellaneous, and click on the “Install command line tools” button. (On Windows and Linux the command line tools are installed automatically.)
|
||||||
|
|
||||||
|
On Windows, open a terminal/command window. (Start/Run… and then type 'cmd' (without the 's) as the program to run).
|
||||||
|
On Macintosh, open the Terminal application (in your Utilities folder).
|
||||||
|
On Linux open a command window. Hopefully all Linux users know how to do this, as I do not.
|
||||||
|
|
||||||
|
You should now have a text-based command-line window open. Also have open the folder containing the ebook to be imported. Make sure that book isn’t already in calibre, and that calibre isn’t running.
|
||||||
|
|
||||||
|
Now type in "calibredb add " (without the " but don’t miss that final space) and now drag the book to be imported onto the window. The full path to the book should be inserted into the command line. Now press the return/enter key. The import routines will run and produce some logging information.
|
||||||
|
|
||||||
|
Now copy the output from the terminal window.
|
||||||
|
On Windows, you must use the window menu (little icon at left of window bar) to select all the text and then to copy it.
|
||||||
|
On Macintosh and Linux, just use the normal text select and copy commands.
|
||||||
|
|
||||||
|
Paste the information into a comment at my blog, http://apprenticealf.wordpress.com/ describing your problem.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Linux Systems Only
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
If you install Kindle for PC in Wine, the plugin should be able to decode files from that Kindle for PC installation under Wine. You might need to enter a Wine Prefix if it's not already set in your Environment variables. You will need to install Python and PyCrypto under Wine as detailed below. In addition, some people who have successfully used the plugin in this way have commented as follows:
|
||||||
|
|
||||||
|
Here are the instructions for using Kindle for PC on Linux under Wine. (Thank you Eyeless and Pete)
|
||||||
|
|
||||||
|
1. upgrade to very recent versions of Wine; This has been tested with Wine 1.3.15 – 1.3.2X. It may work with earlier versions but no promises. It does not work with wine 1.2.X versions.
|
||||||
|
|
||||||
|
If you have not already installed Kindle for PC under wine, follow steps 2 and 3 otherwise jump to step 4
|
||||||
|
|
||||||
|
2. Some versions of winecfg have a bug in setting the volume serial number, so create a .windows-serial file at root of drive_c to set a proper windows volume serial number (8 digit hex value for unsigned integer).
|
||||||
|
cd ~
|
||||||
|
cd .wine
|
||||||
|
cd drive_c
|
||||||
|
echo deadbeef > .windows-serial
|
||||||
|
|
||||||
|
Replace "deadbeef" with whatever hex value you want but I would stay away from the default setting of "ffffffff" which does not seem to work. BTW: deadbeef is itself a valid possible hex value if you want to use it
|
||||||
|
|
||||||
|
3. Only ***after*** setting the volume serial number properly – download and install under wine K4PC version for Windows. Register it and download from your Archive one of your Kindle ebooks. Versions known to work are K4PC 1.7.1 and earlier. Later version may work but no promises.
|
||||||
|
|
||||||
|
|
||||||
|
FIRST user
|
||||||
|
----------
|
||||||
|
Hi everyone, I struggled to get this working on Ubuntu 12.04. Here are the secrets for everyone:
|
||||||
|
|
||||||
|
1. Make sure your Wine installation is set up to be 32 bit. 64 bit is not going to work! To do this, remove your .wine directory (or use a different wineprefix). Then use WINEARCH=win32 winecfg
|
||||||
|
|
||||||
|
2. But wait, you can’t install Kindle yet. It won’t work. You need to do: winetricks -q vcrun2008 or else you’ll get an error: unimplemented function msvcp90.dll .
|
||||||
|
|
||||||
|
3. Now download and install Kindle for PC and download your content as normal.
|
||||||
|
|
||||||
|
4. Now download and install Python 2.7 32 bit for Windows from python.org, 32 bit, install it the usual way, and you can now run the Kindle DRM tools.
|
||||||
|
|
||||||
|
SECOND USER
|
||||||
|
-----------
|
||||||
|
It took a while to figure out that I needed wine 32 bit, plus Python 27 32 bit, plus the winetricks, to get all this working together but once it’s done, it’s great and I can read my Kindle content on my Nook Color running Cyanogenmod!!!
|
||||||
|
Linux Systems Only:
|
||||||
|
For all of the following wine installs, use WINEARCH=win32 if you are on x86_64. Also remember that in order to execute a *.msi file, you have to run ‘WINEARCH=win32 wine msiexec /i xxxxx.msi’.
|
||||||
|
1. Install Kindle for PC with wine.
|
||||||
|
2. Install ActivePython 2.7.x (Windows x86) with wine from here: http://www.activestate.com/activepython/downloads
|
||||||
|
3. Install the pycrypto (Windows 32 bit for Python 2.7) module with wine from here: http://www.voidspace.org.uk/python/modules.shtml#pycrypto
|
||||||
|
4. Install the K4MobiDeDRM plugin into your _Linux_ Calibre installation
|
||||||
|
Now all Kindle books downloaded from Kindle for PC in Wine will be automatically de-DRM’d when they are added to your _Linux_ Calibre. As always, you can troubleshoot problems by adding a book from the terminal using ‘calibredb add xxxx’.
|
||||||
|
|
||||||
|
Or something like that! Hope that helps someone out.
|
||||||
|
|
||||||
|
|
||||||
|
Installing Python on Windows
|
||||||
|
----------------------------
|
||||||
|
I strongly recommend fully installing ActiveState’s Active Python, free Community Edition for Windows (x86) 32 bits. This is a free, full version of the Python. It comes with some important additional modules that are not included in the bare-bones version from www.python.org unless you choose to install everything.
|
||||||
|
|
||||||
|
1. Download ActivePython 2.7.X for Windows (x86) (or later 2.7 version for Windows (x86) ) from http://www.activestate.com/activepython/downloads. Do not download the ActivePython 2.7.X for Windows (64-bit, x64) verson, even if you are running 64-bit Windows.
|
||||||
|
|
||||||
|
2. When it has finished downloading, run the installer. Accept the default options.
|
||||||
|
|
||||||
|
|
||||||
|
Installing PyCrypto on Windows
|
||||||
|
------------------------------
|
||||||
|
PyCrypto is a set of encryption/decryption routines that work with Python. The sources are freely available, and compiled versions are available from several sources. You must install a version that is for 32-bit Windows and Python 2.7. I recommend the installer linked from Michael Foord’s blog.
|
||||||
|
|
||||||
|
1. Download PyCrypto 2.1 for 32bit Windows and Python 2.7 from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
|
||||||
|
|
||||||
|
2. When it has finished downloading, unzip it. This will produce a file “pycrypto-2.1.0.win32-py2.7.exe”.
|
||||||
|
|
||||||
|
3. Double-click “pycrypto-2.1.0.win32-py2.7.exe” to run it. Accept the default options.
|
||||||
|
|
||||||
268
Calibre_Plugins/K4MobiDeDRM_plugin/__init__.py
Normal file
268
Calibre_Plugins/K4MobiDeDRM_plugin/__init__.py
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
"""
|
||||||
|
Decrypt Amazon Kindle and Mobipocket encrypted ebooks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
PLUGIN_NAME = u"Kindle and Mobipocket DeDRM"
|
||||||
|
PLUGIN_VERSION_TUPLE = (0, 4, 13)
|
||||||
|
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(k4mobidedrm.cleanup_name(k4mobidedrm.unescape(book.getBookTitle()))+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
|
||||||
568
Calibre_Plugins/K4MobiDeDRM_plugin/aescbc.py
Normal file
568
Calibre_Plugins/K4MobiDeDRM_plugin/aescbc.py
Normal file
@@ -0,0 +1,568 @@
|
|||||||
|
#! /usr/bin/env python
|
||||||
|
|
||||||
|
"""
|
||||||
|
Routines for doing AES CBC in one file
|
||||||
|
|
||||||
|
Modified by some_updates to extract
|
||||||
|
and combine only those parts needed for AES CBC
|
||||||
|
into one simple to add python file
|
||||||
|
|
||||||
|
Original Version
|
||||||
|
Copyright (c) 2002 by Paul A. Lambert
|
||||||
|
Under:
|
||||||
|
CryptoPy Artisitic License Version 1.0
|
||||||
|
See the wonderful pure python package cryptopy-1.2.5
|
||||||
|
and read its LICENSE.txt for complete license details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class CryptoError(Exception):
|
||||||
|
""" Base class for crypto exceptions """
|
||||||
|
def __init__(self,errorMessage='Error!'):
|
||||||
|
self.message = errorMessage
|
||||||
|
def __str__(self):
|
||||||
|
return self.message
|
||||||
|
|
||||||
|
class InitCryptoError(CryptoError):
|
||||||
|
""" Crypto errors during algorithm initialization """
|
||||||
|
class BadKeySizeError(InitCryptoError):
|
||||||
|
""" Bad key size error """
|
||||||
|
class EncryptError(CryptoError):
|
||||||
|
""" Error in encryption processing """
|
||||||
|
class DecryptError(CryptoError):
|
||||||
|
""" Error in decryption processing """
|
||||||
|
class DecryptNotBlockAlignedError(DecryptError):
|
||||||
|
""" Error in decryption processing """
|
||||||
|
|
||||||
|
def xorS(a,b):
|
||||||
|
""" XOR two strings """
|
||||||
|
assert len(a)==len(b)
|
||||||
|
x = []
|
||||||
|
for i in range(len(a)):
|
||||||
|
x.append( chr(ord(a[i])^ord(b[i])))
|
||||||
|
return ''.join(x)
|
||||||
|
|
||||||
|
def xor(a,b):
|
||||||
|
""" XOR two strings """
|
||||||
|
x = []
|
||||||
|
for i in range(min(len(a),len(b))):
|
||||||
|
x.append( chr(ord(a[i])^ord(b[i])))
|
||||||
|
return ''.join(x)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Base 'BlockCipher' and Pad classes for cipher instances.
|
||||||
|
BlockCipher supports automatic padding and type conversion. The BlockCipher
|
||||||
|
class was written to make the actual algorithm code more readable and
|
||||||
|
not for performance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class BlockCipher:
|
||||||
|
""" Block ciphers """
|
||||||
|
def __init__(self):
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.resetEncrypt()
|
||||||
|
self.resetDecrypt()
|
||||||
|
def resetEncrypt(self):
|
||||||
|
self.encryptBlockCount = 0
|
||||||
|
self.bytesToEncrypt = ''
|
||||||
|
def resetDecrypt(self):
|
||||||
|
self.decryptBlockCount = 0
|
||||||
|
self.bytesToDecrypt = ''
|
||||||
|
|
||||||
|
def encrypt(self, plainText, more = None):
|
||||||
|
""" Encrypt a string and return a binary string """
|
||||||
|
self.bytesToEncrypt += plainText # append plainText to any bytes from prior encrypt
|
||||||
|
numBlocks, numExtraBytes = divmod(len(self.bytesToEncrypt), self.blockSize)
|
||||||
|
cipherText = ''
|
||||||
|
for i in range(numBlocks):
|
||||||
|
bStart = i*self.blockSize
|
||||||
|
ctBlock = self.encryptBlock(self.bytesToEncrypt[bStart:bStart+self.blockSize])
|
||||||
|
self.encryptBlockCount += 1
|
||||||
|
cipherText += ctBlock
|
||||||
|
if numExtraBytes > 0: # save any bytes that are not block aligned
|
||||||
|
self.bytesToEncrypt = self.bytesToEncrypt[-numExtraBytes:]
|
||||||
|
else:
|
||||||
|
self.bytesToEncrypt = ''
|
||||||
|
|
||||||
|
if more == None: # no more data expected from caller
|
||||||
|
finalBytes = self.padding.addPad(self.bytesToEncrypt,self.blockSize)
|
||||||
|
if len(finalBytes) > 0:
|
||||||
|
ctBlock = self.encryptBlock(finalBytes)
|
||||||
|
self.encryptBlockCount += 1
|
||||||
|
cipherText += ctBlock
|
||||||
|
self.resetEncrypt()
|
||||||
|
return cipherText
|
||||||
|
|
||||||
|
def decrypt(self, cipherText, more = None):
|
||||||
|
""" Decrypt a string and return a string """
|
||||||
|
self.bytesToDecrypt += cipherText # append to any bytes from prior decrypt
|
||||||
|
|
||||||
|
numBlocks, numExtraBytes = divmod(len(self.bytesToDecrypt), self.blockSize)
|
||||||
|
if more == None: # no more calls to decrypt, should have all the data
|
||||||
|
if numExtraBytes != 0:
|
||||||
|
raise DecryptNotBlockAlignedError, 'Data not block aligned on decrypt'
|
||||||
|
|
||||||
|
# hold back some bytes in case last decrypt has zero len
|
||||||
|
if (more != None) and (numExtraBytes == 0) and (numBlocks >0) :
|
||||||
|
numBlocks -= 1
|
||||||
|
numExtraBytes = self.blockSize
|
||||||
|
|
||||||
|
plainText = ''
|
||||||
|
for i in range(numBlocks):
|
||||||
|
bStart = i*self.blockSize
|
||||||
|
ptBlock = self.decryptBlock(self.bytesToDecrypt[bStart : bStart+self.blockSize])
|
||||||
|
self.decryptBlockCount += 1
|
||||||
|
plainText += ptBlock
|
||||||
|
|
||||||
|
if numExtraBytes > 0: # save any bytes that are not block aligned
|
||||||
|
self.bytesToEncrypt = self.bytesToEncrypt[-numExtraBytes:]
|
||||||
|
else:
|
||||||
|
self.bytesToEncrypt = ''
|
||||||
|
|
||||||
|
if more == None: # last decrypt remove padding
|
||||||
|
plainText = self.padding.removePad(plainText, self.blockSize)
|
||||||
|
self.resetDecrypt()
|
||||||
|
return plainText
|
||||||
|
|
||||||
|
|
||||||
|
class Pad:
|
||||||
|
def __init__(self):
|
||||||
|
pass # eventually could put in calculation of min and max size extension
|
||||||
|
|
||||||
|
class padWithPadLen(Pad):
|
||||||
|
""" Pad a binary string with the length of the padding """
|
||||||
|
|
||||||
|
def addPad(self, extraBytes, blockSize):
|
||||||
|
""" Add padding to a binary string to make it an even multiple
|
||||||
|
of the block size """
|
||||||
|
blocks, numExtraBytes = divmod(len(extraBytes), blockSize)
|
||||||
|
padLength = blockSize - numExtraBytes
|
||||||
|
return extraBytes + padLength*chr(padLength)
|
||||||
|
|
||||||
|
def removePad(self, paddedBinaryString, blockSize):
|
||||||
|
""" Remove padding from a binary string """
|
||||||
|
if not(0<len(paddedBinaryString)):
|
||||||
|
raise DecryptNotBlockAlignedError, 'Expected More Data'
|
||||||
|
return paddedBinaryString[:-ord(paddedBinaryString[-1])]
|
||||||
|
|
||||||
|
class noPadding(Pad):
|
||||||
|
""" No padding. Use this to get ECB behavior from encrypt/decrypt """
|
||||||
|
|
||||||
|
def addPad(self, extraBytes, blockSize):
|
||||||
|
""" Add no padding """
|
||||||
|
return extraBytes
|
||||||
|
|
||||||
|
def removePad(self, paddedBinaryString, blockSize):
|
||||||
|
""" Remove no padding """
|
||||||
|
return paddedBinaryString
|
||||||
|
|
||||||
|
"""
|
||||||
|
Rijndael encryption algorithm
|
||||||
|
This byte oriented implementation is intended to closely
|
||||||
|
match FIPS specification for readability. It is not implemented
|
||||||
|
for performance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Rijndael(BlockCipher):
|
||||||
|
""" Rijndael encryption algorithm """
|
||||||
|
def __init__(self, key = None, padding = padWithPadLen(), keySize=16, blockSize=16 ):
|
||||||
|
self.name = 'RIJNDAEL'
|
||||||
|
self.keySize = keySize
|
||||||
|
self.strength = keySize*8
|
||||||
|
self.blockSize = blockSize # blockSize is in bytes
|
||||||
|
self.padding = padding # change default to noPadding() to get normal ECB behavior
|
||||||
|
|
||||||
|
assert( keySize%4==0 and NrTable[4].has_key(keySize/4)),'key size must be 16,20,24,29 or 32 bytes'
|
||||||
|
assert( blockSize%4==0 and NrTable.has_key(blockSize/4)), 'block size must be 16,20,24,29 or 32 bytes'
|
||||||
|
|
||||||
|
self.Nb = self.blockSize/4 # Nb is number of columns of 32 bit words
|
||||||
|
self.Nk = keySize/4 # Nk is the key length in 32-bit words
|
||||||
|
self.Nr = NrTable[self.Nb][self.Nk] # The number of rounds (Nr) is a function of
|
||||||
|
# the block (Nb) and key (Nk) sizes.
|
||||||
|
if key != None:
|
||||||
|
self.setKey(key)
|
||||||
|
|
||||||
|
def setKey(self, key):
|
||||||
|
""" Set a key and generate the expanded key """
|
||||||
|
assert( len(key) == (self.Nk*4) ), 'Key length must be same as keySize parameter'
|
||||||
|
self.__expandedKey = keyExpansion(self, key)
|
||||||
|
self.reset() # BlockCipher.reset()
|
||||||
|
|
||||||
|
def encryptBlock(self, plainTextBlock):
|
||||||
|
""" Encrypt a block, plainTextBlock must be a array of bytes [Nb by 4] """
|
||||||
|
self.state = self._toBlock(plainTextBlock)
|
||||||
|
AddRoundKey(self, self.__expandedKey[0:self.Nb])
|
||||||
|
for round in range(1,self.Nr): #for round = 1 step 1 to Nr
|
||||||
|
SubBytes(self)
|
||||||
|
ShiftRows(self)
|
||||||
|
MixColumns(self)
|
||||||
|
AddRoundKey(self, self.__expandedKey[round*self.Nb:(round+1)*self.Nb])
|
||||||
|
SubBytes(self)
|
||||||
|
ShiftRows(self)
|
||||||
|
AddRoundKey(self, self.__expandedKey[self.Nr*self.Nb:(self.Nr+1)*self.Nb])
|
||||||
|
return self._toBString(self.state)
|
||||||
|
|
||||||
|
|
||||||
|
def decryptBlock(self, encryptedBlock):
|
||||||
|
""" decrypt a block (array of bytes) """
|
||||||
|
self.state = self._toBlock(encryptedBlock)
|
||||||
|
AddRoundKey(self, self.__expandedKey[self.Nr*self.Nb:(self.Nr+1)*self.Nb])
|
||||||
|
for round in range(self.Nr-1,0,-1):
|
||||||
|
InvShiftRows(self)
|
||||||
|
InvSubBytes(self)
|
||||||
|
AddRoundKey(self, self.__expandedKey[round*self.Nb:(round+1)*self.Nb])
|
||||||
|
InvMixColumns(self)
|
||||||
|
InvShiftRows(self)
|
||||||
|
InvSubBytes(self)
|
||||||
|
AddRoundKey(self, self.__expandedKey[0:self.Nb])
|
||||||
|
return self._toBString(self.state)
|
||||||
|
|
||||||
|
def _toBlock(self, bs):
|
||||||
|
""" Convert binary string to array of bytes, state[col][row]"""
|
||||||
|
assert ( len(bs) == 4*self.Nb ), 'Rijndarl blocks must be of size blockSize'
|
||||||
|
return [[ord(bs[4*i]),ord(bs[4*i+1]),ord(bs[4*i+2]),ord(bs[4*i+3])] for i in range(self.Nb)]
|
||||||
|
|
||||||
|
def _toBString(self, block):
|
||||||
|
""" Convert block (array of bytes) to binary string """
|
||||||
|
l = []
|
||||||
|
for col in block:
|
||||||
|
for rowElement in col:
|
||||||
|
l.append(chr(rowElement))
|
||||||
|
return ''.join(l)
|
||||||
|
#-------------------------------------
|
||||||
|
""" Number of rounds Nr = NrTable[Nb][Nk]
|
||||||
|
|
||||||
|
Nb Nk=4 Nk=5 Nk=6 Nk=7 Nk=8
|
||||||
|
------------------------------------- """
|
||||||
|
NrTable = {4: {4:10, 5:11, 6:12, 7:13, 8:14},
|
||||||
|
5: {4:11, 5:11, 6:12, 7:13, 8:14},
|
||||||
|
6: {4:12, 5:12, 6:12, 7:13, 8:14},
|
||||||
|
7: {4:13, 5:13, 6:13, 7:13, 8:14},
|
||||||
|
8: {4:14, 5:14, 6:14, 7:14, 8:14}}
|
||||||
|
#-------------------------------------
|
||||||
|
def keyExpansion(algInstance, keyString):
|
||||||
|
""" Expand a string of size keySize into a larger array """
|
||||||
|
Nk, Nb, Nr = algInstance.Nk, algInstance.Nb, algInstance.Nr # for readability
|
||||||
|
key = [ord(byte) for byte in keyString] # convert string to list
|
||||||
|
w = [[key[4*i],key[4*i+1],key[4*i+2],key[4*i+3]] for i in range(Nk)]
|
||||||
|
for i in range(Nk,Nb*(Nr+1)):
|
||||||
|
temp = w[i-1] # a four byte column
|
||||||
|
if (i%Nk) == 0 :
|
||||||
|
temp = temp[1:]+[temp[0]] # RotWord(temp)
|
||||||
|
temp = [ Sbox[byte] for byte in temp ]
|
||||||
|
temp[0] ^= Rcon[i/Nk]
|
||||||
|
elif Nk > 6 and i%Nk == 4 :
|
||||||
|
temp = [ Sbox[byte] for byte in temp ] # SubWord(temp)
|
||||||
|
w.append( [ w[i-Nk][byte]^temp[byte] for byte in range(4) ] )
|
||||||
|
return w
|
||||||
|
|
||||||
|
Rcon = (0,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36, # note extra '0' !!!
|
||||||
|
0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6,
|
||||||
|
0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91)
|
||||||
|
|
||||||
|
#-------------------------------------
|
||||||
|
def AddRoundKey(algInstance, keyBlock):
|
||||||
|
""" XOR the algorithm state with a block of key material """
|
||||||
|
for column in range(algInstance.Nb):
|
||||||
|
for row in range(4):
|
||||||
|
algInstance.state[column][row] ^= keyBlock[column][row]
|
||||||
|
#-------------------------------------
|
||||||
|
|
||||||
|
def SubBytes(algInstance):
|
||||||
|
for column in range(algInstance.Nb):
|
||||||
|
for row in range(4):
|
||||||
|
algInstance.state[column][row] = Sbox[algInstance.state[column][row]]
|
||||||
|
|
||||||
|
def InvSubBytes(algInstance):
|
||||||
|
for column in range(algInstance.Nb):
|
||||||
|
for row in range(4):
|
||||||
|
algInstance.state[column][row] = InvSbox[algInstance.state[column][row]]
|
||||||
|
|
||||||
|
Sbox = (0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,
|
||||||
|
0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
|
||||||
|
0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,
|
||||||
|
0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
|
||||||
|
0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,
|
||||||
|
0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
|
||||||
|
0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,
|
||||||
|
0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
|
||||||
|
0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,
|
||||||
|
0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
|
||||||
|
0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,
|
||||||
|
0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
|
||||||
|
0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,
|
||||||
|
0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
|
||||||
|
0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,
|
||||||
|
0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
|
||||||
|
0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,
|
||||||
|
0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
|
||||||
|
0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,
|
||||||
|
0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
|
||||||
|
0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,
|
||||||
|
0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
|
||||||
|
0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,
|
||||||
|
0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
|
||||||
|
0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,
|
||||||
|
0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
|
||||||
|
0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,
|
||||||
|
0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
|
||||||
|
0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,
|
||||||
|
0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
|
||||||
|
0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,
|
||||||
|
0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16)
|
||||||
|
|
||||||
|
InvSbox = (0x52,0x09,0x6a,0xd5,0x30,0x36,0xa5,0x38,
|
||||||
|
0xbf,0x40,0xa3,0x9e,0x81,0xf3,0xd7,0xfb,
|
||||||
|
0x7c,0xe3,0x39,0x82,0x9b,0x2f,0xff,0x87,
|
||||||
|
0x34,0x8e,0x43,0x44,0xc4,0xde,0xe9,0xcb,
|
||||||
|
0x54,0x7b,0x94,0x32,0xa6,0xc2,0x23,0x3d,
|
||||||
|
0xee,0x4c,0x95,0x0b,0x42,0xfa,0xc3,0x4e,
|
||||||
|
0x08,0x2e,0xa1,0x66,0x28,0xd9,0x24,0xb2,
|
||||||
|
0x76,0x5b,0xa2,0x49,0x6d,0x8b,0xd1,0x25,
|
||||||
|
0x72,0xf8,0xf6,0x64,0x86,0x68,0x98,0x16,
|
||||||
|
0xd4,0xa4,0x5c,0xcc,0x5d,0x65,0xb6,0x92,
|
||||||
|
0x6c,0x70,0x48,0x50,0xfd,0xed,0xb9,0xda,
|
||||||
|
0x5e,0x15,0x46,0x57,0xa7,0x8d,0x9d,0x84,
|
||||||
|
0x90,0xd8,0xab,0x00,0x8c,0xbc,0xd3,0x0a,
|
||||||
|
0xf7,0xe4,0x58,0x05,0xb8,0xb3,0x45,0x06,
|
||||||
|
0xd0,0x2c,0x1e,0x8f,0xca,0x3f,0x0f,0x02,
|
||||||
|
0xc1,0xaf,0xbd,0x03,0x01,0x13,0x8a,0x6b,
|
||||||
|
0x3a,0x91,0x11,0x41,0x4f,0x67,0xdc,0xea,
|
||||||
|
0x97,0xf2,0xcf,0xce,0xf0,0xb4,0xe6,0x73,
|
||||||
|
0x96,0xac,0x74,0x22,0xe7,0xad,0x35,0x85,
|
||||||
|
0xe2,0xf9,0x37,0xe8,0x1c,0x75,0xdf,0x6e,
|
||||||
|
0x47,0xf1,0x1a,0x71,0x1d,0x29,0xc5,0x89,
|
||||||
|
0x6f,0xb7,0x62,0x0e,0xaa,0x18,0xbe,0x1b,
|
||||||
|
0xfc,0x56,0x3e,0x4b,0xc6,0xd2,0x79,0x20,
|
||||||
|
0x9a,0xdb,0xc0,0xfe,0x78,0xcd,0x5a,0xf4,
|
||||||
|
0x1f,0xdd,0xa8,0x33,0x88,0x07,0xc7,0x31,
|
||||||
|
0xb1,0x12,0x10,0x59,0x27,0x80,0xec,0x5f,
|
||||||
|
0x60,0x51,0x7f,0xa9,0x19,0xb5,0x4a,0x0d,
|
||||||
|
0x2d,0xe5,0x7a,0x9f,0x93,0xc9,0x9c,0xef,
|
||||||
|
0xa0,0xe0,0x3b,0x4d,0xae,0x2a,0xf5,0xb0,
|
||||||
|
0xc8,0xeb,0xbb,0x3c,0x83,0x53,0x99,0x61,
|
||||||
|
0x17,0x2b,0x04,0x7e,0xba,0x77,0xd6,0x26,
|
||||||
|
0xe1,0x69,0x14,0x63,0x55,0x21,0x0c,0x7d)
|
||||||
|
|
||||||
|
#-------------------------------------
|
||||||
|
""" For each block size (Nb), the ShiftRow operation shifts row i
|
||||||
|
by the amount Ci. Note that row 0 is not shifted.
|
||||||
|
Nb C1 C2 C3
|
||||||
|
------------------- """
|
||||||
|
shiftOffset = { 4 : ( 0, 1, 2, 3),
|
||||||
|
5 : ( 0, 1, 2, 3),
|
||||||
|
6 : ( 0, 1, 2, 3),
|
||||||
|
7 : ( 0, 1, 2, 4),
|
||||||
|
8 : ( 0, 1, 3, 4) }
|
||||||
|
def ShiftRows(algInstance):
|
||||||
|
tmp = [0]*algInstance.Nb # list of size Nb
|
||||||
|
for r in range(1,4): # row 0 reamains unchanged and can be skipped
|
||||||
|
for c in range(algInstance.Nb):
|
||||||
|
tmp[c] = algInstance.state[(c+shiftOffset[algInstance.Nb][r]) % algInstance.Nb][r]
|
||||||
|
for c in range(algInstance.Nb):
|
||||||
|
algInstance.state[c][r] = tmp[c]
|
||||||
|
def InvShiftRows(algInstance):
|
||||||
|
tmp = [0]*algInstance.Nb # list of size Nb
|
||||||
|
for r in range(1,4): # row 0 reamains unchanged and can be skipped
|
||||||
|
for c in range(algInstance.Nb):
|
||||||
|
tmp[c] = algInstance.state[(c+algInstance.Nb-shiftOffset[algInstance.Nb][r]) % algInstance.Nb][r]
|
||||||
|
for c in range(algInstance.Nb):
|
||||||
|
algInstance.state[c][r] = tmp[c]
|
||||||
|
#-------------------------------------
|
||||||
|
def MixColumns(a):
|
||||||
|
Sprime = [0,0,0,0]
|
||||||
|
for j in range(a.Nb): # for each column
|
||||||
|
Sprime[0] = mul(2,a.state[j][0])^mul(3,a.state[j][1])^mul(1,a.state[j][2])^mul(1,a.state[j][3])
|
||||||
|
Sprime[1] = mul(1,a.state[j][0])^mul(2,a.state[j][1])^mul(3,a.state[j][2])^mul(1,a.state[j][3])
|
||||||
|
Sprime[2] = mul(1,a.state[j][0])^mul(1,a.state[j][1])^mul(2,a.state[j][2])^mul(3,a.state[j][3])
|
||||||
|
Sprime[3] = mul(3,a.state[j][0])^mul(1,a.state[j][1])^mul(1,a.state[j][2])^mul(2,a.state[j][3])
|
||||||
|
for i in range(4):
|
||||||
|
a.state[j][i] = Sprime[i]
|
||||||
|
|
||||||
|
def InvMixColumns(a):
|
||||||
|
""" Mix the four bytes of every column in a linear way
|
||||||
|
This is the opposite operation of Mixcolumn """
|
||||||
|
Sprime = [0,0,0,0]
|
||||||
|
for j in range(a.Nb): # for each column
|
||||||
|
Sprime[0] = mul(0x0E,a.state[j][0])^mul(0x0B,a.state[j][1])^mul(0x0D,a.state[j][2])^mul(0x09,a.state[j][3])
|
||||||
|
Sprime[1] = mul(0x09,a.state[j][0])^mul(0x0E,a.state[j][1])^mul(0x0B,a.state[j][2])^mul(0x0D,a.state[j][3])
|
||||||
|
Sprime[2] = mul(0x0D,a.state[j][0])^mul(0x09,a.state[j][1])^mul(0x0E,a.state[j][2])^mul(0x0B,a.state[j][3])
|
||||||
|
Sprime[3] = mul(0x0B,a.state[j][0])^mul(0x0D,a.state[j][1])^mul(0x09,a.state[j][2])^mul(0x0E,a.state[j][3])
|
||||||
|
for i in range(4):
|
||||||
|
a.state[j][i] = Sprime[i]
|
||||||
|
|
||||||
|
#-------------------------------------
|
||||||
|
def mul(a, b):
|
||||||
|
""" Multiply two elements of GF(2^m)
|
||||||
|
needed for MixColumn and InvMixColumn """
|
||||||
|
if (a !=0 and b!=0):
|
||||||
|
return Alogtable[(Logtable[a] + Logtable[b])%255]
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
Logtable = ( 0, 0, 25, 1, 50, 2, 26, 198, 75, 199, 27, 104, 51, 238, 223, 3,
|
||||||
|
100, 4, 224, 14, 52, 141, 129, 239, 76, 113, 8, 200, 248, 105, 28, 193,
|
||||||
|
125, 194, 29, 181, 249, 185, 39, 106, 77, 228, 166, 114, 154, 201, 9, 120,
|
||||||
|
101, 47, 138, 5, 33, 15, 225, 36, 18, 240, 130, 69, 53, 147, 218, 142,
|
||||||
|
150, 143, 219, 189, 54, 208, 206, 148, 19, 92, 210, 241, 64, 70, 131, 56,
|
||||||
|
102, 221, 253, 48, 191, 6, 139, 98, 179, 37, 226, 152, 34, 136, 145, 16,
|
||||||
|
126, 110, 72, 195, 163, 182, 30, 66, 58, 107, 40, 84, 250, 133, 61, 186,
|
||||||
|
43, 121, 10, 21, 155, 159, 94, 202, 78, 212, 172, 229, 243, 115, 167, 87,
|
||||||
|
175, 88, 168, 80, 244, 234, 214, 116, 79, 174, 233, 213, 231, 230, 173, 232,
|
||||||
|
44, 215, 117, 122, 235, 22, 11, 245, 89, 203, 95, 176, 156, 169, 81, 160,
|
||||||
|
127, 12, 246, 111, 23, 196, 73, 236, 216, 67, 31, 45, 164, 118, 123, 183,
|
||||||
|
204, 187, 62, 90, 251, 96, 177, 134, 59, 82, 161, 108, 170, 85, 41, 157,
|
||||||
|
151, 178, 135, 144, 97, 190, 220, 252, 188, 149, 207, 205, 55, 63, 91, 209,
|
||||||
|
83, 57, 132, 60, 65, 162, 109, 71, 20, 42, 158, 93, 86, 242, 211, 171,
|
||||||
|
68, 17, 146, 217, 35, 32, 46, 137, 180, 124, 184, 38, 119, 153, 227, 165,
|
||||||
|
103, 74, 237, 222, 197, 49, 254, 24, 13, 99, 140, 128, 192, 247, 112, 7)
|
||||||
|
|
||||||
|
Alogtable= ( 1, 3, 5, 15, 17, 51, 85, 255, 26, 46, 114, 150, 161, 248, 19, 53,
|
||||||
|
95, 225, 56, 72, 216, 115, 149, 164, 247, 2, 6, 10, 30, 34, 102, 170,
|
||||||
|
229, 52, 92, 228, 55, 89, 235, 38, 106, 190, 217, 112, 144, 171, 230, 49,
|
||||||
|
83, 245, 4, 12, 20, 60, 68, 204, 79, 209, 104, 184, 211, 110, 178, 205,
|
||||||
|
76, 212, 103, 169, 224, 59, 77, 215, 98, 166, 241, 8, 24, 40, 120, 136,
|
||||||
|
131, 158, 185, 208, 107, 189, 220, 127, 129, 152, 179, 206, 73, 219, 118, 154,
|
||||||
|
181, 196, 87, 249, 16, 48, 80, 240, 11, 29, 39, 105, 187, 214, 97, 163,
|
||||||
|
254, 25, 43, 125, 135, 146, 173, 236, 47, 113, 147, 174, 233, 32, 96, 160,
|
||||||
|
251, 22, 58, 78, 210, 109, 183, 194, 93, 231, 50, 86, 250, 21, 63, 65,
|
||||||
|
195, 94, 226, 61, 71, 201, 64, 192, 91, 237, 44, 116, 156, 191, 218, 117,
|
||||||
|
159, 186, 213, 100, 172, 239, 42, 126, 130, 157, 188, 223, 122, 142, 137, 128,
|
||||||
|
155, 182, 193, 88, 232, 35, 101, 175, 234, 37, 111, 177, 200, 67, 197, 84,
|
||||||
|
252, 31, 33, 99, 165, 244, 7, 9, 27, 45, 119, 153, 176, 203, 70, 202,
|
||||||
|
69, 207, 74, 222, 121, 139, 134, 145, 168, 227, 62, 66, 198, 81, 243, 14,
|
||||||
|
18, 54, 90, 238, 41, 123, 141, 140, 143, 138, 133, 148, 167, 242, 13, 23,
|
||||||
|
57, 75, 221, 124, 132, 151, 162, 253, 28, 36, 108, 180, 199, 82, 246, 1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
AES Encryption Algorithm
|
||||||
|
The AES algorithm is just Rijndael algorithm restricted to the default
|
||||||
|
blockSize of 128 bits.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class AES(Rijndael):
|
||||||
|
""" The AES algorithm is the Rijndael block cipher restricted to block
|
||||||
|
sizes of 128 bits and key sizes of 128, 192 or 256 bits
|
||||||
|
"""
|
||||||
|
def __init__(self, key = None, padding = padWithPadLen(), keySize=16):
|
||||||
|
""" Initialize AES, keySize is in bytes """
|
||||||
|
if not (keySize == 16 or keySize == 24 or keySize == 32) :
|
||||||
|
raise BadKeySizeError, 'Illegal AES key size, must be 16, 24, or 32 bytes'
|
||||||
|
|
||||||
|
Rijndael.__init__( self, key, padding=padding, keySize=keySize, blockSize=16 )
|
||||||
|
|
||||||
|
self.name = 'AES'
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
CBC mode of encryption for block ciphers.
|
||||||
|
This algorithm mode wraps any BlockCipher to make a
|
||||||
|
Cipher Block Chaining mode.
|
||||||
|
"""
|
||||||
|
from random import Random # should change to crypto.random!!!
|
||||||
|
|
||||||
|
|
||||||
|
class CBC(BlockCipher):
|
||||||
|
""" The CBC class wraps block ciphers to make cipher block chaining (CBC) mode
|
||||||
|
algorithms. The initialization (IV) is automatic if set to None. Padding
|
||||||
|
is also automatic based on the Pad class used to initialize the algorithm
|
||||||
|
"""
|
||||||
|
def __init__(self, blockCipherInstance, padding = padWithPadLen()):
|
||||||
|
""" CBC algorithms are created by initializing with a BlockCipher instance """
|
||||||
|
self.baseCipher = blockCipherInstance
|
||||||
|
self.name = self.baseCipher.name + '_CBC'
|
||||||
|
self.blockSize = self.baseCipher.blockSize
|
||||||
|
self.keySize = self.baseCipher.keySize
|
||||||
|
self.padding = padding
|
||||||
|
self.baseCipher.padding = noPadding() # baseCipher should NOT pad!!
|
||||||
|
self.r = Random() # for IV generation, currently uses
|
||||||
|
# mediocre standard distro version <----------------
|
||||||
|
import time
|
||||||
|
newSeed = time.ctime()+str(self.r) # seed with instance location
|
||||||
|
self.r.seed(newSeed) # to make unique
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
def setKey(self, key):
|
||||||
|
self.baseCipher.setKey(key)
|
||||||
|
|
||||||
|
# Overload to reset both CBC state and the wrapped baseCipher
|
||||||
|
def resetEncrypt(self):
|
||||||
|
BlockCipher.resetEncrypt(self) # reset CBC encrypt state (super class)
|
||||||
|
self.baseCipher.resetEncrypt() # reset base cipher encrypt state
|
||||||
|
|
||||||
|
def resetDecrypt(self):
|
||||||
|
BlockCipher.resetDecrypt(self) # reset CBC state (super class)
|
||||||
|
self.baseCipher.resetDecrypt() # reset base cipher decrypt state
|
||||||
|
|
||||||
|
def encrypt(self, plainText, iv=None, more=None):
|
||||||
|
""" CBC encryption - overloads baseCipher to allow optional explicit IV
|
||||||
|
when iv=None, iv is auto generated!
|
||||||
|
"""
|
||||||
|
if self.encryptBlockCount == 0:
|
||||||
|
self.iv = iv
|
||||||
|
else:
|
||||||
|
assert(iv==None), 'IV used only on first call to encrypt'
|
||||||
|
|
||||||
|
return BlockCipher.encrypt(self,plainText, more=more)
|
||||||
|
|
||||||
|
def decrypt(self, cipherText, iv=None, more=None):
|
||||||
|
""" CBC decryption - overloads baseCipher to allow optional explicit IV
|
||||||
|
when iv=None, iv is auto generated!
|
||||||
|
"""
|
||||||
|
if self.decryptBlockCount == 0:
|
||||||
|
self.iv = iv
|
||||||
|
else:
|
||||||
|
assert(iv==None), 'IV used only on first call to decrypt'
|
||||||
|
|
||||||
|
return BlockCipher.decrypt(self, cipherText, more=more)
|
||||||
|
|
||||||
|
def encryptBlock(self, plainTextBlock):
|
||||||
|
""" CBC block encryption, IV is set with 'encrypt' """
|
||||||
|
auto_IV = ''
|
||||||
|
if self.encryptBlockCount == 0:
|
||||||
|
if self.iv == None:
|
||||||
|
# generate IV and use
|
||||||
|
self.iv = ''.join([chr(self.r.randrange(256)) for i in range(self.blockSize)])
|
||||||
|
self.prior_encr_CT_block = self.iv
|
||||||
|
auto_IV = self.prior_encr_CT_block # prepend IV if it's automatic
|
||||||
|
else: # application provided IV
|
||||||
|
assert(len(self.iv) == self.blockSize ),'IV must be same length as block'
|
||||||
|
self.prior_encr_CT_block = self.iv
|
||||||
|
""" encrypt the prior CT XORed with the PT """
|
||||||
|
ct = self.baseCipher.encryptBlock( xor(self.prior_encr_CT_block, plainTextBlock) )
|
||||||
|
self.prior_encr_CT_block = ct
|
||||||
|
return auto_IV+ct
|
||||||
|
|
||||||
|
def decryptBlock(self, encryptedBlock):
|
||||||
|
""" Decrypt a single block """
|
||||||
|
|
||||||
|
if self.decryptBlockCount == 0: # first call, process IV
|
||||||
|
if self.iv == None: # auto decrypt IV?
|
||||||
|
self.prior_CT_block = encryptedBlock
|
||||||
|
return ''
|
||||||
|
else:
|
||||||
|
assert(len(self.iv)==self.blockSize),"Bad IV size on CBC decryption"
|
||||||
|
self.prior_CT_block = self.iv
|
||||||
|
|
||||||
|
dct = self.baseCipher.decryptBlock(encryptedBlock)
|
||||||
|
""" XOR the prior decrypted CT with the prior CT """
|
||||||
|
dct_XOR_priorCT = xor( self.prior_CT_block, dct )
|
||||||
|
|
||||||
|
self.prior_CT_block = encryptedBlock
|
||||||
|
|
||||||
|
return dct_XOR_priorCT
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
AES_CBC Encryption Algorithm
|
||||||
|
"""
|
||||||
|
|
||||||
|
class AES_CBC(CBC):
|
||||||
|
""" AES encryption in CBC feedback mode """
|
||||||
|
def __init__(self, key=None, padding=padWithPadLen(), keySize=16):
|
||||||
|
CBC.__init__( self, AES(key, noPadding(), keySize), padding)
|
||||||
|
self.name = 'AES_CBC'
|
||||||
BIN
Calibre_Plugins/K4MobiDeDRM_plugin/alfcrypto.dll
Normal file
BIN
Calibre_Plugins/K4MobiDeDRM_plugin/alfcrypto.dll
Normal file
Binary file not shown.
297
Calibre_Plugins/K4MobiDeDRM_plugin/alfcrypto.py
Normal file
297
Calibre_Plugins/K4MobiDeDRM_plugin/alfcrypto.py
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# crypto library mainly by some_updates
|
||||||
|
|
||||||
|
# pbkdf2.py pbkdf2 code taken from pbkdf2.py
|
||||||
|
# pbkdf2.py Copyright © 2004 Matt Johnston <matt @ ucc asn au>
|
||||||
|
# pbkdf2.py Copyright © 2009 Daniel Holth <dholth@fastmail.fm>
|
||||||
|
# pbkdf2.py This code may be freely used and modified for any purpose.
|
||||||
|
|
||||||
|
import sys, os
|
||||||
|
import hmac
|
||||||
|
from struct import pack
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
# interface to needed routines libalfcrypto
|
||||||
|
def _load_libalfcrypto():
|
||||||
|
import ctypes
|
||||||
|
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, sizeof
|
||||||
|
|
||||||
|
pointer_size = ctypes.sizeof(ctypes.c_voidp)
|
||||||
|
name_of_lib = None
|
||||||
|
if sys.platform.startswith('darwin'):
|
||||||
|
name_of_lib = 'libalfcrypto.dylib'
|
||||||
|
elif sys.platform.startswith('win'):
|
||||||
|
if pointer_size == 4:
|
||||||
|
name_of_lib = 'alfcrypto.dll'
|
||||||
|
else:
|
||||||
|
name_of_lib = 'alfcrypto64.dll'
|
||||||
|
else:
|
||||||
|
if pointer_size == 4:
|
||||||
|
name_of_lib = 'libalfcrypto32.so'
|
||||||
|
else:
|
||||||
|
name_of_lib = 'libalfcrypto64.so'
|
||||||
|
|
||||||
|
libalfcrypto = os.path.join(sys.path[0],name_of_lib)
|
||||||
|
|
||||||
|
if not os.path.isfile(libalfcrypto):
|
||||||
|
raise Exception('libalfcrypto not found')
|
||||||
|
|
||||||
|
libalfcrypto = CDLL(libalfcrypto)
|
||||||
|
|
||||||
|
c_char_pp = POINTER(c_char_p)
|
||||||
|
c_int_p = POINTER(c_int)
|
||||||
|
|
||||||
|
|
||||||
|
def F(restype, name, argtypes):
|
||||||
|
func = getattr(libalfcrypto, name)
|
||||||
|
func.restype = restype
|
||||||
|
func.argtypes = argtypes
|
||||||
|
return func
|
||||||
|
|
||||||
|
# aes cbc decryption
|
||||||
|
#
|
||||||
|
# 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);
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
|
||||||
|
class AES_KEY(Structure):
|
||||||
|
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)]
|
||||||
|
|
||||||
|
AES_KEY_p = POINTER(AES_KEY)
|
||||||
|
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])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Pukall 1 Cipher
|
||||||
|
# unsigned char *PC1(const unsigned char *key, unsigned int klen, const unsigned char *src,
|
||||||
|
# unsigned char *dest, unsigned int len, int decryption);
|
||||||
|
|
||||||
|
PC1 = F(c_char_p, 'PC1', [c_char_p, c_ulong, c_char_p, c_char_p, c_ulong, c_ulong])
|
||||||
|
|
||||||
|
# Topaz Encryption
|
||||||
|
# typedef struct _TpzCtx {
|
||||||
|
# unsigned int v[2];
|
||||||
|
# } TpzCtx;
|
||||||
|
#
|
||||||
|
# void topazCryptoInit(TpzCtx *ctx, const unsigned char *key, int klen);
|
||||||
|
# void topazCryptoDecrypt(const TpzCtx *ctx, const unsigned char *in, unsigned char *out, int len);
|
||||||
|
|
||||||
|
class TPZ_CTX(Structure):
|
||||||
|
_fields_ = [('v', c_long * 2)]
|
||||||
|
|
||||||
|
TPZ_CTX_p = POINTER(TPZ_CTX)
|
||||||
|
topazCryptoInit = F(None, 'topazCryptoInit', [TPZ_CTX_p, c_char_p, c_ulong])
|
||||||
|
topazCryptoDecrypt = F(None, 'topazCryptoDecrypt', [TPZ_CTX_p, c_char_p, c_char_p, c_ulong])
|
||||||
|
|
||||||
|
|
||||||
|
class AES_CBC(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 Exception('AES CBC improper key used')
|
||||||
|
return
|
||||||
|
keyctx = self._keyctx = AES_KEY()
|
||||||
|
self._iv = iv
|
||||||
|
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
|
||||||
|
if rv < 0:
|
||||||
|
raise Exception('Failed to initialize AES CBC key')
|
||||||
|
|
||||||
|
def decrypt(self, data):
|
||||||
|
out = create_string_buffer(len(data))
|
||||||
|
mutable_iv = create_string_buffer(self._iv, len(self._iv))
|
||||||
|
rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, mutable_iv, 0)
|
||||||
|
if rv == 0:
|
||||||
|
raise Exception('AES CBC decryption failed')
|
||||||
|
return out.raw
|
||||||
|
|
||||||
|
class Pukall_Cipher(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.key = None
|
||||||
|
|
||||||
|
def PC1(self, key, src, decryption=True):
|
||||||
|
self.key = key
|
||||||
|
out = create_string_buffer(len(src))
|
||||||
|
de = 0
|
||||||
|
if decryption:
|
||||||
|
de = 1
|
||||||
|
rv = PC1(key, len(key), src, out, len(src), de)
|
||||||
|
return out.raw
|
||||||
|
|
||||||
|
class Topaz_Cipher(object):
|
||||||
|
def __init__(self):
|
||||||
|
self._ctx = None
|
||||||
|
|
||||||
|
def ctx_init(self, key):
|
||||||
|
tpz_ctx = self._ctx = TPZ_CTX()
|
||||||
|
topazCryptoInit(tpz_ctx, key, len(key))
|
||||||
|
return tpz_ctx
|
||||||
|
|
||||||
|
def decrypt(self, data, ctx=None):
|
||||||
|
if ctx == None:
|
||||||
|
ctx = self._ctx
|
||||||
|
out = create_string_buffer(len(data))
|
||||||
|
topazCryptoDecrypt(ctx, data, out, len(data))
|
||||||
|
return out.raw
|
||||||
|
|
||||||
|
print u"Using Library AlfCrypto DLL/DYLIB/SO"
|
||||||
|
return (AES_CBC, Pukall_Cipher, Topaz_Cipher)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_python_alfcrypto():
|
||||||
|
|
||||||
|
import aescbc
|
||||||
|
|
||||||
|
class Pukall_Cipher(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.key = None
|
||||||
|
|
||||||
|
def PC1(self, key, src, decryption=True):
|
||||||
|
sum1 = 0;
|
||||||
|
sum2 = 0;
|
||||||
|
keyXorVal = 0;
|
||||||
|
if len(key)!=16:
|
||||||
|
raise Exception('Pukall_Cipher: Bad key length.')
|
||||||
|
wkey = []
|
||||||
|
for i in xrange(8):
|
||||||
|
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
|
||||||
|
dst = ""
|
||||||
|
for i in xrange(len(src)):
|
||||||
|
temp1 = 0;
|
||||||
|
byteXorVal = 0;
|
||||||
|
for j in xrange(8):
|
||||||
|
temp1 ^= wkey[j]
|
||||||
|
sum2 = (sum2+j)*20021 + sum1
|
||||||
|
sum1 = (temp1*346)&0xFFFF
|
||||||
|
sum2 = (sum2+sum1)&0xFFFF
|
||||||
|
temp1 = (temp1*20021+1)&0xFFFF
|
||||||
|
byteXorVal ^= temp1 ^ sum2
|
||||||
|
curByte = ord(src[i])
|
||||||
|
if not decryption:
|
||||||
|
keyXorVal = curByte * 257;
|
||||||
|
curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
|
||||||
|
if decryption:
|
||||||
|
keyXorVal = curByte * 257;
|
||||||
|
for j in xrange(8):
|
||||||
|
wkey[j] ^= keyXorVal;
|
||||||
|
dst+=chr(curByte)
|
||||||
|
return dst
|
||||||
|
|
||||||
|
class Topaz_Cipher(object):
|
||||||
|
def __init__(self):
|
||||||
|
self._ctx = None
|
||||||
|
|
||||||
|
def ctx_init(self, key):
|
||||||
|
ctx1 = 0x0CAFFE19E
|
||||||
|
for keyChar in key:
|
||||||
|
keyByte = ord(keyChar)
|
||||||
|
ctx2 = ctx1
|
||||||
|
ctx1 = ((((ctx1 >>2) * (ctx1 >>7))&0xFFFFFFFF) ^ (keyByte * keyByte * 0x0F902007)& 0xFFFFFFFF )
|
||||||
|
self._ctx = [ctx1, ctx2]
|
||||||
|
return [ctx1,ctx2]
|
||||||
|
|
||||||
|
def decrypt(self, data, ctx=None):
|
||||||
|
if ctx == None:
|
||||||
|
ctx = self._ctx
|
||||||
|
ctx1 = ctx[0]
|
||||||
|
ctx2 = ctx[1]
|
||||||
|
plainText = ""
|
||||||
|
for dataChar in data:
|
||||||
|
dataByte = ord(dataChar)
|
||||||
|
m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF
|
||||||
|
ctx2 = ctx1
|
||||||
|
ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF)
|
||||||
|
plainText += chr(m)
|
||||||
|
return plainText
|
||||||
|
|
||||||
|
class AES_CBC(object):
|
||||||
|
def __init__(self):
|
||||||
|
self._key = None
|
||||||
|
self._iv = None
|
||||||
|
self.aes = None
|
||||||
|
|
||||||
|
def set_decrypt_key(self, userkey, iv):
|
||||||
|
self._key = userkey
|
||||||
|
self._iv = iv
|
||||||
|
self.aes = aescbc.AES_CBC(userkey, aescbc.noPadding(), len(userkey))
|
||||||
|
|
||||||
|
def decrypt(self, data):
|
||||||
|
iv = self._iv
|
||||||
|
cleartext = self.aes.decrypt(iv + data)
|
||||||
|
return cleartext
|
||||||
|
|
||||||
|
print u"Using Library AlfCrypto Python"
|
||||||
|
return (AES_CBC, Pukall_Cipher, Topaz_Cipher)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_crypto():
|
||||||
|
AES_CBC = Pukall_Cipher = Topaz_Cipher = None
|
||||||
|
cryptolist = (_load_libalfcrypto, _load_python_alfcrypto)
|
||||||
|
for loader in cryptolist:
|
||||||
|
try:
|
||||||
|
AES_CBC, Pukall_Cipher, Topaz_Cipher = loader()
|
||||||
|
break
|
||||||
|
except (ImportError, Exception):
|
||||||
|
pass
|
||||||
|
return AES_CBC, Pukall_Cipher, Topaz_Cipher
|
||||||
|
|
||||||
|
AES_CBC, Pukall_Cipher, Topaz_Cipher = _load_crypto()
|
||||||
|
|
||||||
|
|
||||||
|
class KeyIVGen(object):
|
||||||
|
# this only exists in openssl so we will use pure python implementation instead
|
||||||
|
# PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1',
|
||||||
|
# [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p])
|
||||||
|
def pbkdf2(self, passwd, salt, iter, keylen):
|
||||||
|
|
||||||
|
def xorstr( a, b ):
|
||||||
|
if len(a) != len(b):
|
||||||
|
raise Exception("xorstr(): lengths differ")
|
||||||
|
return ''.join((chr(ord(x)^ord(y)) for x, y in zip(a, b)))
|
||||||
|
|
||||||
|
def prf( h, data ):
|
||||||
|
hm = h.copy()
|
||||||
|
hm.update( data )
|
||||||
|
return hm.digest()
|
||||||
|
|
||||||
|
def pbkdf2_F( h, salt, itercount, blocknum ):
|
||||||
|
U = prf( h, salt + pack('>i',blocknum ) )
|
||||||
|
T = U
|
||||||
|
for i in range(2, itercount+1):
|
||||||
|
U = prf( h, U )
|
||||||
|
T = xorstr( T, U )
|
||||||
|
return T
|
||||||
|
|
||||||
|
sha = hashlib.sha1
|
||||||
|
digest_size = sha().digest_size
|
||||||
|
# l - number of output blocks to produce
|
||||||
|
l = keylen / digest_size
|
||||||
|
if keylen % digest_size != 0:
|
||||||
|
l += 1
|
||||||
|
h = hmac.new( passwd, None, sha )
|
||||||
|
T = ""
|
||||||
|
for i in range(1, l+1):
|
||||||
|
T += pbkdf2_F( h, salt, iter, i )
|
||||||
|
return T[0: keylen]
|
||||||
|
|
||||||
|
|
||||||
BIN
Calibre_Plugins/K4MobiDeDRM_plugin/alfcrypto64.dll
Normal file
BIN
Calibre_Plugins/K4MobiDeDRM_plugin/alfcrypto64.dll
Normal file
Binary file not shown.
BIN
Calibre_Plugins/K4MobiDeDRM_plugin/alfcrypto_src.zip
Normal file
BIN
Calibre_Plugins/K4MobiDeDRM_plugin/alfcrypto_src.zip
Normal file
Binary file not shown.
62
Calibre_Plugins/K4MobiDeDRM_plugin/config.py
Normal file
62
Calibre_Plugins/K4MobiDeDRM_plugin/config.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#!/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
|
||||||
@@ -20,6 +20,8 @@ import getopt
|
|||||||
from struct import pack
|
from struct import pack
|
||||||
from struct import unpack
|
from struct import unpack
|
||||||
|
|
||||||
|
class TpzDRMError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
# Get a 7 bit encoded number from string. The most
|
# Get a 7 bit encoded number from string. The most
|
||||||
# significant byte comes first and has the high bit (8th) set
|
# significant byte comes first and has the high bit (8th) set
|
||||||
@@ -138,7 +140,8 @@ class Dictionary(object):
|
|||||||
return self.stable[self.pos]
|
return self.stable[self.pos]
|
||||||
else:
|
else:
|
||||||
print "Error - %d outside of string table limits" % val
|
print "Error - %d outside of string table limits" % val
|
||||||
sys.exit(-1)
|
raise TpzDRMError('outside of string table limits')
|
||||||
|
# sys.exit(-1)
|
||||||
|
|
||||||
def getSize(self):
|
def getSize(self):
|
||||||
return self.size
|
return self.size
|
||||||
@@ -211,6 +214,7 @@ class PageParser(object):
|
|||||||
'links.title' : (1, 'text', 0, 0),
|
'links.title' : (1, 'text', 0, 0),
|
||||||
'links.href' : (1, 'text', 0, 0),
|
'links.href' : (1, 'text', 0, 0),
|
||||||
'links.type' : (1, 'text', 0, 0),
|
'links.type' : (1, 'text', 0, 0),
|
||||||
|
'links.id' : (1, 'number', 0, 0),
|
||||||
|
|
||||||
'paraCont' : (0, 'number', 1, 1),
|
'paraCont' : (0, 'number', 1, 1),
|
||||||
'paraCont.rootID' : (1, 'number', 0, 0),
|
'paraCont.rootID' : (1, 'number', 0, 0),
|
||||||
@@ -226,6 +230,7 @@ class PageParser(object):
|
|||||||
'empty' : (1, 'snippets', 1, 0),
|
'empty' : (1, 'snippets', 1, 0),
|
||||||
|
|
||||||
'page' : (1, 'snippets', 1, 0),
|
'page' : (1, 'snippets', 1, 0),
|
||||||
|
'page.class' : (1, 'scalar_text', 0, 0),
|
||||||
'page.pageid' : (1, 'scalar_text', 0, 0),
|
'page.pageid' : (1, 'scalar_text', 0, 0),
|
||||||
'page.pagelabel' : (1, 'scalar_text', 0, 0),
|
'page.pagelabel' : (1, 'scalar_text', 0, 0),
|
||||||
'page.type' : (1, 'scalar_text', 0, 0),
|
'page.type' : (1, 'scalar_text', 0, 0),
|
||||||
@@ -234,14 +239,19 @@ class PageParser(object):
|
|||||||
'page.startID' : (1, 'scalar_number', 0, 0),
|
'page.startID' : (1, 'scalar_number', 0, 0),
|
||||||
|
|
||||||
'group' : (1, 'snippets', 1, 0),
|
'group' : (1, 'snippets', 1, 0),
|
||||||
|
'group.class' : (1, 'scalar_text', 0, 0),
|
||||||
'group.type' : (1, 'scalar_text', 0, 0),
|
'group.type' : (1, 'scalar_text', 0, 0),
|
||||||
|
'group._tag' : (1, 'scalar_text', 0, 0),
|
||||||
|
'group.orientation': (1, 'scalar_text', 0, 0),
|
||||||
|
|
||||||
'region' : (1, 'snippets', 1, 0),
|
'region' : (1, 'snippets', 1, 0),
|
||||||
|
'region.class' : (1, 'scalar_text', 0, 0),
|
||||||
'region.type' : (1, 'scalar_text', 0, 0),
|
'region.type' : (1, 'scalar_text', 0, 0),
|
||||||
'region.x' : (1, 'scalar_number', 0, 0),
|
'region.x' : (1, 'scalar_number', 0, 0),
|
||||||
'region.y' : (1, 'scalar_number', 0, 0),
|
'region.y' : (1, 'scalar_number', 0, 0),
|
||||||
'region.h' : (1, 'scalar_number', 0, 0),
|
'region.h' : (1, 'scalar_number', 0, 0),
|
||||||
'region.w' : (1, 'scalar_number', 0, 0),
|
'region.w' : (1, 'scalar_number', 0, 0),
|
||||||
|
'region.orientation' : (1, 'scalar_text', 0, 0),
|
||||||
|
|
||||||
'empty_text_region' : (1, 'snippets', 1, 0),
|
'empty_text_region' : (1, 'snippets', 1, 0),
|
||||||
|
|
||||||
@@ -257,9 +267,17 @@ class PageParser(object):
|
|||||||
'paragraph.class' : (1, 'scalar_text', 0, 0),
|
'paragraph.class' : (1, 'scalar_text', 0, 0),
|
||||||
'paragraph.firstWord' : (1, 'scalar_number', 0, 0),
|
'paragraph.firstWord' : (1, 'scalar_number', 0, 0),
|
||||||
'paragraph.lastWord' : (1, 'scalar_number', 0, 0),
|
'paragraph.lastWord' : (1, 'scalar_number', 0, 0),
|
||||||
|
'paragraph.lastWord' : (1, 'scalar_number', 0, 0),
|
||||||
|
'paragraph.gridSize' : (1, 'scalar_number', 0, 0),
|
||||||
|
'paragraph.gridBottomCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
'paragraph.gridTopCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
'paragraph.gridBeginCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
'paragraph.gridEndCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
|
||||||
|
|
||||||
'word_semantic' : (1, 'snippets', 1, 1),
|
'word_semantic' : (1, 'snippets', 1, 1),
|
||||||
'word_semantic.type' : (1, 'scalar_text', 0, 0),
|
'word_semantic.type' : (1, 'scalar_text', 0, 0),
|
||||||
|
'word_semantic.class' : (1, 'scalar_text', 0, 0),
|
||||||
'word_semantic.firstWord' : (1, 'scalar_number', 0, 0),
|
'word_semantic.firstWord' : (1, 'scalar_number', 0, 0),
|
||||||
'word_semantic.lastWord' : (1, 'scalar_number', 0, 0),
|
'word_semantic.lastWord' : (1, 'scalar_number', 0, 0),
|
||||||
|
|
||||||
@@ -270,12 +288,23 @@ class PageParser(object):
|
|||||||
'word.lastGlyph' : (1, 'scalar_number', 0, 0),
|
'word.lastGlyph' : (1, 'scalar_number', 0, 0),
|
||||||
|
|
||||||
'_span' : (1, 'snippets', 1, 0),
|
'_span' : (1, 'snippets', 1, 0),
|
||||||
|
'_span.class' : (1, 'scalar_text', 0, 0),
|
||||||
'_span.firstWord' : (1, 'scalar_number', 0, 0),
|
'_span.firstWord' : (1, 'scalar_number', 0, 0),
|
||||||
'-span.lastWord' : (1, 'scalar_number', 0, 0),
|
'_span.lastWord' : (1, 'scalar_number', 0, 0),
|
||||||
|
'_span.gridSize' : (1, 'scalar_number', 0, 0),
|
||||||
|
'_span.gridBottomCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
'_span.gridTopCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
'_span.gridBeginCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
'_span.gridEndCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
|
||||||
'span' : (1, 'snippets', 1, 0),
|
'span' : (1, 'snippets', 1, 0),
|
||||||
'span.firstWord' : (1, 'scalar_number', 0, 0),
|
'span.firstWord' : (1, 'scalar_number', 0, 0),
|
||||||
'span.lastWord' : (1, 'scalar_number', 0, 0),
|
'span.lastWord' : (1, 'scalar_number', 0, 0),
|
||||||
|
'span.gridSize' : (1, 'scalar_number', 0, 0),
|
||||||
|
'span.gridBottomCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
'span.gridTopCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
'span.gridBeginCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
'span.gridEndCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
|
||||||
'extratokens' : (1, 'snippets', 1, 0),
|
'extratokens' : (1, 'snippets', 1, 0),
|
||||||
'extratokens.type' : (1, 'scalar_text', 0, 0),
|
'extratokens.type' : (1, 'scalar_text', 0, 0),
|
||||||
@@ -327,7 +356,9 @@ class PageParser(object):
|
|||||||
'style' : (1, 'snippets', 1, 0),
|
'style' : (1, 'snippets', 1, 0),
|
||||||
'style._tag' : (1, 'scalar_text', 0, 0),
|
'style._tag' : (1, 'scalar_text', 0, 0),
|
||||||
'style.type' : (1, 'scalar_text', 0, 0),
|
'style.type' : (1, 'scalar_text', 0, 0),
|
||||||
|
'style._after_type' : (1, 'scalar_text', 0, 0),
|
||||||
'style._parent_type' : (1, 'scalar_text', 0, 0),
|
'style._parent_type' : (1, 'scalar_text', 0, 0),
|
||||||
|
'style._after_parent_type' : (1, 'scalar_text', 0, 0),
|
||||||
'style.class' : (1, 'scalar_text', 0, 0),
|
'style.class' : (1, 'scalar_text', 0, 0),
|
||||||
'style._after_class' : (1, 'scalar_text', 0, 0),
|
'style._after_class' : (1, 'scalar_text', 0, 0),
|
||||||
'rule' : (1, 'snippets', 1, 0),
|
'rule' : (1, 'snippets', 1, 0),
|
||||||
@@ -594,28 +625,30 @@ class PageParser(object):
|
|||||||
nodename = fullpathname.pop()
|
nodename = fullpathname.pop()
|
||||||
ilvl = len(fullpathname)
|
ilvl = len(fullpathname)
|
||||||
indent = ' ' * (3 * ilvl)
|
indent = ' ' * (3 * ilvl)
|
||||||
result = indent + '<' + nodename + '>'
|
rlst = []
|
||||||
|
rlst.append(indent + '<' + nodename + '>')
|
||||||
if len(argList) > 0:
|
if len(argList) > 0:
|
||||||
argres = ''
|
alst = []
|
||||||
for j in argList:
|
for j in argList:
|
||||||
if (argtype == 'text') or (argtype == 'scalar_text') :
|
if (argtype == 'text') or (argtype == 'scalar_text') :
|
||||||
argres += j + '|'
|
alst.append(j + '|')
|
||||||
else :
|
else :
|
||||||
argres += str(j) + ','
|
alst.append(str(j) + ',')
|
||||||
|
argres = "".join(alst)
|
||||||
argres = argres[0:-1]
|
argres = argres[0:-1]
|
||||||
if argtype == 'snippets' :
|
if argtype == 'snippets' :
|
||||||
result += 'snippets:' + argres
|
rlst.append('snippets:' + argres)
|
||||||
else :
|
else :
|
||||||
result += argres
|
rlst.append(argres)
|
||||||
if len(subtagList) > 0 :
|
if len(subtagList) > 0 :
|
||||||
result += '\n'
|
rlst.append('\n')
|
||||||
for j in subtagList:
|
for j in subtagList:
|
||||||
if len(j) > 0 :
|
if len(j) > 0 :
|
||||||
result += self.formatTag(j)
|
rlst.append(self.formatTag(j))
|
||||||
result += indent + '</' + nodename + '>\n'
|
rlst.append(indent + '</' + nodename + '>\n')
|
||||||
else:
|
else:
|
||||||
result += '</' + nodename + '>\n'
|
rlst.append('</' + nodename + '>\n')
|
||||||
return result
|
return "".join(rlst)
|
||||||
|
|
||||||
|
|
||||||
# flatten tag
|
# flatten tag
|
||||||
@@ -624,35 +657,38 @@ class PageParser(object):
|
|||||||
subtagList = node[1]
|
subtagList = node[1]
|
||||||
argtype = node[2]
|
argtype = node[2]
|
||||||
argList = node[3]
|
argList = node[3]
|
||||||
result = name
|
rlst = []
|
||||||
|
rlst.append(name)
|
||||||
if (len(argList) > 0):
|
if (len(argList) > 0):
|
||||||
argres = ''
|
alst = []
|
||||||
for j in argList:
|
for j in argList:
|
||||||
if (argtype == 'text') or (argtype == 'scalar_text') :
|
if (argtype == 'text') or (argtype == 'scalar_text') :
|
||||||
argres += j + '|'
|
alst.append(j + '|')
|
||||||
else :
|
else :
|
||||||
argres += str(j) + '|'
|
alst.append(str(j) + '|')
|
||||||
|
argres = "".join(alst)
|
||||||
argres = argres[0:-1]
|
argres = argres[0:-1]
|
||||||
if argtype == 'snippets' :
|
if argtype == 'snippets' :
|
||||||
result += '.snippets=' + argres
|
rlst.append('.snippets=' + argres)
|
||||||
else :
|
else :
|
||||||
result += '=' + argres
|
rlst.append('=' + argres)
|
||||||
result += '\n'
|
rlst.append('\n')
|
||||||
for j in subtagList:
|
for j in subtagList:
|
||||||
if len(j) > 0 :
|
if len(j) > 0 :
|
||||||
result += self.flattenTag(j)
|
rlst.append(self.flattenTag(j))
|
||||||
return result
|
return "".join(rlst)
|
||||||
|
|
||||||
|
|
||||||
# reduce create xml output
|
# reduce create xml output
|
||||||
def formatDoc(self, flat_xml):
|
def formatDoc(self, flat_xml):
|
||||||
result = ''
|
rlst = []
|
||||||
for j in self.doc :
|
for j in self.doc :
|
||||||
if len(j) > 0:
|
if len(j) > 0:
|
||||||
if flat_xml:
|
if flat_xml:
|
||||||
result += self.flattenTag(j)
|
rlst.append(self.flattenTag(j))
|
||||||
else:
|
else:
|
||||||
result += self.formatTag(j)
|
rlst.append(self.formatTag(j))
|
||||||
|
result = "".join(rlst)
|
||||||
if self.debug : print result
|
if self.debug : print result
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -730,6 +766,19 @@ class PageParser(object):
|
|||||||
return xmlpage
|
return xmlpage
|
||||||
|
|
||||||
|
|
||||||
|
def fromData(dict, fname):
|
||||||
|
flat_xml = True
|
||||||
|
debug = False
|
||||||
|
pp = PageParser(fname, dict, debug, flat_xml)
|
||||||
|
xmlpage = pp.process()
|
||||||
|
return xmlpage
|
||||||
|
|
||||||
|
def getXML(dict, fname):
|
||||||
|
flat_xml = False
|
||||||
|
debug = False
|
||||||
|
pp = PageParser(fname, dict, debug, flat_xml)
|
||||||
|
xmlpage = pp.process()
|
||||||
|
return xmlpage
|
||||||
|
|
||||||
def usage():
|
def usage():
|
||||||
print 'Usage: '
|
print 'Usage: '
|
||||||
@@ -12,15 +12,14 @@ from struct import unpack
|
|||||||
|
|
||||||
|
|
||||||
class DocParser(object):
|
class DocParser(object):
|
||||||
def __init__(self, flatxml, classlst, fileid, bookDir, fixedimage):
|
def __init__(self, flatxml, classlst, fileid, bookDir, gdict, fixedimage):
|
||||||
self.id = os.path.basename(fileid).replace('.dat','')
|
self.id = os.path.basename(fileid).replace('.dat','')
|
||||||
self.svgcount = 0
|
self.svgcount = 0
|
||||||
self.docList = flatxml.split('\n')
|
self.docList = flatxml.split('\n')
|
||||||
self.docSize = len(self.docList)
|
self.docSize = len(self.docList)
|
||||||
self.classList = {}
|
self.classList = {}
|
||||||
self.bookDir = bookDir
|
self.bookDir = bookDir
|
||||||
self.glyphPaths = { }
|
self.gdict = gdict
|
||||||
self.numPaths = 0
|
|
||||||
tmpList = classlst.split('\n')
|
tmpList = classlst.split('\n')
|
||||||
for pclass in tmpList:
|
for pclass in tmpList:
|
||||||
if pclass != '':
|
if pclass != '':
|
||||||
@@ -41,9 +40,8 @@ class DocParser(object):
|
|||||||
|
|
||||||
def getGlyph(self, gid):
|
def getGlyph(self, gid):
|
||||||
result = ''
|
result = ''
|
||||||
id='gl%d' % gid
|
id='id="gl%d"' % gid
|
||||||
return self.glyphPaths[id]
|
return self.gdict.lookup(id)
|
||||||
|
|
||||||
|
|
||||||
def glyphs_to_image(self, glyphList):
|
def glyphs_to_image(self, glyphList):
|
||||||
|
|
||||||
@@ -52,31 +50,12 @@ class DocParser(object):
|
|||||||
e = path.find(' ',b)
|
e = path.find(' ',b)
|
||||||
return int(path[b:e])
|
return int(path[b:e])
|
||||||
|
|
||||||
def extractID(path, key):
|
|
||||||
b = path.find(key) + len(key)
|
|
||||||
e = path.find('"',b)
|
|
||||||
return path[b:e]
|
|
||||||
|
|
||||||
|
|
||||||
svgDir = os.path.join(self.bookDir,'svg')
|
svgDir = os.path.join(self.bookDir,'svg')
|
||||||
glyfile = os.path.join(svgDir,'glyphs.svg')
|
|
||||||
|
|
||||||
imgDir = os.path.join(self.bookDir,'img')
|
imgDir = os.path.join(self.bookDir,'img')
|
||||||
imgname = self.id + '_%04d.svg' % self.svgcount
|
imgname = self.id + '_%04d.svg' % self.svgcount
|
||||||
imgfile = os.path.join(imgDir,imgname)
|
imgfile = os.path.join(imgDir,imgname)
|
||||||
|
|
||||||
# build hashtable of glyph paths keyed by glyph id
|
|
||||||
if self.numPaths == 0:
|
|
||||||
gfile = open(glyfile, 'r')
|
|
||||||
while True:
|
|
||||||
path = gfile.readline()
|
|
||||||
if (path == ''): break
|
|
||||||
glyphid = extractID(path,'id="')
|
|
||||||
self.glyphPaths[glyphid] = path
|
|
||||||
self.numPaths += 1
|
|
||||||
gfile.close()
|
|
||||||
|
|
||||||
|
|
||||||
# get glyph information
|
# get glyph information
|
||||||
gxList = self.getData('info.glyph.x',0,-1)
|
gxList = self.getData('info.glyph.x',0,-1)
|
||||||
gyList = self.getData('info.glyph.y',0,-1)
|
gyList = self.getData('info.glyph.y',0,-1)
|
||||||
@@ -89,7 +68,7 @@ class DocParser(object):
|
|||||||
ys = []
|
ys = []
|
||||||
gdefs = []
|
gdefs = []
|
||||||
|
|
||||||
# get path defintions, positions, dimensions for ecah glyph
|
# get path defintions, positions, dimensions for each glyph
|
||||||
# that makes up the image, and find min x and min y to reposition origin
|
# that makes up the image, and find min x and min y to reposition origin
|
||||||
minx = -1
|
minx = -1
|
||||||
miny = -1
|
miny = -1
|
||||||
@@ -292,6 +271,9 @@ class DocParser(object):
|
|||||||
|
|
||||||
pclass = self.getClass(pclass)
|
pclass = self.getClass(pclass)
|
||||||
|
|
||||||
|
# if paragraph uses extratokens (extra glyphs) then make it fixed
|
||||||
|
(pos, extraglyphs) = self.findinDoc('paragraph.extratokens',start,end)
|
||||||
|
|
||||||
# build up a description of the paragraph in result and return it
|
# build up a description of the paragraph in result and return it
|
||||||
# first check for the basic - all words paragraph
|
# first check for the basic - all words paragraph
|
||||||
(pos, sfirst) = self.findinDoc('paragraph.firstWord',start,end)
|
(pos, sfirst) = self.findinDoc('paragraph.firstWord',start,end)
|
||||||
@@ -301,6 +283,7 @@ class DocParser(object):
|
|||||||
last = int(slast)
|
last = int(slast)
|
||||||
|
|
||||||
makeImage = (regtype == 'vertical') or (regtype == 'table')
|
makeImage = (regtype == 'vertical') or (regtype == 'table')
|
||||||
|
makeImage = makeImage or (extraglyphs != None)
|
||||||
if self.fixedimage:
|
if self.fixedimage:
|
||||||
makeImage = makeImage or (regtype == 'fixed')
|
makeImage = makeImage or (regtype == 'fixed')
|
||||||
|
|
||||||
@@ -309,6 +292,11 @@ class DocParser(object):
|
|||||||
if self.fixedimage :
|
if self.fixedimage :
|
||||||
makeImage = makeImage or (pclass.find('cl-f-') >= 0)
|
makeImage = makeImage or (pclass.find('cl-f-') >= 0)
|
||||||
|
|
||||||
|
# before creating an image make sure glyph info exists
|
||||||
|
gidList = self.getData('info.glyph.glyphID',0,-1)
|
||||||
|
|
||||||
|
makeImage = makeImage & (len(gidList) > 0)
|
||||||
|
|
||||||
if not makeImage :
|
if not makeImage :
|
||||||
# standard all word paragraph
|
# standard all word paragraph
|
||||||
for wordnum in xrange(first, last):
|
for wordnum in xrange(first, last):
|
||||||
@@ -326,6 +314,15 @@ class DocParser(object):
|
|||||||
lastGlyph = firstglyphList[last]
|
lastGlyph = firstglyphList[last]
|
||||||
else :
|
else :
|
||||||
lastGlyph = len(gidList)
|
lastGlyph = len(gidList)
|
||||||
|
|
||||||
|
# handle case of white sapce paragraphs with no actual glyphs in them
|
||||||
|
# by reverting to text based paragraph
|
||||||
|
if firstGlyph >= lastGlyph:
|
||||||
|
# revert to standard text based paragraph
|
||||||
|
for wordnum in xrange(first, last):
|
||||||
|
result.append(('ocr', wordnum))
|
||||||
|
return pclass, result
|
||||||
|
|
||||||
for glyphnum in xrange(firstGlyph, lastGlyph):
|
for glyphnum in xrange(firstGlyph, lastGlyph):
|
||||||
glyphList.append(glyphnum)
|
glyphList.append(glyphnum)
|
||||||
# include any extratokens if they exist
|
# include any extratokens if they exist
|
||||||
@@ -365,6 +362,8 @@ class DocParser(object):
|
|||||||
|
|
||||||
word_class = ''
|
word_class = ''
|
||||||
|
|
||||||
|
word_semantic_type = ''
|
||||||
|
|
||||||
while (line < end) :
|
while (line < end) :
|
||||||
|
|
||||||
(name, argres) = self.lineinDoc(line)
|
(name, argres) = self.lineinDoc(line)
|
||||||
@@ -524,13 +523,80 @@ class DocParser(object):
|
|||||||
return parares
|
return parares
|
||||||
|
|
||||||
|
|
||||||
|
def buildTOCEntry(self, pdesc) :
|
||||||
|
parares = ''
|
||||||
|
sep =''
|
||||||
|
tocentry = ''
|
||||||
|
handle_links = len(self.link_id) > 0
|
||||||
|
|
||||||
|
lstart = 0
|
||||||
|
|
||||||
|
cnt = len(pdesc)
|
||||||
|
for j in xrange( 0, cnt) :
|
||||||
|
|
||||||
|
(wtype, num) = pdesc[j]
|
||||||
|
|
||||||
|
if wtype == 'ocr' :
|
||||||
|
word = self.ocrtext[num]
|
||||||
|
sep = ' '
|
||||||
|
|
||||||
|
if handle_links:
|
||||||
|
link = self.link_id[num]
|
||||||
|
if (link > 0):
|
||||||
|
linktype = self.link_type[link-1]
|
||||||
|
title = self.link_title[link-1]
|
||||||
|
title = title.rstrip('. ')
|
||||||
|
alt_title = parares[lstart:]
|
||||||
|
alt_title = alt_title.strip()
|
||||||
|
# now strip off the actual printed page number
|
||||||
|
alt_title = alt_title.rstrip('01234567890ivxldIVXLD-.')
|
||||||
|
alt_title = alt_title.rstrip('. ')
|
||||||
|
# skip over any external links - can't have them in a books toc
|
||||||
|
if linktype == 'external' :
|
||||||
|
title = ''
|
||||||
|
alt_title = ''
|
||||||
|
linkpage = ''
|
||||||
|
else :
|
||||||
|
if len(self.link_page) >= link :
|
||||||
|
ptarget = self.link_page[link-1] - 1
|
||||||
|
linkpage = '%04d' % ptarget
|
||||||
|
else :
|
||||||
|
# just link to the current page
|
||||||
|
linkpage = self.id[4:]
|
||||||
|
if len(alt_title) >= len(title):
|
||||||
|
title = alt_title
|
||||||
|
if title != '' and linkpage != '':
|
||||||
|
tocentry += title + '|' + linkpage + '\n'
|
||||||
|
lstart = len(parares)
|
||||||
|
if word == '_link_' : word = ''
|
||||||
|
elif (link < 0) :
|
||||||
|
if word == '_link_' : word = ''
|
||||||
|
|
||||||
|
if word == '_lb_':
|
||||||
|
word = ''
|
||||||
|
sep = ''
|
||||||
|
|
||||||
|
if num in self.dehyphen_rootid :
|
||||||
|
word = word[0:-1]
|
||||||
|
sep = ''
|
||||||
|
|
||||||
|
parares += word + sep
|
||||||
|
|
||||||
|
else :
|
||||||
|
continue
|
||||||
|
|
||||||
|
return tocentry
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# walk the document tree collecting the information needed
|
# walk the document tree collecting the information needed
|
||||||
# to build an html page using the ocrText
|
# to build an html page using the ocrText
|
||||||
|
|
||||||
def process(self):
|
def process(self):
|
||||||
|
|
||||||
htmlpage = ''
|
tocinfo = ''
|
||||||
|
hlst = []
|
||||||
|
|
||||||
# get the ocr text
|
# get the ocr text
|
||||||
(pos, argres) = self.findinDoc('info.word.ocrText',0,-1)
|
(pos, argres) = self.findinDoc('info.word.ocrText',0,-1)
|
||||||
@@ -587,8 +653,8 @@ class DocParser(object):
|
|||||||
|
|
||||||
# set anchor for link target on this page
|
# set anchor for link target on this page
|
||||||
if not anchorSet and not first_para_continued:
|
if not anchorSet and not first_para_continued:
|
||||||
htmlpage += '<div style="visibility: hidden; height: 0; width: 0;" id="'
|
hlst.append('<div style="visibility: hidden; height: 0; width: 0;" id="')
|
||||||
htmlpage += self.id + '" title="pagetype_' + pagetype + '"></div>\n'
|
hlst.append(self.id + '" title="pagetype_' + pagetype + '"></div>\n')
|
||||||
anchorSet = True
|
anchorSet = True
|
||||||
|
|
||||||
# handle groups of graphics with text captions
|
# handle groups of graphics with text captions
|
||||||
@@ -597,12 +663,12 @@ class DocParser(object):
|
|||||||
if grptype != None:
|
if grptype != None:
|
||||||
if grptype == 'graphic':
|
if grptype == 'graphic':
|
||||||
gcstr = ' class="' + grptype + '"'
|
gcstr = ' class="' + grptype + '"'
|
||||||
htmlpage += '<div' + gcstr + '>'
|
hlst.append('<div' + gcstr + '>')
|
||||||
inGroup = True
|
inGroup = True
|
||||||
|
|
||||||
elif (etype == 'grpend'):
|
elif (etype == 'grpend'):
|
||||||
if inGroup:
|
if inGroup:
|
||||||
htmlpage += '</div>\n'
|
hlst.append('</div>\n')
|
||||||
inGroup = False
|
inGroup = False
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -612,25 +678,25 @@ class DocParser(object):
|
|||||||
(pos, simgsrc) = self.findinDoc('img.src',start,end)
|
(pos, simgsrc) = self.findinDoc('img.src',start,end)
|
||||||
if simgsrc:
|
if simgsrc:
|
||||||
if inGroup:
|
if inGroup:
|
||||||
htmlpage += '<img src="img/img%04d.jpg" alt="" />' % int(simgsrc)
|
hlst.append('<img src="img/img%04d.jpg" alt="" />' % int(simgsrc))
|
||||||
else:
|
else:
|
||||||
htmlpage += '<div class="graphic"><img src="img/img%04d.jpg" alt="" /></div>' % int(simgsrc)
|
hlst.append('<div class="graphic"><img src="img/img%04d.jpg" alt="" /></div>' % int(simgsrc))
|
||||||
|
|
||||||
elif regtype == 'chapterheading' :
|
elif regtype == 'chapterheading' :
|
||||||
(pclass, pdesc) = self.getParaDescription(start,end, regtype)
|
(pclass, pdesc) = self.getParaDescription(start,end, regtype)
|
||||||
if not breakSet:
|
if not breakSet:
|
||||||
htmlpage += '<div style="page-break-after: always;"> </div>\n'
|
hlst.append('<div style="page-break-after: always;"> </div>\n')
|
||||||
breakSet = True
|
breakSet = True
|
||||||
tag = 'h1'
|
tag = 'h1'
|
||||||
if pclass and (len(pclass) >= 7):
|
if pclass and (len(pclass) >= 7):
|
||||||
if pclass[3:7] == 'ch1-' : tag = 'h1'
|
if pclass[3:7] == 'ch1-' : tag = 'h1'
|
||||||
if pclass[3:7] == 'ch2-' : tag = 'h2'
|
if pclass[3:7] == 'ch2-' : tag = 'h2'
|
||||||
if pclass[3:7] == 'ch3-' : tag = 'h3'
|
if pclass[3:7] == 'ch3-' : tag = 'h3'
|
||||||
htmlpage += '<' + tag + ' class="' + pclass + '">'
|
hlst.append('<' + tag + ' class="' + pclass + '">')
|
||||||
else:
|
else:
|
||||||
htmlpage += '<' + tag + '>'
|
hlst.append('<' + tag + '>')
|
||||||
htmlpage += self.buildParagraph(pclass, pdesc, 'middle', regtype)
|
hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype))
|
||||||
htmlpage += '</' + tag + '>'
|
hlst.append('</' + tag + '>')
|
||||||
|
|
||||||
elif (regtype == 'text') or (regtype == 'fixed') or (regtype == 'insert') or (regtype == 'listitem'):
|
elif (regtype == 'text') or (regtype == 'fixed') or (regtype == 'insert') or (regtype == 'listitem'):
|
||||||
ptype = 'full'
|
ptype = 'full'
|
||||||
@@ -644,11 +710,11 @@ class DocParser(object):
|
|||||||
if pclass[3:6] == 'h1-' : tag = 'h4'
|
if pclass[3:6] == 'h1-' : tag = 'h4'
|
||||||
if pclass[3:6] == 'h2-' : tag = 'h5'
|
if pclass[3:6] == 'h2-' : tag = 'h5'
|
||||||
if pclass[3:6] == 'h3-' : tag = 'h6'
|
if pclass[3:6] == 'h3-' : tag = 'h6'
|
||||||
htmlpage += '<' + tag + ' class="' + pclass + '">'
|
hlst.append('<' + tag + ' class="' + pclass + '">')
|
||||||
htmlpage += self.buildParagraph(pclass, pdesc, 'middle', regtype)
|
hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype))
|
||||||
htmlpage += '</' + tag + '>'
|
hlst.append('</' + tag + '>')
|
||||||
else :
|
else :
|
||||||
htmlpage += self.buildParagraph(pclass, pdesc, ptype, regtype)
|
hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype))
|
||||||
|
|
||||||
elif (regtype == 'tocentry') :
|
elif (regtype == 'tocentry') :
|
||||||
ptype = 'full'
|
ptype = 'full'
|
||||||
@@ -656,8 +722,8 @@ class DocParser(object):
|
|||||||
ptype = 'end'
|
ptype = 'end'
|
||||||
first_para_continued = False
|
first_para_continued = False
|
||||||
(pclass, pdesc) = self.getParaDescription(start,end, regtype)
|
(pclass, pdesc) = self.getParaDescription(start,end, regtype)
|
||||||
htmlpage += self.buildParagraph(pclass, pdesc, ptype, regtype)
|
tocinfo += self.buildTOCEntry(pdesc)
|
||||||
|
hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype))
|
||||||
|
|
||||||
elif (regtype == 'vertical') or (regtype == 'table') :
|
elif (regtype == 'vertical') or (regtype == 'table') :
|
||||||
ptype = 'full'
|
ptype = 'full'
|
||||||
@@ -667,13 +733,13 @@ class DocParser(object):
|
|||||||
ptype = 'end'
|
ptype = 'end'
|
||||||
first_para_continued = False
|
first_para_continued = False
|
||||||
(pclass, pdesc) = self.getParaDescription(start, end, regtype)
|
(pclass, pdesc) = self.getParaDescription(start, end, regtype)
|
||||||
htmlpage += self.buildParagraph(pclass, pdesc, ptype, regtype)
|
hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype))
|
||||||
|
|
||||||
|
|
||||||
elif (regtype == 'synth_fcvr.center'):
|
elif (regtype == 'synth_fcvr.center'):
|
||||||
(pos, simgsrc) = self.findinDoc('img.src',start,end)
|
(pos, simgsrc) = self.findinDoc('img.src',start,end)
|
||||||
if simgsrc:
|
if simgsrc:
|
||||||
htmlpage += '<div class="graphic"><img src="img/img%04d.jpg" alt="" /></div>' % int(simgsrc)
|
hlst.append('<div class="graphic"><img src="img/img%04d.jpg" alt="" /></div>' % int(simgsrc))
|
||||||
|
|
||||||
else :
|
else :
|
||||||
print ' Making region type', regtype,
|
print ' Making region type', regtype,
|
||||||
@@ -699,32 +765,29 @@ class DocParser(object):
|
|||||||
if pclass[3:6] == 'h1-' : tag = 'h4'
|
if pclass[3:6] == 'h1-' : tag = 'h4'
|
||||||
if pclass[3:6] == 'h2-' : tag = 'h5'
|
if pclass[3:6] == 'h2-' : tag = 'h5'
|
||||||
if pclass[3:6] == 'h3-' : tag = 'h6'
|
if pclass[3:6] == 'h3-' : tag = 'h6'
|
||||||
htmlpage += '<' + tag + ' class="' + pclass + '">'
|
hlst.append('<' + tag + ' class="' + pclass + '">')
|
||||||
htmlpage += self.buildParagraph(pclass, pdesc, 'middle', regtype)
|
hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype))
|
||||||
htmlpage += '</' + tag + '>'
|
hlst.append('</' + tag + '>')
|
||||||
else :
|
else :
|
||||||
htmlpage += self.buildParagraph(pclass, pdesc, ptype, regtype)
|
hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype))
|
||||||
else :
|
else :
|
||||||
print ' a "graphic" region'
|
print ' a "graphic" region'
|
||||||
(pos, simgsrc) = self.findinDoc('img.src',start,end)
|
(pos, simgsrc) = self.findinDoc('img.src',start,end)
|
||||||
if simgsrc:
|
if simgsrc:
|
||||||
htmlpage += '<div class="graphic"><img src="img/img%04d.jpg" alt="" /></div>' % int(simgsrc)
|
hlst.append('<div class="graphic"><img src="img/img%04d.jpg" alt="" /></div>' % int(simgsrc))
|
||||||
|
|
||||||
|
|
||||||
|
htmlpage = "".join(hlst)
|
||||||
if last_para_continued :
|
if last_para_continued :
|
||||||
if htmlpage[-4:] == '</p>':
|
if htmlpage[-4:] == '</p>':
|
||||||
htmlpage = htmlpage[0:-4]
|
htmlpage = htmlpage[0:-4]
|
||||||
last_para_continued = False
|
last_para_continued = False
|
||||||
|
|
||||||
return htmlpage
|
return htmlpage, tocinfo
|
||||||
|
|
||||||
|
|
||||||
|
def convert2HTML(flatxml, classlst, fileid, bookDir, gdict, fixedimage):
|
||||||
def convert2HTML(flatxml, classlst, fileid, bookDir, fixedimage):
|
|
||||||
|
|
||||||
# create a document parser
|
# create a document parser
|
||||||
dp = DocParser(flatxml, classlst, fileid, bookDir, fixedimage)
|
dp = DocParser(flatxml, classlst, fileid, bookDir, gdict, fixedimage)
|
||||||
|
htmlpage, tocinfo = dp.process()
|
||||||
htmlpage = dp.process()
|
return htmlpage, tocinfo
|
||||||
|
|
||||||
return htmlpage
|
|
||||||
249
Calibre_Plugins/K4MobiDeDRM_plugin/flatxml2svg.py
Normal file
249
Calibre_Plugins/K4MobiDeDRM_plugin/flatxml2svg.py
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
#! /usr/bin/python
|
||||||
|
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
import getopt
|
||||||
|
from struct import pack
|
||||||
|
from struct import unpack
|
||||||
|
|
||||||
|
|
||||||
|
class PParser(object):
|
||||||
|
def __init__(self, gd, flatxml, meta_array):
|
||||||
|
self.gd = gd
|
||||||
|
self.flatdoc = flatxml.split('\n')
|
||||||
|
self.docSize = len(self.flatdoc)
|
||||||
|
self.temp = []
|
||||||
|
|
||||||
|
self.ph = -1
|
||||||
|
self.pw = -1
|
||||||
|
startpos = self.posinDoc('page.h') or self.posinDoc('book.h')
|
||||||
|
for p in startpos:
|
||||||
|
(name, argres) = self.lineinDoc(p)
|
||||||
|
self.ph = max(self.ph, int(argres))
|
||||||
|
startpos = self.posinDoc('page.w') or self.posinDoc('book.w')
|
||||||
|
for p in startpos:
|
||||||
|
(name, argres) = self.lineinDoc(p)
|
||||||
|
self.pw = max(self.pw, int(argres))
|
||||||
|
|
||||||
|
if self.ph <= 0:
|
||||||
|
self.ph = int(meta_array.get('pageHeight', '11000'))
|
||||||
|
if self.pw <= 0:
|
||||||
|
self.pw = int(meta_array.get('pageWidth', '8500'))
|
||||||
|
|
||||||
|
res = []
|
||||||
|
startpos = self.posinDoc('info.glyph.x')
|
||||||
|
for p in startpos:
|
||||||
|
argres = self.getDataatPos('info.glyph.x', p)
|
||||||
|
res.extend(argres)
|
||||||
|
self.gx = res
|
||||||
|
|
||||||
|
res = []
|
||||||
|
startpos = self.posinDoc('info.glyph.y')
|
||||||
|
for p in startpos:
|
||||||
|
argres = self.getDataatPos('info.glyph.y', p)
|
||||||
|
res.extend(argres)
|
||||||
|
self.gy = res
|
||||||
|
|
||||||
|
res = []
|
||||||
|
startpos = self.posinDoc('info.glyph.glyphID')
|
||||||
|
for p in startpos:
|
||||||
|
argres = self.getDataatPos('info.glyph.glyphID', p)
|
||||||
|
res.extend(argres)
|
||||||
|
self.gid = res
|
||||||
|
|
||||||
|
|
||||||
|
# return tag at line pos in document
|
||||||
|
def lineinDoc(self, pos) :
|
||||||
|
if (pos >= 0) and (pos < self.docSize) :
|
||||||
|
item = self.flatdoc[pos]
|
||||||
|
if item.find('=') >= 0:
|
||||||
|
(name, argres) = item.split('=',1)
|
||||||
|
else :
|
||||||
|
name = item
|
||||||
|
argres = ''
|
||||||
|
return name, argres
|
||||||
|
|
||||||
|
# find tag in doc if within pos to end inclusive
|
||||||
|
def findinDoc(self, tagpath, pos, end) :
|
||||||
|
result = None
|
||||||
|
if end == -1 :
|
||||||
|
end = self.docSize
|
||||||
|
else:
|
||||||
|
end = min(self.docSize, end)
|
||||||
|
foundat = -1
|
||||||
|
for j in xrange(pos, end):
|
||||||
|
item = self.flatdoc[j]
|
||||||
|
if item.find('=') >= 0:
|
||||||
|
(name, argres) = item.split('=',1)
|
||||||
|
else :
|
||||||
|
name = item
|
||||||
|
argres = ''
|
||||||
|
if name.endswith(tagpath) :
|
||||||
|
result = argres
|
||||||
|
foundat = j
|
||||||
|
break
|
||||||
|
return foundat, result
|
||||||
|
|
||||||
|
# return list of start positions for the tagpath
|
||||||
|
def posinDoc(self, tagpath):
|
||||||
|
startpos = []
|
||||||
|
pos = 0
|
||||||
|
res = ""
|
||||||
|
while res != None :
|
||||||
|
(foundpos, res) = self.findinDoc(tagpath, pos, -1)
|
||||||
|
if res != None :
|
||||||
|
startpos.append(foundpos)
|
||||||
|
pos = foundpos + 1
|
||||||
|
return startpos
|
||||||
|
|
||||||
|
def getData(self, path):
|
||||||
|
result = None
|
||||||
|
cnt = len(self.flatdoc)
|
||||||
|
for j in xrange(cnt):
|
||||||
|
item = self.flatdoc[j]
|
||||||
|
if item.find('=') >= 0:
|
||||||
|
(name, argt) = item.split('=')
|
||||||
|
argres = argt.split('|')
|
||||||
|
else:
|
||||||
|
name = item
|
||||||
|
argres = []
|
||||||
|
if (name.endswith(path)):
|
||||||
|
result = argres
|
||||||
|
break
|
||||||
|
if (len(argres) > 0) :
|
||||||
|
for j in xrange(0,len(argres)):
|
||||||
|
argres[j] = int(argres[j])
|
||||||
|
return result
|
||||||
|
|
||||||
|
def getDataatPos(self, path, pos):
|
||||||
|
result = None
|
||||||
|
item = self.flatdoc[pos]
|
||||||
|
if item.find('=') >= 0:
|
||||||
|
(name, argt) = item.split('=')
|
||||||
|
argres = argt.split('|')
|
||||||
|
else:
|
||||||
|
name = item
|
||||||
|
argres = []
|
||||||
|
if (len(argres) > 0) :
|
||||||
|
for j in xrange(0,len(argres)):
|
||||||
|
argres[j] = int(argres[j])
|
||||||
|
if (name.endswith(path)):
|
||||||
|
result = argres
|
||||||
|
return result
|
||||||
|
|
||||||
|
def getDataTemp(self, path):
|
||||||
|
result = None
|
||||||
|
cnt = len(self.temp)
|
||||||
|
for j in xrange(cnt):
|
||||||
|
item = self.temp[j]
|
||||||
|
if item.find('=') >= 0:
|
||||||
|
(name, argt) = item.split('=')
|
||||||
|
argres = argt.split('|')
|
||||||
|
else:
|
||||||
|
name = item
|
||||||
|
argres = []
|
||||||
|
if (name.endswith(path)):
|
||||||
|
result = argres
|
||||||
|
self.temp.pop(j)
|
||||||
|
break
|
||||||
|
if (len(argres) > 0) :
|
||||||
|
for j in xrange(0,len(argres)):
|
||||||
|
argres[j] = int(argres[j])
|
||||||
|
return result
|
||||||
|
|
||||||
|
def getImages(self):
|
||||||
|
result = []
|
||||||
|
self.temp = self.flatdoc
|
||||||
|
while (self.getDataTemp('img') != None):
|
||||||
|
h = self.getDataTemp('img.h')[0]
|
||||||
|
w = self.getDataTemp('img.w')[0]
|
||||||
|
x = self.getDataTemp('img.x')[0]
|
||||||
|
y = self.getDataTemp('img.y')[0]
|
||||||
|
src = self.getDataTemp('img.src')[0]
|
||||||
|
result.append('<image xlink:href="../img/img%04d.jpg" x="%d" y="%d" width="%d" height="%d" />\n' % (src, x, y, w, h))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def getGlyphs(self):
|
||||||
|
result = []
|
||||||
|
if (self.gid != None) and (len(self.gid) > 0):
|
||||||
|
glyphs = []
|
||||||
|
for j in set(self.gid):
|
||||||
|
glyphs.append(j)
|
||||||
|
glyphs.sort()
|
||||||
|
for gid in glyphs:
|
||||||
|
id='id="gl%d"' % gid
|
||||||
|
path = self.gd.lookup(id)
|
||||||
|
if path:
|
||||||
|
result.append(id + ' ' + path)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def convert2SVG(gdict, flat_xml, pageid, previd, nextid, svgDir, raw, meta_array, scaledpi):
|
||||||
|
mlst = []
|
||||||
|
pp = PParser(gdict, flat_xml, meta_array)
|
||||||
|
mlst.append('<?xml version="1.0" standalone="no"?>\n')
|
||||||
|
if (raw):
|
||||||
|
mlst.append('<!DOCTYPE svg PUBLIC "-//W3C/DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
|
||||||
|
mlst.append('<svg width="%fin" height="%fin" viewBox="0 0 %d %d" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">\n' % (pp.pw / scaledpi, pp.ph / scaledpi, pp.pw -1, pp.ph -1))
|
||||||
|
mlst.append('<title>Page %d - %s by %s</title>\n' % (pageid, meta_array['Title'],meta_array['Authors']))
|
||||||
|
else:
|
||||||
|
mlst.append('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n')
|
||||||
|
mlst.append('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" ><head>\n')
|
||||||
|
mlst.append('<title>Page %d - %s by %s</title>\n' % (pageid, meta_array['Title'],meta_array['Authors']))
|
||||||
|
mlst.append('<script><![CDATA[\n')
|
||||||
|
mlst.append('function gd(){var p=window.location.href.replace(/^.*\?dpi=(\d+).*$/i,"$1");return p;}\n')
|
||||||
|
mlst.append('var dpi=%d;\n' % scaledpi)
|
||||||
|
if (previd) :
|
||||||
|
mlst.append('var prevpage="page%04d.xhtml";\n' % (previd))
|
||||||
|
if (nextid) :
|
||||||
|
mlst.append('var nextpage="page%04d.xhtml";\n' % (nextid))
|
||||||
|
mlst.append('var pw=%d;var ph=%d;' % (pp.pw, pp.ph))
|
||||||
|
mlst.append('function zoomin(){dpi=dpi*(0.8);setsize();}\n')
|
||||||
|
mlst.append('function zoomout(){dpi=dpi*1.25;setsize();}\n')
|
||||||
|
mlst.append('function setsize(){var svg=document.getElementById("svgimg");var prev=document.getElementById("prevsvg");var next=document.getElementById("nextsvg");var width=(pw/dpi)+"in";var height=(ph/dpi)+"in";svg.setAttribute("width",width);svg.setAttribute("height",height);prev.setAttribute("height",height);prev.setAttribute("width","50px");next.setAttribute("height",height);next.setAttribute("width","50px");}\n')
|
||||||
|
mlst.append('function ppage(){window.location.href=prevpage+"?dpi="+Math.round(dpi);}\n')
|
||||||
|
mlst.append('function npage(){window.location.href=nextpage+"?dpi="+Math.round(dpi);}\n')
|
||||||
|
mlst.append('var gt=gd();if(gt>0){dpi=gt;}\n')
|
||||||
|
mlst.append('window.onload=setsize;\n')
|
||||||
|
mlst.append(']]></script>\n')
|
||||||
|
mlst.append('</head>\n')
|
||||||
|
mlst.append('<body onLoad="setsize();" style="background-color:#777;text-align:center;">\n')
|
||||||
|
mlst.append('<div style="white-space:nowrap;">\n')
|
||||||
|
if previd == None:
|
||||||
|
mlst.append('<a href="javascript:ppage();"><svg id="prevsvg" viewBox="0 0 100 300" xmlns="http://www.w3.org/2000/svg" version="1.1" style="background-color:#777"></svg></a>\n')
|
||||||
|
else:
|
||||||
|
mlst.append('<a href="javascript:ppage();"><svg id="prevsvg" viewBox="0 0 100 300" xmlns="http://www.w3.org/2000/svg" version="1.1" style="background-color:#777"><polygon points="5,150,95,5,95,295" fill="#AAAAAA" /></svg></a>\n')
|
||||||
|
|
||||||
|
mlst.append('<a href="javascript:npage();"><svg id="svgimg" viewBox="0 0 %d %d" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" style="background-color:#FFF;border:1px solid black;">' % (pp.pw, pp.ph))
|
||||||
|
if (pp.gid != None):
|
||||||
|
mlst.append('<defs>\n')
|
||||||
|
gdefs = pp.getGlyphs()
|
||||||
|
for j in xrange(0,len(gdefs)):
|
||||||
|
mlst.append(gdefs[j])
|
||||||
|
mlst.append('</defs>\n')
|
||||||
|
img = pp.getImages()
|
||||||
|
if (img != None):
|
||||||
|
for j in xrange(0,len(img)):
|
||||||
|
mlst.append(img[j])
|
||||||
|
if (pp.gid != None):
|
||||||
|
for j in xrange(0,len(pp.gid)):
|
||||||
|
mlst.append('<use xlink:href="#gl%d" x="%d" y="%d" />\n' % (pp.gid[j], pp.gx[j], pp.gy[j]))
|
||||||
|
if (img == None or len(img) == 0) and (pp.gid == None or len(pp.gid) == 0):
|
||||||
|
xpos = "%d" % (pp.pw // 3)
|
||||||
|
ypos = "%d" % (pp.ph // 3)
|
||||||
|
mlst.append('<text x="' + xpos + '" y="' + ypos + '" font-size="' + meta_array['fontSize'] + '" font-family="Helvetica" stroke="black">This page intentionally left blank.</text>\n')
|
||||||
|
if (raw) :
|
||||||
|
mlst.append('</svg>')
|
||||||
|
else :
|
||||||
|
mlst.append('</svg></a>\n')
|
||||||
|
if nextid == None:
|
||||||
|
mlst.append('<a href="javascript:npage();"><svg id="nextsvg" viewBox="0 0 100 300" xmlns="http://www.w3.org/2000/svg" version="1.1" style="background-color:#777"></svg></a>\n')
|
||||||
|
else :
|
||||||
|
mlst.append('<a href="javascript:npage();"><svg id="nextsvg" viewBox="0 0 100 300" xmlns="http://www.w3.org/2000/svg" version="1.1" style="background-color:#777"><polygon points="5,5,5,295,95,150" fill="#AAAAAA" /></svg></a>\n')
|
||||||
|
mlst.append('</div>\n')
|
||||||
|
mlst.append('<div><a href="javascript:zoomin();">zoom in</a> - <a href="javascript:zoomout();">zoom out</a></div>\n')
|
||||||
|
mlst.append('</body>\n')
|
||||||
|
mlst.append('</html>\n')
|
||||||
|
return "".join(mlst)
|
||||||
721
Calibre_Plugins/K4MobiDeDRM_plugin/genbook.py
Normal file
721
Calibre_Plugins/K4MobiDeDRM_plugin/genbook.py
Normal file
@@ -0,0 +1,721 @@
|
|||||||
|
#! /usr/bin/python
|
||||||
|
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||||
|
|
||||||
|
class Unbuffered:
|
||||||
|
def __init__(self, stream):
|
||||||
|
self.stream = stream
|
||||||
|
def write(self, data):
|
||||||
|
self.stream.write(data)
|
||||||
|
self.stream.flush()
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return getattr(self.stream, attr)
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.stdout=Unbuffered(sys.stdout)
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
import getopt
|
||||||
|
from struct import pack
|
||||||
|
from struct import unpack
|
||||||
|
|
||||||
|
class TpzDRMError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# local support routines
|
||||||
|
if 'calibre' in sys.modules:
|
||||||
|
inCalibre = True
|
||||||
|
else:
|
||||||
|
inCalibre = False
|
||||||
|
|
||||||
|
if inCalibre :
|
||||||
|
from calibre_plugins.k4mobidedrm import convert2xml
|
||||||
|
from calibre_plugins.k4mobidedrm import flatxml2html
|
||||||
|
from calibre_plugins.k4mobidedrm import flatxml2svg
|
||||||
|
from calibre_plugins.k4mobidedrm import stylexml2css
|
||||||
|
else :
|
||||||
|
import convert2xml
|
||||||
|
import flatxml2html
|
||||||
|
import flatxml2svg
|
||||||
|
import stylexml2css
|
||||||
|
|
||||||
|
# global switch
|
||||||
|
buildXML = False
|
||||||
|
|
||||||
|
# Get a 7 bit encoded number from a file
|
||||||
|
def readEncodedNumber(file):
|
||||||
|
flag = False
|
||||||
|
c = file.read(1)
|
||||||
|
if (len(c) == 0):
|
||||||
|
return None
|
||||||
|
data = ord(c)
|
||||||
|
if data == 0xFF:
|
||||||
|
flag = True
|
||||||
|
c = file.read(1)
|
||||||
|
if (len(c) == 0):
|
||||||
|
return None
|
||||||
|
data = ord(c)
|
||||||
|
if data >= 0x80:
|
||||||
|
datax = (data & 0x7F)
|
||||||
|
while data >= 0x80 :
|
||||||
|
c = file.read(1)
|
||||||
|
if (len(c) == 0):
|
||||||
|
return None
|
||||||
|
data = ord(c)
|
||||||
|
datax = (datax <<7) + (data & 0x7F)
|
||||||
|
data = datax
|
||||||
|
if flag:
|
||||||
|
data = -data
|
||||||
|
return data
|
||||||
|
|
||||||
|
# Get a length prefixed string from the file
|
||||||
|
def lengthPrefixString(data):
|
||||||
|
return encodeNumber(len(data))+data
|
||||||
|
|
||||||
|
def readString(file):
|
||||||
|
stringLength = readEncodedNumber(file)
|
||||||
|
if (stringLength == None):
|
||||||
|
return None
|
||||||
|
sv = file.read(stringLength)
|
||||||
|
if (len(sv) != stringLength):
|
||||||
|
return ""
|
||||||
|
return unpack(str(stringLength)+"s",sv)[0]
|
||||||
|
|
||||||
|
def getMetaArray(metaFile):
|
||||||
|
# parse the meta file
|
||||||
|
result = {}
|
||||||
|
fo = file(metaFile,'rb')
|
||||||
|
size = readEncodedNumber(fo)
|
||||||
|
for i in xrange(size):
|
||||||
|
tag = readString(fo)
|
||||||
|
value = readString(fo)
|
||||||
|
result[tag] = value
|
||||||
|
# print tag, value
|
||||||
|
fo.close()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# dictionary of all text strings by index value
|
||||||
|
class Dictionary(object):
|
||||||
|
def __init__(self, dictFile):
|
||||||
|
self.filename = dictFile
|
||||||
|
self.size = 0
|
||||||
|
self.fo = file(dictFile,'rb')
|
||||||
|
self.stable = []
|
||||||
|
self.size = readEncodedNumber(self.fo)
|
||||||
|
for i in xrange(self.size):
|
||||||
|
self.stable.append(self.escapestr(readString(self.fo)))
|
||||||
|
self.pos = 0
|
||||||
|
def escapestr(self, str):
|
||||||
|
str = str.replace('&','&')
|
||||||
|
str = str.replace('<','<')
|
||||||
|
str = str.replace('>','>')
|
||||||
|
str = str.replace('=','=')
|
||||||
|
return str
|
||||||
|
def lookup(self,val):
|
||||||
|
if ((val >= 0) and (val < self.size)) :
|
||||||
|
self.pos = val
|
||||||
|
return self.stable[self.pos]
|
||||||
|
else:
|
||||||
|
print "Error - %d outside of string table limits" % val
|
||||||
|
raise TpzDRMError('outside or string table limits')
|
||||||
|
# sys.exit(-1)
|
||||||
|
def getSize(self):
|
||||||
|
return self.size
|
||||||
|
def getPos(self):
|
||||||
|
return self.pos
|
||||||
|
|
||||||
|
|
||||||
|
class PageDimParser(object):
|
||||||
|
def __init__(self, flatxml):
|
||||||
|
self.flatdoc = flatxml.split('\n')
|
||||||
|
# find tag if within pos to end inclusive
|
||||||
|
def findinDoc(self, tagpath, pos, end) :
|
||||||
|
result = None
|
||||||
|
docList = self.flatdoc
|
||||||
|
cnt = len(docList)
|
||||||
|
if end == -1 :
|
||||||
|
end = cnt
|
||||||
|
else:
|
||||||
|
end = min(cnt,end)
|
||||||
|
foundat = -1
|
||||||
|
for j in xrange(pos, end):
|
||||||
|
item = docList[j]
|
||||||
|
if item.find('=') >= 0:
|
||||||
|
(name, argres) = item.split('=')
|
||||||
|
else :
|
||||||
|
name = item
|
||||||
|
argres = ''
|
||||||
|
if name.endswith(tagpath) :
|
||||||
|
result = argres
|
||||||
|
foundat = j
|
||||||
|
break
|
||||||
|
return foundat, result
|
||||||
|
def process(self):
|
||||||
|
(pos, sph) = self.findinDoc('page.h',0,-1)
|
||||||
|
(pos, spw) = self.findinDoc('page.w',0,-1)
|
||||||
|
if (sph == None): sph = '-1'
|
||||||
|
if (spw == None): spw = '-1'
|
||||||
|
return sph, spw
|
||||||
|
|
||||||
|
def getPageDim(flatxml):
|
||||||
|
# create a document parser
|
||||||
|
dp = PageDimParser(flatxml)
|
||||||
|
(ph, pw) = dp.process()
|
||||||
|
return ph, pw
|
||||||
|
|
||||||
|
class GParser(object):
|
||||||
|
def __init__(self, flatxml):
|
||||||
|
self.flatdoc = flatxml.split('\n')
|
||||||
|
self.dpi = 1440
|
||||||
|
self.gh = self.getData('info.glyph.h')
|
||||||
|
self.gw = self.getData('info.glyph.w')
|
||||||
|
self.guse = self.getData('info.glyph.use')
|
||||||
|
if self.guse :
|
||||||
|
self.count = len(self.guse)
|
||||||
|
else :
|
||||||
|
self.count = 0
|
||||||
|
self.gvtx = self.getData('info.glyph.vtx')
|
||||||
|
self.glen = self.getData('info.glyph.len')
|
||||||
|
self.gdpi = self.getData('info.glyph.dpi')
|
||||||
|
self.vx = self.getData('info.vtx.x')
|
||||||
|
self.vy = self.getData('info.vtx.y')
|
||||||
|
self.vlen = self.getData('info.len.n')
|
||||||
|
if self.vlen :
|
||||||
|
self.glen.append(len(self.vlen))
|
||||||
|
elif self.glen:
|
||||||
|
self.glen.append(0)
|
||||||
|
if self.vx :
|
||||||
|
self.gvtx.append(len(self.vx))
|
||||||
|
elif self.gvtx :
|
||||||
|
self.gvtx.append(0)
|
||||||
|
def getData(self, path):
|
||||||
|
result = None
|
||||||
|
cnt = len(self.flatdoc)
|
||||||
|
for j in xrange(cnt):
|
||||||
|
item = self.flatdoc[j]
|
||||||
|
if item.find('=') >= 0:
|
||||||
|
(name, argt) = item.split('=')
|
||||||
|
argres = argt.split('|')
|
||||||
|
else:
|
||||||
|
name = item
|
||||||
|
argres = []
|
||||||
|
if (name == path):
|
||||||
|
result = argres
|
||||||
|
break
|
||||||
|
if (len(argres) > 0) :
|
||||||
|
for j in xrange(0,len(argres)):
|
||||||
|
argres[j] = int(argres[j])
|
||||||
|
return result
|
||||||
|
def getGlyphDim(self, gly):
|
||||||
|
if self.gdpi[gly] == 0:
|
||||||
|
return 0, 0
|
||||||
|
maxh = (self.gh[gly] * self.dpi) / self.gdpi[gly]
|
||||||
|
maxw = (self.gw[gly] * self.dpi) / self.gdpi[gly]
|
||||||
|
return maxh, maxw
|
||||||
|
def getPath(self, gly):
|
||||||
|
path = ''
|
||||||
|
if (gly < 0) or (gly >= self.count):
|
||||||
|
return path
|
||||||
|
tx = self.vx[self.gvtx[gly]:self.gvtx[gly+1]]
|
||||||
|
ty = self.vy[self.gvtx[gly]:self.gvtx[gly+1]]
|
||||||
|
p = 0
|
||||||
|
for k in xrange(self.glen[gly], self.glen[gly+1]):
|
||||||
|
if (p == 0):
|
||||||
|
zx = tx[0:self.vlen[k]+1]
|
||||||
|
zy = ty[0:self.vlen[k]+1]
|
||||||
|
else:
|
||||||
|
zx = tx[self.vlen[k-1]+1:self.vlen[k]+1]
|
||||||
|
zy = ty[self.vlen[k-1]+1:self.vlen[k]+1]
|
||||||
|
p += 1
|
||||||
|
j = 0
|
||||||
|
while ( j < len(zx) ):
|
||||||
|
if (j == 0):
|
||||||
|
# Start Position.
|
||||||
|
path += 'M %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly])
|
||||||
|
elif (j <= len(zx)-3):
|
||||||
|
# Cubic Bezier Curve
|
||||||
|
path += 'C %d %d %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[j+1] * self.dpi / self.gdpi[gly], zy[j+1] * self.dpi / self.gdpi[gly], zx[j+2] * self.dpi / self.gdpi[gly], zy[j+2] * self.dpi / self.gdpi[gly])
|
||||||
|
j += 2
|
||||||
|
elif (j == len(zx)-2):
|
||||||
|
# Cubic Bezier Curve to Start Position
|
||||||
|
path += 'C %d %d %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[j+1] * self.dpi / self.gdpi[gly], zy[j+1] * self.dpi / self.gdpi[gly], zx[0] * self.dpi / self.gdpi[gly], zy[0] * self.dpi / self.gdpi[gly])
|
||||||
|
j += 1
|
||||||
|
elif (j == len(zx)-1):
|
||||||
|
# Quadratic Bezier Curve to Start Position
|
||||||
|
path += 'Q %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[0] * self.dpi / self.gdpi[gly], zy[0] * self.dpi / self.gdpi[gly])
|
||||||
|
|
||||||
|
j += 1
|
||||||
|
path += 'z'
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# dictionary of all text strings by index value
|
||||||
|
class GlyphDict(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.gdict = {}
|
||||||
|
def lookup(self, id):
|
||||||
|
# id='id="gl%d"' % val
|
||||||
|
if id in self.gdict:
|
||||||
|
return self.gdict[id]
|
||||||
|
return None
|
||||||
|
def addGlyph(self, val, path):
|
||||||
|
id='id="gl%d"' % val
|
||||||
|
self.gdict[id] = path
|
||||||
|
|
||||||
|
|
||||||
|
def generateBook(bookDir, raw, fixedimage):
|
||||||
|
# sanity check Topaz file extraction
|
||||||
|
if not os.path.exists(bookDir) :
|
||||||
|
print "Can not find directory with unencrypted book"
|
||||||
|
return 1
|
||||||
|
|
||||||
|
dictFile = os.path.join(bookDir,'dict0000.dat')
|
||||||
|
if not os.path.exists(dictFile) :
|
||||||
|
print "Can not find dict0000.dat file"
|
||||||
|
return 1
|
||||||
|
|
||||||
|
pageDir = os.path.join(bookDir,'page')
|
||||||
|
if not os.path.exists(pageDir) :
|
||||||
|
print "Can not find page directory in unencrypted book"
|
||||||
|
return 1
|
||||||
|
|
||||||
|
imgDir = os.path.join(bookDir,'img')
|
||||||
|
if not os.path.exists(imgDir) :
|
||||||
|
print "Can not find image directory in unencrypted book"
|
||||||
|
return 1
|
||||||
|
|
||||||
|
glyphsDir = os.path.join(bookDir,'glyphs')
|
||||||
|
if not os.path.exists(glyphsDir) :
|
||||||
|
print "Can not find glyphs directory in unencrypted book"
|
||||||
|
return 1
|
||||||
|
|
||||||
|
metaFile = os.path.join(bookDir,'metadata0000.dat')
|
||||||
|
if not os.path.exists(metaFile) :
|
||||||
|
print "Can not find metadata0000.dat in unencrypted book"
|
||||||
|
return 1
|
||||||
|
|
||||||
|
svgDir = os.path.join(bookDir,'svg')
|
||||||
|
if not os.path.exists(svgDir) :
|
||||||
|
os.makedirs(svgDir)
|
||||||
|
|
||||||
|
if buildXML:
|
||||||
|
xmlDir = os.path.join(bookDir,'xml')
|
||||||
|
if not os.path.exists(xmlDir) :
|
||||||
|
os.makedirs(xmlDir)
|
||||||
|
|
||||||
|
otherFile = os.path.join(bookDir,'other0000.dat')
|
||||||
|
if not os.path.exists(otherFile) :
|
||||||
|
print "Can not find other0000.dat in unencrypted book"
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print "Updating to color images if available"
|
||||||
|
spath = os.path.join(bookDir,'color_img')
|
||||||
|
dpath = os.path.join(bookDir,'img')
|
||||||
|
filenames = os.listdir(spath)
|
||||||
|
filenames = sorted(filenames)
|
||||||
|
for filename in filenames:
|
||||||
|
imgname = filename.replace('color','img')
|
||||||
|
sfile = os.path.join(spath,filename)
|
||||||
|
dfile = os.path.join(dpath,imgname)
|
||||||
|
imgdata = file(sfile,'rb').read()
|
||||||
|
file(dfile,'wb').write(imgdata)
|
||||||
|
|
||||||
|
print "Creating cover.jpg"
|
||||||
|
isCover = False
|
||||||
|
cpath = os.path.join(bookDir,'img')
|
||||||
|
cpath = os.path.join(cpath,'img0000.jpg')
|
||||||
|
if os.path.isfile(cpath):
|
||||||
|
cover = file(cpath, 'rb').read()
|
||||||
|
cpath = os.path.join(bookDir,'cover.jpg')
|
||||||
|
file(cpath, 'wb').write(cover)
|
||||||
|
isCover = True
|
||||||
|
|
||||||
|
|
||||||
|
print 'Processing Dictionary'
|
||||||
|
dict = Dictionary(dictFile)
|
||||||
|
|
||||||
|
print 'Processing Meta Data and creating OPF'
|
||||||
|
meta_array = getMetaArray(metaFile)
|
||||||
|
|
||||||
|
# replace special chars in title and authors like & < >
|
||||||
|
title = meta_array.get('Title','No Title Provided')
|
||||||
|
title = title.replace('&','&')
|
||||||
|
title = title.replace('<','<')
|
||||||
|
title = title.replace('>','>')
|
||||||
|
meta_array['Title'] = title
|
||||||
|
authors = meta_array.get('Authors','No Authors Provided')
|
||||||
|
authors = authors.replace('&','&')
|
||||||
|
authors = authors.replace('<','<')
|
||||||
|
authors = authors.replace('>','>')
|
||||||
|
meta_array['Authors'] = authors
|
||||||
|
|
||||||
|
if buildXML:
|
||||||
|
xname = os.path.join(xmlDir, 'metadata.xml')
|
||||||
|
mlst = []
|
||||||
|
for key in meta_array:
|
||||||
|
mlst.append('<meta name="' + key + '" content="' + meta_array[key] + '" />\n')
|
||||||
|
metastr = "".join(mlst)
|
||||||
|
mlst = None
|
||||||
|
file(xname, 'wb').write(metastr)
|
||||||
|
|
||||||
|
print 'Processing StyleSheet'
|
||||||
|
|
||||||
|
# get some scaling info from metadata to use while processing styles
|
||||||
|
# and first page info
|
||||||
|
|
||||||
|
fontsize = '135'
|
||||||
|
if 'fontSize' in meta_array:
|
||||||
|
fontsize = meta_array['fontSize']
|
||||||
|
|
||||||
|
# also get the size of a normal text page
|
||||||
|
# get the total number of pages unpacked as a safety check
|
||||||
|
filenames = os.listdir(pageDir)
|
||||||
|
numfiles = len(filenames)
|
||||||
|
|
||||||
|
spage = '1'
|
||||||
|
if 'firstTextPage' in meta_array:
|
||||||
|
spage = meta_array['firstTextPage']
|
||||||
|
pnum = int(spage)
|
||||||
|
if pnum >= numfiles or pnum < 0:
|
||||||
|
# metadata is wrong so just select a page near the front
|
||||||
|
# 10% of the book to get a normal text page
|
||||||
|
pnum = int(0.10 * numfiles)
|
||||||
|
# print "first normal text page is", spage
|
||||||
|
|
||||||
|
# get page height and width from first text page for use in stylesheet scaling
|
||||||
|
pname = 'page%04d.dat' % (pnum + 1)
|
||||||
|
fname = os.path.join(pageDir,pname)
|
||||||
|
flat_xml = convert2xml.fromData(dict, fname)
|
||||||
|
|
||||||
|
(ph, pw) = getPageDim(flat_xml)
|
||||||
|
if (ph == '-1') or (ph == '0') : ph = '11000'
|
||||||
|
if (pw == '-1') or (pw == '0') : pw = '8500'
|
||||||
|
meta_array['pageHeight'] = ph
|
||||||
|
meta_array['pageWidth'] = pw
|
||||||
|
if 'fontSize' not in meta_array.keys():
|
||||||
|
meta_array['fontSize'] = fontsize
|
||||||
|
|
||||||
|
# process other.dat for css info and for map of page files to svg images
|
||||||
|
# this map is needed because some pages actually are made up of multiple
|
||||||
|
# pageXXXX.xml files
|
||||||
|
xname = os.path.join(bookDir, 'style.css')
|
||||||
|
flat_xml = convert2xml.fromData(dict, otherFile)
|
||||||
|
|
||||||
|
# extract info.original.pid to get original page information
|
||||||
|
pageIDMap = {}
|
||||||
|
pageidnums = stylexml2css.getpageIDMap(flat_xml)
|
||||||
|
if len(pageidnums) == 0:
|
||||||
|
filenames = os.listdir(pageDir)
|
||||||
|
numfiles = len(filenames)
|
||||||
|
for k in range(numfiles):
|
||||||
|
pageidnums.append(k)
|
||||||
|
# create a map from page ids to list of page file nums to process for that page
|
||||||
|
for i in range(len(pageidnums)):
|
||||||
|
id = pageidnums[i]
|
||||||
|
if id in pageIDMap.keys():
|
||||||
|
pageIDMap[id].append(i)
|
||||||
|
else:
|
||||||
|
pageIDMap[id] = [i]
|
||||||
|
|
||||||
|
# now get the css info
|
||||||
|
cssstr , classlst = stylexml2css.convert2CSS(flat_xml, fontsize, ph, pw)
|
||||||
|
file(xname, 'wb').write(cssstr)
|
||||||
|
if buildXML:
|
||||||
|
xname = os.path.join(xmlDir, 'other0000.xml')
|
||||||
|
file(xname, 'wb').write(convert2xml.getXML(dict, otherFile))
|
||||||
|
|
||||||
|
print 'Processing Glyphs'
|
||||||
|
gd = GlyphDict()
|
||||||
|
filenames = os.listdir(glyphsDir)
|
||||||
|
filenames = sorted(filenames)
|
||||||
|
glyfname = os.path.join(svgDir,'glyphs.svg')
|
||||||
|
glyfile = open(glyfname, 'w')
|
||||||
|
glyfile.write('<?xml version="1.0" standalone="no"?>\n')
|
||||||
|
glyfile.write('<!DOCTYPE svg PUBLIC "-//W3C/DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
|
||||||
|
glyfile.write('<svg width="512" height="512" viewBox="0 0 511 511" xmlns="http://www.w3.org/2000/svg" version="1.1">\n')
|
||||||
|
glyfile.write('<title>Glyphs for %s</title>\n' % meta_array['Title'])
|
||||||
|
glyfile.write('<defs>\n')
|
||||||
|
counter = 0
|
||||||
|
for filename in filenames:
|
||||||
|
# print ' ', filename
|
||||||
|
print '.',
|
||||||
|
fname = os.path.join(glyphsDir,filename)
|
||||||
|
flat_xml = convert2xml.fromData(dict, fname)
|
||||||
|
|
||||||
|
if buildXML:
|
||||||
|
xname = os.path.join(xmlDir, filename.replace('.dat','.xml'))
|
||||||
|
file(xname, 'wb').write(convert2xml.getXML(dict, fname))
|
||||||
|
|
||||||
|
gp = GParser(flat_xml)
|
||||||
|
for i in xrange(0, gp.count):
|
||||||
|
path = gp.getPath(i)
|
||||||
|
maxh, maxw = gp.getGlyphDim(i)
|
||||||
|
fullpath = '<path id="gl%d" d="%s" fill="black" /><!-- width=%d height=%d -->\n' % (counter * 256 + i, path, maxw, maxh)
|
||||||
|
glyfile.write(fullpath)
|
||||||
|
gd.addGlyph(counter * 256 + i, fullpath)
|
||||||
|
counter += 1
|
||||||
|
glyfile.write('</defs>\n')
|
||||||
|
glyfile.write('</svg>\n')
|
||||||
|
glyfile.close()
|
||||||
|
print " "
|
||||||
|
|
||||||
|
|
||||||
|
# start up the html
|
||||||
|
# also build up tocentries while processing html
|
||||||
|
htmlFileName = "book.html"
|
||||||
|
hlst = []
|
||||||
|
hlst.append('<?xml version="1.0" encoding="utf-8"?>\n')
|
||||||
|
hlst.append('<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.1 Strict//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11-strict.dtd">\n')
|
||||||
|
hlst.append('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">\n')
|
||||||
|
hlst.append('<head>\n')
|
||||||
|
hlst.append('<meta http-equiv="content-type" content="text/html; charset=utf-8"/>\n')
|
||||||
|
hlst.append('<title>' + meta_array['Title'] + ' by ' + meta_array['Authors'] + '</title>\n')
|
||||||
|
hlst.append('<meta name="Author" content="' + meta_array['Authors'] + '" />\n')
|
||||||
|
hlst.append('<meta name="Title" content="' + meta_array['Title'] + '" />\n')
|
||||||
|
if 'ASIN' in meta_array:
|
||||||
|
hlst.append('<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n')
|
||||||
|
if 'GUID' in meta_array:
|
||||||
|
hlst.append('<meta name="GUID" content="' + meta_array['GUID'] + '" />\n')
|
||||||
|
hlst.append('<link href="style.css" rel="stylesheet" type="text/css" />\n')
|
||||||
|
hlst.append('</head>\n<body>\n')
|
||||||
|
|
||||||
|
print 'Processing Pages'
|
||||||
|
# Books are at 1440 DPI. This is rendering at twice that size for
|
||||||
|
# readability when rendering to the screen.
|
||||||
|
scaledpi = 1440.0
|
||||||
|
|
||||||
|
filenames = os.listdir(pageDir)
|
||||||
|
filenames = sorted(filenames)
|
||||||
|
numfiles = len(filenames)
|
||||||
|
|
||||||
|
xmllst = []
|
||||||
|
elst = []
|
||||||
|
|
||||||
|
for filename in filenames:
|
||||||
|
# print ' ', filename
|
||||||
|
print ".",
|
||||||
|
fname = os.path.join(pageDir,filename)
|
||||||
|
flat_xml = convert2xml.fromData(dict, fname)
|
||||||
|
|
||||||
|
# keep flat_xml for later svg processing
|
||||||
|
xmllst.append(flat_xml)
|
||||||
|
|
||||||
|
if buildXML:
|
||||||
|
xname = os.path.join(xmlDir, filename.replace('.dat','.xml'))
|
||||||
|
file(xname, 'wb').write(convert2xml.getXML(dict, fname))
|
||||||
|
|
||||||
|
# first get the html
|
||||||
|
pagehtml, tocinfo = flatxml2html.convert2HTML(flat_xml, classlst, fname, bookDir, gd, fixedimage)
|
||||||
|
elst.append(tocinfo)
|
||||||
|
hlst.append(pagehtml)
|
||||||
|
|
||||||
|
# finish up the html string and output it
|
||||||
|
hlst.append('</body>\n</html>\n')
|
||||||
|
htmlstr = "".join(hlst)
|
||||||
|
hlst = None
|
||||||
|
file(os.path.join(bookDir, htmlFileName), 'wb').write(htmlstr)
|
||||||
|
|
||||||
|
print " "
|
||||||
|
print 'Extracting Table of Contents from Amazon OCR'
|
||||||
|
|
||||||
|
# first create a table of contents file for the svg images
|
||||||
|
tlst = []
|
||||||
|
tlst.append('<?xml version="1.0" encoding="utf-8"?>\n')
|
||||||
|
tlst.append('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n')
|
||||||
|
tlst.append('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" >')
|
||||||
|
tlst.append('<head>\n')
|
||||||
|
tlst.append('<title>' + meta_array['Title'] + '</title>\n')
|
||||||
|
tlst.append('<meta name="Author" content="' + meta_array['Authors'] + '" />\n')
|
||||||
|
tlst.append('<meta name="Title" content="' + meta_array['Title'] + '" />\n')
|
||||||
|
if 'ASIN' in meta_array:
|
||||||
|
tlst.append('<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n')
|
||||||
|
if 'GUID' in meta_array:
|
||||||
|
tlst.append('<meta name="GUID" content="' + meta_array['GUID'] + '" />\n')
|
||||||
|
tlst.append('</head>\n')
|
||||||
|
tlst.append('<body>\n')
|
||||||
|
|
||||||
|
tlst.append('<h2>Table of Contents</h2>\n')
|
||||||
|
start = pageidnums[0]
|
||||||
|
if (raw):
|
||||||
|
startname = 'page%04d.svg' % start
|
||||||
|
else:
|
||||||
|
startname = 'page%04d.xhtml' % start
|
||||||
|
|
||||||
|
tlst.append('<h3><a href="' + startname + '">Start of Book</a></h3>\n')
|
||||||
|
# build up a table of contents for the svg xhtml output
|
||||||
|
tocentries = "".join(elst)
|
||||||
|
elst = None
|
||||||
|
toclst = tocentries.split('\n')
|
||||||
|
toclst.pop()
|
||||||
|
for entry in toclst:
|
||||||
|
print entry
|
||||||
|
title, pagenum = entry.split('|')
|
||||||
|
id = pageidnums[int(pagenum)]
|
||||||
|
if (raw):
|
||||||
|
fname = 'page%04d.svg' % id
|
||||||
|
else:
|
||||||
|
fname = 'page%04d.xhtml' % id
|
||||||
|
tlst.append('<h3><a href="'+ fname + '">' + title + '</a></h3>\n')
|
||||||
|
tlst.append('</body>\n')
|
||||||
|
tlst.append('</html>\n')
|
||||||
|
tochtml = "".join(tlst)
|
||||||
|
file(os.path.join(svgDir, 'toc.xhtml'), 'wb').write(tochtml)
|
||||||
|
|
||||||
|
|
||||||
|
# now create index_svg.xhtml that points to all required files
|
||||||
|
slst = []
|
||||||
|
slst.append('<?xml version="1.0" encoding="utf-8"?>\n')
|
||||||
|
slst.append('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n')
|
||||||
|
slst.append('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" >')
|
||||||
|
slst.append('<head>\n')
|
||||||
|
slst.append('<title>' + meta_array['Title'] + '</title>\n')
|
||||||
|
slst.append('<meta name="Author" content="' + meta_array['Authors'] + '" />\n')
|
||||||
|
slst.append('<meta name="Title" content="' + meta_array['Title'] + '" />\n')
|
||||||
|
if 'ASIN' in meta_array:
|
||||||
|
slst.append('<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n')
|
||||||
|
if 'GUID' in meta_array:
|
||||||
|
slst.append('<meta name="GUID" content="' + meta_array['GUID'] + '" />\n')
|
||||||
|
slst.append('</head>\n')
|
||||||
|
slst.append('<body>\n')
|
||||||
|
|
||||||
|
print "Building svg images of each book page"
|
||||||
|
slst.append('<h2>List of Pages</h2>\n')
|
||||||
|
slst.append('<div>\n')
|
||||||
|
idlst = sorted(pageIDMap.keys())
|
||||||
|
numids = len(idlst)
|
||||||
|
cnt = len(idlst)
|
||||||
|
previd = None
|
||||||
|
for j in range(cnt):
|
||||||
|
pageid = idlst[j]
|
||||||
|
if j < cnt - 1:
|
||||||
|
nextid = idlst[j+1]
|
||||||
|
else:
|
||||||
|
nextid = None
|
||||||
|
print '.',
|
||||||
|
pagelst = pageIDMap[pageid]
|
||||||
|
flst = []
|
||||||
|
for page in pagelst:
|
||||||
|
flst.append(xmllst[page])
|
||||||
|
flat_svg = "".join(flst)
|
||||||
|
flst=None
|
||||||
|
svgxml = flatxml2svg.convert2SVG(gd, flat_svg, pageid, previd, nextid, svgDir, raw, meta_array, scaledpi)
|
||||||
|
if (raw) :
|
||||||
|
pfile = open(os.path.join(svgDir,'page%04d.svg' % pageid),'w')
|
||||||
|
slst.append('<a href="svg/page%04d.svg">Page %d</a>\n' % (pageid, pageid))
|
||||||
|
else :
|
||||||
|
pfile = open(os.path.join(svgDir,'page%04d.xhtml' % pageid), 'w')
|
||||||
|
slst.append('<a href="svg/page%04d.xhtml">Page %d</a>\n' % (pageid, pageid))
|
||||||
|
previd = pageid
|
||||||
|
pfile.write(svgxml)
|
||||||
|
pfile.close()
|
||||||
|
counter += 1
|
||||||
|
slst.append('</div>\n')
|
||||||
|
slst.append('<h2><a href="svg/toc.xhtml">Table of Contents</a></h2>\n')
|
||||||
|
slst.append('</body>\n</html>\n')
|
||||||
|
svgindex = "".join(slst)
|
||||||
|
slst = None
|
||||||
|
file(os.path.join(bookDir, 'index_svg.xhtml'), 'wb').write(svgindex)
|
||||||
|
|
||||||
|
print " "
|
||||||
|
|
||||||
|
# build the opf file
|
||||||
|
opfname = os.path.join(bookDir, 'book.opf')
|
||||||
|
olst = []
|
||||||
|
olst.append('<?xml version="1.0" encoding="utf-8"?>\n')
|
||||||
|
olst.append('<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="guid_id">\n')
|
||||||
|
# adding metadata
|
||||||
|
olst.append(' <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">\n')
|
||||||
|
if 'GUID' in meta_array:
|
||||||
|
olst.append(' <dc:identifier opf:scheme="GUID" id="guid_id">' + meta_array['GUID'] + '</dc:identifier>\n')
|
||||||
|
if 'ASIN' in meta_array:
|
||||||
|
olst.append(' <dc:identifier opf:scheme="ASIN">' + meta_array['ASIN'] + '</dc:identifier>\n')
|
||||||
|
if 'oASIN' in meta_array:
|
||||||
|
olst.append(' <dc:identifier opf:scheme="oASIN">' + meta_array['oASIN'] + '</dc:identifier>\n')
|
||||||
|
olst.append(' <dc:title>' + meta_array['Title'] + '</dc:title>\n')
|
||||||
|
olst.append(' <dc:creator opf:role="aut">' + meta_array['Authors'] + '</dc:creator>\n')
|
||||||
|
olst.append(' <dc:language>en</dc:language>\n')
|
||||||
|
olst.append(' <dc:date>' + meta_array['UpdateTime'] + '</dc:date>\n')
|
||||||
|
if isCover:
|
||||||
|
olst.append(' <meta name="cover" content="bookcover"/>\n')
|
||||||
|
olst.append(' </metadata>\n')
|
||||||
|
olst.append('<manifest>\n')
|
||||||
|
olst.append(' <item id="book" href="book.html" media-type="application/xhtml+xml"/>\n')
|
||||||
|
olst.append(' <item id="stylesheet" href="style.css" media-type="text/css"/>\n')
|
||||||
|
# adding image files to manifest
|
||||||
|
filenames = os.listdir(imgDir)
|
||||||
|
filenames = sorted(filenames)
|
||||||
|
for filename in filenames:
|
||||||
|
imgname, imgext = os.path.splitext(filename)
|
||||||
|
if imgext == '.jpg':
|
||||||
|
imgext = 'jpeg'
|
||||||
|
if imgext == '.svg':
|
||||||
|
imgext = 'svg+xml'
|
||||||
|
olst.append(' <item id="' + imgname + '" href="img/' + filename + '" media-type="image/' + imgext + '"/>\n')
|
||||||
|
if isCover:
|
||||||
|
olst.append(' <item id="bookcover" href="cover.jpg" media-type="image/jpeg" />\n')
|
||||||
|
olst.append('</manifest>\n')
|
||||||
|
# adding spine
|
||||||
|
olst.append('<spine>\n <itemref idref="book" />\n</spine>\n')
|
||||||
|
if isCover:
|
||||||
|
olst.append(' <guide>\n')
|
||||||
|
olst.append(' <reference href="cover.jpg" type="cover" title="Cover"/>\n')
|
||||||
|
olst.append(' </guide>\n')
|
||||||
|
olst.append('</package>\n')
|
||||||
|
opfstr = "".join(olst)
|
||||||
|
olst = None
|
||||||
|
file(opfname, 'wb').write(opfstr)
|
||||||
|
|
||||||
|
print 'Processing Complete'
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def usage():
|
||||||
|
print "genbook.py generates a book from the extract Topaz Files"
|
||||||
|
print "Usage:"
|
||||||
|
print " genbook.py [-r] [-h [--fixed-image] <bookDir> "
|
||||||
|
print " "
|
||||||
|
print "Options:"
|
||||||
|
print " -h : help - print this usage message"
|
||||||
|
print " -r : generate raw svg files (not wrapped in xhtml)"
|
||||||
|
print " --fixed-image : genearate any Fixed Area as an svg image in the html"
|
||||||
|
print " "
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
bookDir = ''
|
||||||
|
if len(argv) == 0:
|
||||||
|
argv = sys.argv
|
||||||
|
|
||||||
|
try:
|
||||||
|
opts, args = getopt.getopt(argv[1:], "rh:",["fixed-image"])
|
||||||
|
|
||||||
|
except getopt.GetoptError, err:
|
||||||
|
print str(err)
|
||||||
|
usage()
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if len(opts) == 0 and len(args) == 0 :
|
||||||
|
usage()
|
||||||
|
return 1
|
||||||
|
|
||||||
|
raw = 0
|
||||||
|
fixedimage = True
|
||||||
|
for o, a in opts:
|
||||||
|
if o =="-h":
|
||||||
|
usage()
|
||||||
|
return 0
|
||||||
|
if o =="-r":
|
||||||
|
raw = 1
|
||||||
|
if o =="--fixed-image":
|
||||||
|
fixedimage = True
|
||||||
|
|
||||||
|
bookDir = args[0]
|
||||||
|
|
||||||
|
rv = generateBook(bookDir, raw, fixedimage)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main(''))
|
||||||
84
Calibre_Plugins/K4MobiDeDRM_plugin/getk4pcpids.py
Normal file
84
Calibre_Plugins/K4MobiDeDRM_plugin/getk4pcpids.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
#!/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())
|
||||||
302
Calibre_Plugins/K4MobiDeDRM_plugin/k4mobidedrm.py
Normal file
302
Calibre_Plugins/K4MobiDeDRM_plugin/k4mobidedrm.py
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
# ignobleepub.pyw, version 3.6
|
||||||
|
# Copyright © 2009-2012 by DiapDealer et al.
|
||||||
|
|
||||||
|
# engine to remove drm from Kindle for Mac and Kindle for PC books
|
||||||
|
# for personal use for archiving and converting your ebooks
|
||||||
|
|
||||||
|
# PLEASE DO NOT PIRATE EBOOKS!
|
||||||
|
|
||||||
|
# We want all authors and publishers, and eBook stores to live
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
|
||||||
|
# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates
|
||||||
|
# and many many others
|
||||||
|
# Special thanks to The Dark Reverser for MobiDeDrm and CMBDTC for cmbdtc_dump
|
||||||
|
# from which this script borrows most unashamedly.
|
||||||
|
|
||||||
|
|
||||||
|
# Changelog
|
||||||
|
# 1.0 - Name change to k4mobidedrm. Adds Mac support, Adds plugin code
|
||||||
|
# 1.1 - Adds support for additional kindle.info files
|
||||||
|
# 1.2 - Better error handling for older Mobipocket
|
||||||
|
# 1.3 - Don't try to decrypt Topaz books
|
||||||
|
# 1.7 - Add support for Topaz books and Kindle serial numbers. Split code.
|
||||||
|
# 1.9 - Tidy up after Topaz, minor exception changes
|
||||||
|
# 2.1 - Topaz fix and filename sanitizing
|
||||||
|
# 2.2 - Topaz Fix and minor Mac code fix
|
||||||
|
# 2.3 - More Topaz fixes
|
||||||
|
# 2.4 - K4PC/Mac key generation fix
|
||||||
|
# 2.6 - Better handling of non-K4PC/Mac ebooks
|
||||||
|
# 2.7 - Better trailing bytes handling in mobidedrm
|
||||||
|
# 2.8 - Moved parsing of kindle.info files to mac & pc util files.
|
||||||
|
# 3.1 - Updated for new calibre interface. Now __init__ in plugin.
|
||||||
|
# 3.5 - Now support Kindle for PC/Mac 1.6
|
||||||
|
# 3.6 - Even better trailing bytes handling in mobidedrm
|
||||||
|
# 3.7 - Add support for Amazon Print Replica ebooks.
|
||||||
|
# 3.8 - Improved Topaz support
|
||||||
|
# 4.1 - Improved Topaz support and faster decryption with alfcrypto
|
||||||
|
# 4.2 - Added support for Amazon's KF8 format ebooks
|
||||||
|
# 4.4 - Linux calls to Wine added, and improved configuration dialog
|
||||||
|
# 4.5 - Linux works again without Wine. Some Mac key file search changes
|
||||||
|
# 4.6 - First attempt to handle unicode properly
|
||||||
|
# 4.7 - Added timing reports, and changed search for Mac key files
|
||||||
|
# 4.8 - Much better unicode handling, matching the updated inept and ignoble scripts
|
||||||
|
# - Moved back into plugin, __init__ in plugin now only contains plugin code.
|
||||||
|
|
||||||
|
__version__ = '4.8'
|
||||||
|
|
||||||
|
|
||||||
|
import sys, os, re
|
||||||
|
import csv
|
||||||
|
import getopt
|
||||||
|
import re
|
||||||
|
import traceback
|
||||||
|
import time
|
||||||
|
import htmlentitydefs
|
||||||
|
|
||||||
|
class DrmException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if 'calibre' in sys.modules:
|
||||||
|
inCalibre = True
|
||||||
|
else:
|
||||||
|
inCalibre = False
|
||||||
|
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.k4mobidedrm import mobidedrm
|
||||||
|
from calibre_plugins.k4mobidedrm import topazextract
|
||||||
|
from calibre_plugins.k4mobidedrm import kgenpids
|
||||||
|
else:
|
||||||
|
import mobidedrm
|
||||||
|
import topazextract
|
||||||
|
import kgenpids
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
iswindows = sys.platform.startswith('win')
|
||||||
|
isosx = sys.platform.startswith('darwin')
|
||||||
|
|
||||||
|
def unicode_argv():
|
||||||
|
if iswindows:
|
||||||
|
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||||
|
# strings.
|
||||||
|
|
||||||
|
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||||
|
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||||
|
# characters with '?'.
|
||||||
|
|
||||||
|
|
||||||
|
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||||
|
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||||
|
|
||||||
|
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||||
|
GetCommandLineW.argtypes = []
|
||||||
|
GetCommandLineW.restype = LPCWSTR
|
||||||
|
|
||||||
|
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||||
|
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||||
|
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||||
|
|
||||||
|
cmd = GetCommandLineW()
|
||||||
|
argc = c_int(0)
|
||||||
|
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||||
|
if argc.value > 0:
|
||||||
|
# Remove Python executable and commands if present
|
||||||
|
start = argc.value - len(sys.argv)
|
||||||
|
return [argv[i] for i in
|
||||||
|
xrange(start, argc.value)]
|
||||||
|
# if we don't have any arguments at all, just pass back script name
|
||||||
|
# this should never happen
|
||||||
|
return [u"mobidedrm.py"]
|
||||||
|
else:
|
||||||
|
argvencoding = sys.stdin.encoding
|
||||||
|
if argvencoding == None:
|
||||||
|
argvencoding = "utf-8"
|
||||||
|
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||||
|
|
||||||
|
# cleanup unicode filenames
|
||||||
|
# borrowed from calibre from calibre/src/calibre/__init__.py
|
||||||
|
# added in removal of control (<32) chars
|
||||||
|
# and removal of . at start and end
|
||||||
|
# and with some (heavily edited) code from Paul Durrant's kindlenamer.py
|
||||||
|
def cleanup_name(name):
|
||||||
|
# substitute filename unfriendly characters
|
||||||
|
name = name.replace(u"<",u"[").replace(u">",u"]").replace(u" : ",u" – ").replace(u": ",u" – ").replace(u":",u"—").replace(u"/",u"_").replace(u"\\",u"_").replace(u"|",u"_").replace(u"\"",u"\'")
|
||||||
|
# delete control characters
|
||||||
|
name = u"".join(char for char in name if ord(char)>=32)
|
||||||
|
# white space to single space, delete leading and trailing while space
|
||||||
|
name = re.sub(ur"\s", u" ", name).strip()
|
||||||
|
# remove leading dots
|
||||||
|
while len(name)>0 and name[0] == u".":
|
||||||
|
name = name[1:]
|
||||||
|
# remove trailing dots (Windows doesn't like them)
|
||||||
|
if name.endswith(u'.'):
|
||||||
|
name = name[:-1]
|
||||||
|
return name
|
||||||
|
|
||||||
|
# must be passed unicode
|
||||||
|
def unescape(text):
|
||||||
|
def fixup(m):
|
||||||
|
text = m.group(0)
|
||||||
|
if text[:2] == u"&#":
|
||||||
|
# character reference
|
||||||
|
try:
|
||||||
|
if text[:3] == u"&#x":
|
||||||
|
return unichr(int(text[3:-1], 16))
|
||||||
|
else:
|
||||||
|
return unichr(int(text[2:-1]))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# named entity
|
||||||
|
try:
|
||||||
|
text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
return text # leave as is
|
||||||
|
return re.sub(u"&#?\w+;", fixup, text)
|
||||||
|
|
||||||
|
def GetDecryptedBook(infile, kInfoFiles, serials, pids, starttime = time.time()):
|
||||||
|
# handle the obvious cases at the beginning
|
||||||
|
if not os.path.isfile(infile):
|
||||||
|
raise DRMException (u"Input file does not exist.")
|
||||||
|
|
||||||
|
mobi = True
|
||||||
|
magic3 = file(infile,'rb').read(3)
|
||||||
|
if magic3 == 'TPZ':
|
||||||
|
mobi = False
|
||||||
|
|
||||||
|
if mobi:
|
||||||
|
mb = mobidedrm.MobiBook(infile)
|
||||||
|
else:
|
||||||
|
mb = topazextract.TopazBook(infile)
|
||||||
|
|
||||||
|
bookname = unescape(mb.getBookTitle())
|
||||||
|
print u"Decrypting {1} ebook: {0}".format(bookname, mb.getBookType())
|
||||||
|
|
||||||
|
# extend PID list with book-specific PIDs
|
||||||
|
md1, md2 = mb.getPIDMetaInfo()
|
||||||
|
pids.extend(kgenpids.getPidList(md1, md2, serials, kInfoFiles))
|
||||||
|
print u"Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(pids))
|
||||||
|
|
||||||
|
try:
|
||||||
|
mb.processBook(pids)
|
||||||
|
except:
|
||||||
|
mb.cleanup
|
||||||
|
raise
|
||||||
|
|
||||||
|
print u"Decryption succeeded after {0:.1f} seconds".format(time.time()-starttime)
|
||||||
|
return mb
|
||||||
|
|
||||||
|
|
||||||
|
# infile, outdir and kInfoFiles should be unicode strings
|
||||||
|
def decryptBook(infile, outdir, kInfoFiles, serials, pids):
|
||||||
|
starttime = time.time()
|
||||||
|
print "Starting decryptBook routine."
|
||||||
|
try:
|
||||||
|
book = GetDecryptedBook(infile, kInfoFiles, serials, pids, starttime)
|
||||||
|
except Exception, e:
|
||||||
|
print u"Error decrypting book after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# if we're saving to the same folder as the original, use file name_
|
||||||
|
# if to a different folder, use book name
|
||||||
|
if os.path.normcase(os.path.normpath(outdir)) == os.path.normcase(os.path.normpath(os.path.dirname(infile))):
|
||||||
|
outfilename = os.path.splitext(os.path.basename(infile))[0]
|
||||||
|
else:
|
||||||
|
outfilename = cleanup_name(book.getBookTitle())
|
||||||
|
|
||||||
|
# avoid excessively long file names
|
||||||
|
if len(outfilename)>150:
|
||||||
|
outfilename = outfilename[:150]
|
||||||
|
|
||||||
|
outfilename = outfilename+u"_nodrm"
|
||||||
|
outfile = os.path.join(outdir, outfilename + book.getBookExtension())
|
||||||
|
|
||||||
|
book.getFile(outfile)
|
||||||
|
print u"Saved decrypted book {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename)
|
||||||
|
|
||||||
|
if book.getBookType()==u"Topaz":
|
||||||
|
zipname = os.path.join(outdir, outfilename + u"_SVG.zip")
|
||||||
|
book.getSVGZip(zipname)
|
||||||
|
print u"Saved SVG ZIP Archive for {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename)
|
||||||
|
|
||||||
|
# remove internal temporary directory of Topaz pieces
|
||||||
|
book.cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
def usage(progname):
|
||||||
|
print u"Removes DRM protection from Mobipocket, Amazon KF8, Amazon Print Replica and Amazon Topaz ebooks"
|
||||||
|
print u"Usage:"
|
||||||
|
print u" {0} [-k <kindle.info>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] <infile> <outdir>".format(progname)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Main
|
||||||
|
#
|
||||||
|
def cli_main(argv=unicode_argv()):
|
||||||
|
progname = os.path.basename(argv[0])
|
||||||
|
print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:")
|
||||||
|
except getopt.GetoptError, err:
|
||||||
|
print u"Error in options or arguments: {0}".format(err.args[0])
|
||||||
|
usage(progname)
|
||||||
|
sys.exit(2)
|
||||||
|
if len(args)<2:
|
||||||
|
usage(progname)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
infile = args[0]
|
||||||
|
outdir = args[1]
|
||||||
|
kInfoFiles = []
|
||||||
|
serials = []
|
||||||
|
pids = []
|
||||||
|
|
||||||
|
for o, a in opts:
|
||||||
|
if o == "-k":
|
||||||
|
if a == None :
|
||||||
|
raise DrmException("Invalid parameter for -k")
|
||||||
|
kInfoFiles.append(a)
|
||||||
|
if o == "-p":
|
||||||
|
if a == None :
|
||||||
|
raise DrmException("Invalid parameter for -p")
|
||||||
|
pids = a.split(',')
|
||||||
|
if o == "-s":
|
||||||
|
if a == None :
|
||||||
|
raise DrmException("Invalid parameter for -s")
|
||||||
|
serials = a.split(',')
|
||||||
|
|
||||||
|
# try with built in Kindle Info files if not on Linux
|
||||||
|
k4 = not sys.platform.startswith('linux')
|
||||||
|
|
||||||
|
return decryptBook(infile, outdir, kInfoFiles, serials, pids)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||||
|
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||||
|
sys.exit(cli_main())
|
||||||
278
Calibre_Plugins/K4MobiDeDRM_plugin/kgenpids.py
Normal file
278
Calibre_Plugins/K4MobiDeDRM_plugin/kgenpids.py
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import with_statement
|
||||||
|
import sys
|
||||||
|
import os, csv
|
||||||
|
import binascii
|
||||||
|
import zlib
|
||||||
|
import re
|
||||||
|
from struct import pack, unpack, unpack_from
|
||||||
|
|
||||||
|
class DrmException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
global charMap1
|
||||||
|
global charMap3
|
||||||
|
global charMap4
|
||||||
|
|
||||||
|
if 'calibre' in sys.modules:
|
||||||
|
inCalibre = True
|
||||||
|
from calibre.constants import iswindows, isosx
|
||||||
|
if iswindows:
|
||||||
|
from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||||
|
if isosx:
|
||||||
|
from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||||
|
else:
|
||||||
|
inCalibre = False
|
||||||
|
iswindows = sys.platform.startswith('win')
|
||||||
|
isosx = sys.platform.startswith('darwin')
|
||||||
|
if iswindows:
|
||||||
|
from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||||
|
if isosx:
|
||||||
|
from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||||
|
|
||||||
|
|
||||||
|
charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M'
|
||||||
|
charMap3 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||||
|
charMap4 = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
#
|
||||||
|
# PID generation routines
|
||||||
|
#
|
||||||
|
|
||||||
|
# Returns two bit at offset from a bit field
|
||||||
|
def getTwoBitsFromBitField(bitField,offset):
|
||||||
|
byteNumber = offset // 4
|
||||||
|
bitPosition = 6 - 2*(offset % 4)
|
||||||
|
return ord(bitField[byteNumber]) >> bitPosition & 3
|
||||||
|
|
||||||
|
# Returns the six bits at offset from a bit field
|
||||||
|
def getSixBitsFromBitField(bitField,offset):
|
||||||
|
offset *= 3
|
||||||
|
value = (getTwoBitsFromBitField(bitField,offset) <<4) + (getTwoBitsFromBitField(bitField,offset+1) << 2) +getTwoBitsFromBitField(bitField,offset+2)
|
||||||
|
return value
|
||||||
|
|
||||||
|
# 8 bits to six bits encoding from hash to generate PID string
|
||||||
|
def encodePID(hash):
|
||||||
|
global charMap3
|
||||||
|
PID = ''
|
||||||
|
for position in range (0,8):
|
||||||
|
PID += charMap3[getSixBitsFromBitField(hash,position)]
|
||||||
|
return PID
|
||||||
|
|
||||||
|
# Encryption table used to generate the device PID
|
||||||
|
def generatePidEncryptionTable() :
|
||||||
|
table = []
|
||||||
|
for counter1 in range (0,0x100):
|
||||||
|
value = counter1
|
||||||
|
for counter2 in range (0,8):
|
||||||
|
if (value & 1 == 0) :
|
||||||
|
value = value >> 1
|
||||||
|
else :
|
||||||
|
value = value >> 1
|
||||||
|
value = value ^ 0xEDB88320
|
||||||
|
table.append(value)
|
||||||
|
return table
|
||||||
|
|
||||||
|
# Seed value used to generate the device PID
|
||||||
|
def generatePidSeed(table,dsn) :
|
||||||
|
value = 0
|
||||||
|
for counter in range (0,4) :
|
||||||
|
index = (ord(dsn[counter]) ^ value) &0xFF
|
||||||
|
value = (value >> 8) ^ table[index]
|
||||||
|
return value
|
||||||
|
|
||||||
|
# Generate the device PID
|
||||||
|
def generateDevicePID(table,dsn,nbRoll):
|
||||||
|
global charMap4
|
||||||
|
seed = generatePidSeed(table,dsn)
|
||||||
|
pidAscii = ''
|
||||||
|
pid = [(seed >>24) &0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF,(seed>>24) & 0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF]
|
||||||
|
index = 0
|
||||||
|
for counter in range (0,nbRoll):
|
||||||
|
pid[index] = pid[index] ^ ord(dsn[counter])
|
||||||
|
index = (index+1) %8
|
||||||
|
for counter in range (0,8):
|
||||||
|
index = ((((pid[counter] >>5) & 3) ^ pid[counter]) & 0x1f) + (pid[counter] >> 7)
|
||||||
|
pidAscii += charMap4[index]
|
||||||
|
return pidAscii
|
||||||
|
|
||||||
|
def crc32(s):
|
||||||
|
return (~binascii.crc32(s,-1))&0xFFFFFFFF
|
||||||
|
|
||||||
|
# convert from 8 digit PID to 10 digit PID with checksum
|
||||||
|
def checksumPid(s):
|
||||||
|
global charMap4
|
||||||
|
crc = crc32(s)
|
||||||
|
crc = crc ^ (crc >> 16)
|
||||||
|
res = s
|
||||||
|
l = len(charMap4)
|
||||||
|
for i in (0,1):
|
||||||
|
b = crc & 0xff
|
||||||
|
pos = (b // l) ^ (b % l)
|
||||||
|
res += charMap4[pos%l]
|
||||||
|
crc >>= 8
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
# old kindle serial number to fixed pid
|
||||||
|
def pidFromSerial(s, l):
|
||||||
|
global charMap4
|
||||||
|
crc = crc32(s)
|
||||||
|
arr1 = [0]*l
|
||||||
|
for i in xrange(len(s)):
|
||||||
|
arr1[i%l] ^= ord(s[i])
|
||||||
|
crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff]
|
||||||
|
for i in xrange(l):
|
||||||
|
arr1[i] ^= crc_bytes[i&3]
|
||||||
|
pid = ""
|
||||||
|
for i in xrange(l):
|
||||||
|
b = arr1[i] & 0xff
|
||||||
|
pid+=charMap4[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))]
|
||||||
|
return pid
|
||||||
|
|
||||||
|
|
||||||
|
# Parse the EXTH header records and use the Kindle serial number to calculate the book pid.
|
||||||
|
def getKindlePids(rec209, token, serialnum):
|
||||||
|
pids=[]
|
||||||
|
|
||||||
|
# Compute book PID
|
||||||
|
pidHash = SHA1(serialnum+rec209+token)
|
||||||
|
bookPID = encodePID(pidHash)
|
||||||
|
bookPID = checksumPid(bookPID)
|
||||||
|
pids.append(bookPID)
|
||||||
|
|
||||||
|
# compute fixed pid for old pre 2.5 firmware update pid as well
|
||||||
|
kindlePID = pidFromSerial(serialnum, 7) + "*"
|
||||||
|
kindlePID = checksumPid(kindlePID)
|
||||||
|
pids.append(kindlePID)
|
||||||
|
|
||||||
|
return pids
|
||||||
|
|
||||||
|
|
||||||
|
# parse the Kindleinfo file to calculate the book pid.
|
||||||
|
|
||||||
|
keynames = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber']
|
||||||
|
|
||||||
|
def getK4Pids(rec209, token, kInfoFile):
|
||||||
|
global charMap1
|
||||||
|
kindleDatabase = None
|
||||||
|
pids = []
|
||||||
|
try:
|
||||||
|
kindleDatabase = getDBfromFile(kInfoFile)
|
||||||
|
except Exception, message:
|
||||||
|
print(message)
|
||||||
|
kindleDatabase = None
|
||||||
|
pass
|
||||||
|
|
||||||
|
if kindleDatabase == None :
|
||||||
|
return pids
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get the Mazama Random number
|
||||||
|
MazamaRandomNumber = kindleDatabase['MazamaRandomNumber']
|
||||||
|
|
||||||
|
# Get the kindle account token
|
||||||
|
kindleAccountToken = kindleDatabase['kindle.account.tokens']
|
||||||
|
except KeyError:
|
||||||
|
print u"Keys not found in {0}".format(os.path.basename(kInfoFile))
|
||||||
|
return pids
|
||||||
|
|
||||||
|
# Get the ID string used
|
||||||
|
encodedIDString = encodeHash(GetIDString(),charMap1)
|
||||||
|
|
||||||
|
# Get the current user name
|
||||||
|
encodedUsername = encodeHash(GetUserName(),charMap1)
|
||||||
|
|
||||||
|
# concat, hash and encode to calculate the DSN
|
||||||
|
DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1)
|
||||||
|
|
||||||
|
# Compute the device PID (for which I can tell, is used for nothing).
|
||||||
|
table = generatePidEncryptionTable()
|
||||||
|
devicePID = generateDevicePID(table,DSN,4)
|
||||||
|
devicePID = checksumPid(devicePID)
|
||||||
|
pids.append(devicePID)
|
||||||
|
|
||||||
|
# Compute book PIDs
|
||||||
|
|
||||||
|
# book pid
|
||||||
|
pidHash = SHA1(DSN+kindleAccountToken+rec209+token)
|
||||||
|
bookPID = encodePID(pidHash)
|
||||||
|
bookPID = checksumPid(bookPID)
|
||||||
|
pids.append(bookPID)
|
||||||
|
|
||||||
|
# variant 1
|
||||||
|
pidHash = SHA1(kindleAccountToken+rec209+token)
|
||||||
|
bookPID = encodePID(pidHash)
|
||||||
|
bookPID = checksumPid(bookPID)
|
||||||
|
pids.append(bookPID)
|
||||||
|
|
||||||
|
# variant 2
|
||||||
|
pidHash = SHA1(DSN+rec209+token)
|
||||||
|
bookPID = encodePID(pidHash)
|
||||||
|
bookPID = checksumPid(bookPID)
|
||||||
|
pids.append(bookPID)
|
||||||
|
|
||||||
|
return pids
|
||||||
|
|
||||||
|
def getPidList(md1, md2, serials=[], kInfoFiles=[]):
|
||||||
|
pidlst = []
|
||||||
|
if kInfoFiles is None:
|
||||||
|
kInfoFiles = []
|
||||||
|
if serials is None:
|
||||||
|
serials = []
|
||||||
|
if iswindows or isosx:
|
||||||
|
kInfoFiles.extend(getKindleInfoFiles())
|
||||||
|
for infoFile in kInfoFiles:
|
||||||
|
try:
|
||||||
|
pidlst.extend(getK4Pids(md1, md2, infoFile))
|
||||||
|
except Exception, e:
|
||||||
|
print u"Error getting PIDs from {0}: {1}".format(os.path.basename(infoFile),e.args[0])
|
||||||
|
for serialnum in serials:
|
||||||
|
try:
|
||||||
|
pidlst.extend(getKindlePids(md1, md2, serialnum))
|
||||||
|
except Exception, message:
|
||||||
|
print u"Error getting PIDs from serial number {0}: {1}".format(serialnum ,e.args[0])
|
||||||
|
return pidlst
|
||||||
142
Calibre_Plugins/K4MobiDeDRM_plugin/kindlepid.py
Normal file
142
Calibre_Plugins/K4MobiDeDRM_plugin/kindlepid.py
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Mobipocket PID calculator v0.4 for Amazon Kindle.
|
||||||
|
# Copyright (c) 2007, 2009 Igor Skochinsky <skochinsky@mail.ru>
|
||||||
|
# History:
|
||||||
|
# 0.1 Initial release
|
||||||
|
# 0.2 Added support for generating PID for iPhone (thanks to mbp)
|
||||||
|
# 0.3 changed to autoflush stdout, fixed return code usage
|
||||||
|
# 0.3 updated for unicode
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import binascii
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
iswindows = sys.platform.startswith('win')
|
||||||
|
isosx = sys.platform.startswith('darwin')
|
||||||
|
|
||||||
|
def unicode_argv():
|
||||||
|
if iswindows:
|
||||||
|
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||||
|
# strings.
|
||||||
|
|
||||||
|
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||||
|
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||||
|
# characters with '?'.
|
||||||
|
|
||||||
|
|
||||||
|
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||||
|
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||||
|
|
||||||
|
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||||
|
GetCommandLineW.argtypes = []
|
||||||
|
GetCommandLineW.restype = LPCWSTR
|
||||||
|
|
||||||
|
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||||
|
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||||
|
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||||
|
|
||||||
|
cmd = GetCommandLineW()
|
||||||
|
argc = c_int(0)
|
||||||
|
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||||
|
if argc.value > 0:
|
||||||
|
# Remove Python executable and commands if present
|
||||||
|
start = argc.value - len(sys.argv)
|
||||||
|
return [argv[i] for i in
|
||||||
|
xrange(start, argc.value)]
|
||||||
|
# if we don't have any arguments at all, just pass back script name
|
||||||
|
# this should never happen
|
||||||
|
return [u"mobidedrm.py"]
|
||||||
|
else:
|
||||||
|
argvencoding = sys.stdin.encoding
|
||||||
|
if argvencoding == None:
|
||||||
|
argvencoding = "utf-8"
|
||||||
|
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||||
|
|
||||||
|
if sys.hexversion >= 0x3000000:
|
||||||
|
print 'This script is incompatible with Python 3.x. Please install Python 2.7.x.'
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
|
||||||
|
|
||||||
|
def crc32(s):
|
||||||
|
return (~binascii.crc32(s,-1))&0xFFFFFFFF
|
||||||
|
|
||||||
|
def checksumPid(s):
|
||||||
|
crc = crc32(s)
|
||||||
|
crc = crc ^ (crc >> 16)
|
||||||
|
res = s
|
||||||
|
l = len(letters)
|
||||||
|
for i in (0,1):
|
||||||
|
b = crc & 0xff
|
||||||
|
pos = (b // l) ^ (b % l)
|
||||||
|
res += letters[pos%l]
|
||||||
|
crc >>= 8
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def pidFromSerial(s, l):
|
||||||
|
crc = crc32(s)
|
||||||
|
|
||||||
|
arr1 = [0]*l
|
||||||
|
for i in xrange(len(s)):
|
||||||
|
arr1[i%l] ^= ord(s[i])
|
||||||
|
|
||||||
|
crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff]
|
||||||
|
for i in xrange(l):
|
||||||
|
arr1[i] ^= crc_bytes[i&3]
|
||||||
|
|
||||||
|
pid = ''
|
||||||
|
for i in xrange(l):
|
||||||
|
b = arr1[i] & 0xff
|
||||||
|
pid+=letters[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))]
|
||||||
|
|
||||||
|
return pid
|
||||||
|
|
||||||
|
def cli_main(argv=unicode_argv()):
|
||||||
|
print u"Mobipocket PID calculator for Amazon Kindle. Copyright © 2007, 2009 Igor Skochinsky"
|
||||||
|
if len(sys.argv)==2:
|
||||||
|
serial = sys.argv[1]
|
||||||
|
else:
|
||||||
|
print u"Usage: kindlepid.py <Kindle Serial Number>/<iPhone/iPod Touch UDID>"
|
||||||
|
return 1
|
||||||
|
if len(serial)==16:
|
||||||
|
if serial.startswith("B"):
|
||||||
|
print u"Kindle serial number detected"
|
||||||
|
else:
|
||||||
|
print u"Warning: unrecognized serial number. Please recheck input."
|
||||||
|
return 1
|
||||||
|
pid = pidFromSerial(serial.encode("utf-8"),7)+'*'
|
||||||
|
print u"Mobipocket PID for Kindle serial#{0} is {1} ".format(serial,checksumPid(pid))
|
||||||
|
return 0
|
||||||
|
elif len(serial)==40:
|
||||||
|
print u"iPhone serial number (UDID) detected"
|
||||||
|
pid = pidFromSerial(serial.encode("utf-8"),8)
|
||||||
|
print u"Mobipocket PID for iPhone serial#{0} is {1} ".format(serial,checksumPid(pid))
|
||||||
|
return 0
|
||||||
|
print u"Warning: unrecognized serial number. Please recheck input."
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||||
|
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||||
|
sys.exit(cli_main())
|
||||||
BIN
Calibre_Plugins/K4MobiDeDRM_plugin/libalfcrypto.dylib
Normal file
BIN
Calibre_Plugins/K4MobiDeDRM_plugin/libalfcrypto.dylib
Normal file
Binary file not shown.
BIN
Calibre_Plugins/K4MobiDeDRM_plugin/libalfcrypto32.so
Normal file
BIN
Calibre_Plugins/K4MobiDeDRM_plugin/libalfcrypto32.so
Normal file
Binary file not shown.
BIN
Calibre_Plugins/K4MobiDeDRM_plugin/libalfcrypto64.so
Normal file
BIN
Calibre_Plugins/K4MobiDeDRM_plugin/libalfcrypto64.so
Normal file
Binary file not shown.
@@ -6,9 +6,11 @@ import csv
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import getopt
|
import getopt
|
||||||
|
import re
|
||||||
from struct import pack
|
from struct import pack
|
||||||
from struct import unpack
|
from struct import unpack
|
||||||
|
|
||||||
|
debug = False
|
||||||
|
|
||||||
class DocParser(object):
|
class DocParser(object):
|
||||||
def __init__(self, flatxml, fontsize, ph, pw):
|
def __init__(self, flatxml, fontsize, ph, pw):
|
||||||
@@ -81,6 +83,21 @@ class DocParser(object):
|
|||||||
pos = foundpos + 1
|
pos = foundpos + 1
|
||||||
return startpos
|
return startpos
|
||||||
|
|
||||||
|
# returns a vector of integers for the tagpath
|
||||||
|
def getData(self, tagpath, pos, end, clean=False):
|
||||||
|
if clean:
|
||||||
|
digits_only = re.compile(r'''([0-9]+)''')
|
||||||
|
argres=[]
|
||||||
|
(foundat, argt) = self.findinDoc(tagpath, pos, end)
|
||||||
|
if (argt != None) and (len(argt) > 0) :
|
||||||
|
argList = argt.split('|')
|
||||||
|
for strval in argList:
|
||||||
|
if clean:
|
||||||
|
m = re.search(digits_only, strval)
|
||||||
|
if m != None:
|
||||||
|
strval = m.group()
|
||||||
|
argres.append(int(strval))
|
||||||
|
return argres
|
||||||
|
|
||||||
def process(self):
|
def process(self):
|
||||||
|
|
||||||
@@ -97,7 +114,9 @@ class DocParser(object):
|
|||||||
|
|
||||||
# process each style converting what you can
|
# process each style converting what you can
|
||||||
|
|
||||||
|
if debug: print ' ', 'Processing styles.'
|
||||||
for j in xrange(stylecnt):
|
for j in xrange(stylecnt):
|
||||||
|
if debug: print ' ', 'Processing style %d' %(j)
|
||||||
start = styleList[j]
|
start = styleList[j]
|
||||||
end = styleList[j+1]
|
end = styleList[j+1]
|
||||||
|
|
||||||
@@ -116,6 +135,8 @@ class DocParser(object):
|
|||||||
else :
|
else :
|
||||||
sclass = ''
|
sclass = ''
|
||||||
|
|
||||||
|
if debug: print 'sclass', sclass
|
||||||
|
|
||||||
# check for any "after class" specifiers
|
# check for any "after class" specifiers
|
||||||
(pos, aftclass) = self.findinDoc('style._after_class',start,end)
|
(pos, aftclass) = self.findinDoc('style._after_class',start,end)
|
||||||
if aftclass != None:
|
if aftclass != None:
|
||||||
@@ -124,6 +145,8 @@ class DocParser(object):
|
|||||||
else :
|
else :
|
||||||
aftclass = ''
|
aftclass = ''
|
||||||
|
|
||||||
|
if debug: print 'aftclass', aftclass
|
||||||
|
|
||||||
cssargs = {}
|
cssargs = {}
|
||||||
|
|
||||||
while True :
|
while True :
|
||||||
@@ -131,6 +154,9 @@ class DocParser(object):
|
|||||||
(pos1, attr) = self.findinDoc('style.rule.attr', start, end)
|
(pos1, attr) = self.findinDoc('style.rule.attr', start, end)
|
||||||
(pos2, val) = self.findinDoc('style.rule.value', start, end)
|
(pos2, val) = self.findinDoc('style.rule.value', start, end)
|
||||||
|
|
||||||
|
if debug: print 'attr', attr
|
||||||
|
if debug: print 'val', val
|
||||||
|
|
||||||
if attr == None : break
|
if attr == None : break
|
||||||
|
|
||||||
if (attr == 'display') or (attr == 'pos') or (attr == 'align'):
|
if (attr == 'display') or (attr == 'pos') or (attr == 'align'):
|
||||||
@@ -149,6 +175,9 @@ class DocParser(object):
|
|||||||
elif attr == 'line-space':
|
elif attr == 'line-space':
|
||||||
scale = self.fontsize * 2.0
|
scale = self.fontsize * 2.0
|
||||||
|
|
||||||
|
if val == "":
|
||||||
|
val = 0
|
||||||
|
|
||||||
if not ((attr == 'hang') and (int(val) == 0)) :
|
if not ((attr == 'hang') and (int(val) == 0)) :
|
||||||
pv = float(val)/scale
|
pv = float(val)/scale
|
||||||
cssargs[attr] = (self.attr_val_map[attr], pv)
|
cssargs[attr] = (self.attr_val_map[attr], pv)
|
||||||
@@ -160,6 +189,7 @@ class DocParser(object):
|
|||||||
if aftclass != "" : keep = False
|
if aftclass != "" : keep = False
|
||||||
|
|
||||||
if keep :
|
if keep :
|
||||||
|
if debug: print 'keeping style'
|
||||||
# make sure line-space does not go below 100% or above 300% since
|
# make sure line-space does not go below 100% or above 300% since
|
||||||
# it can be wacky in some styles
|
# it can be wacky in some styles
|
||||||
if 'line-space' in cssargs:
|
if 'line-space' in cssargs:
|
||||||
@@ -237,7 +267,13 @@ def convert2CSS(flatxml, fontsize, ph, pw):
|
|||||||
|
|
||||||
# create a document parser
|
# create a document parser
|
||||||
dp = DocParser(flatxml, fontsize, ph, pw)
|
dp = DocParser(flatxml, fontsize, ph, pw)
|
||||||
|
if debug: print ' ', 'Created DocParser.'
|
||||||
csspage = dp.process()
|
csspage = dp.process()
|
||||||
|
if debug: print ' ', 'Processed DocParser.'
|
||||||
return csspage
|
return csspage
|
||||||
|
|
||||||
|
|
||||||
|
def getpageIDMap(flatxml):
|
||||||
|
dp = DocParser(flatxml, 0, 0, 0)
|
||||||
|
pageidnumbers = dp.getData('info.original.pid', 0, -1, True)
|
||||||
|
return pageidnumbers
|
||||||
@@ -146,4 +146,3 @@ class Process(object):
|
|||||||
self.__quit = True
|
self.__quit = True
|
||||||
self.__inputsem.release()
|
self.__inputsem.release()
|
||||||
self.__lock.release()
|
self.__lock.release()
|
||||||
|
|
||||||
533
Calibre_Plugins/K4MobiDeDRM_plugin/topazextract.py
Normal file
533
Calibre_Plugins/K4MobiDeDRM_plugin/topazextract.py
Normal file
@@ -0,0 +1,533 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# topazextract.py, version ?
|
||||||
|
# Mostly written by some_updates based on code from many others
|
||||||
|
|
||||||
|
__version__ = '4.8'
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os, csv, getopt
|
||||||
|
import zlib, zipfile, tempfile, shutil
|
||||||
|
import traceback
|
||||||
|
from struct import pack
|
||||||
|
from struct import unpack
|
||||||
|
from alfcrypto import Topaz_Cipher
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
iswindows = sys.platform.startswith('win')
|
||||||
|
isosx = sys.platform.startswith('darwin')
|
||||||
|
|
||||||
|
def unicode_argv():
|
||||||
|
if iswindows:
|
||||||
|
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||||
|
# strings.
|
||||||
|
|
||||||
|
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||||
|
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||||
|
# characters with '?'.
|
||||||
|
|
||||||
|
|
||||||
|
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||||
|
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||||
|
|
||||||
|
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||||
|
GetCommandLineW.argtypes = []
|
||||||
|
GetCommandLineW.restype = LPCWSTR
|
||||||
|
|
||||||
|
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||||
|
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||||
|
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||||
|
|
||||||
|
cmd = GetCommandLineW()
|
||||||
|
argc = c_int(0)
|
||||||
|
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||||
|
if argc.value > 0:
|
||||||
|
# Remove Python executable and commands if present
|
||||||
|
start = argc.value - len(sys.argv)
|
||||||
|
return [argv[i] for i in
|
||||||
|
xrange(start, argc.value)]
|
||||||
|
# if we don't have any arguments at all, just pass back script name
|
||||||
|
# this should never happen
|
||||||
|
return [u"mobidedrm.py"]
|
||||||
|
else:
|
||||||
|
argvencoding = sys.stdin.encoding
|
||||||
|
if argvencoding == None:
|
||||||
|
argvencoding = 'utf-8'
|
||||||
|
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||||
|
|
||||||
|
#global switch
|
||||||
|
debug = False
|
||||||
|
|
||||||
|
if 'calibre' in sys.modules:
|
||||||
|
inCalibre = True
|
||||||
|
from calibre_plugins.k4mobidedrm import kgenpids
|
||||||
|
else:
|
||||||
|
inCalibre = False
|
||||||
|
import kgenpids
|
||||||
|
|
||||||
|
|
||||||
|
class DrmException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# recursive zip creation support routine
|
||||||
|
def zipUpDir(myzip, tdir, localname):
|
||||||
|
currentdir = tdir
|
||||||
|
if localname != u"":
|
||||||
|
currentdir = os.path.join(currentdir,localname)
|
||||||
|
list = os.listdir(currentdir)
|
||||||
|
for file in list:
|
||||||
|
afilename = file
|
||||||
|
localfilePath = os.path.join(localname, afilename)
|
||||||
|
realfilePath = os.path.join(currentdir,file)
|
||||||
|
if os.path.isfile(realfilePath):
|
||||||
|
myzip.write(realfilePath, localfilePath)
|
||||||
|
elif os.path.isdir(realfilePath):
|
||||||
|
zipUpDir(myzip, tdir, localfilePath)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Utility routines
|
||||||
|
#
|
||||||
|
|
||||||
|
# Get a 7 bit encoded number from file
|
||||||
|
def bookReadEncodedNumber(fo):
|
||||||
|
flag = False
|
||||||
|
data = ord(fo.read(1))
|
||||||
|
if data == 0xFF:
|
||||||
|
flag = True
|
||||||
|
data = ord(fo.read(1))
|
||||||
|
if data >= 0x80:
|
||||||
|
datax = (data & 0x7F)
|
||||||
|
while data >= 0x80 :
|
||||||
|
data = ord(fo.read(1))
|
||||||
|
datax = (datax <<7) + (data & 0x7F)
|
||||||
|
data = datax
|
||||||
|
if flag:
|
||||||
|
data = -data
|
||||||
|
return data
|
||||||
|
|
||||||
|
# Get a length prefixed string from file
|
||||||
|
def bookReadString(fo):
|
||||||
|
stringLength = bookReadEncodedNumber(fo)
|
||||||
|
return unpack(str(stringLength)+'s',fo.read(stringLength))[0]
|
||||||
|
|
||||||
|
#
|
||||||
|
# crypto routines
|
||||||
|
#
|
||||||
|
|
||||||
|
# Context initialisation for the Topaz Crypto
|
||||||
|
def topazCryptoInit(key):
|
||||||
|
return Topaz_Cipher().ctx_init(key)
|
||||||
|
|
||||||
|
# ctx1 = 0x0CAFFE19E
|
||||||
|
# for keyChar in key:
|
||||||
|
# keyByte = ord(keyChar)
|
||||||
|
# ctx2 = ctx1
|
||||||
|
# ctx1 = ((((ctx1 >>2) * (ctx1 >>7))&0xFFFFFFFF) ^ (keyByte * keyByte * 0x0F902007)& 0xFFFFFFFF )
|
||||||
|
# return [ctx1,ctx2]
|
||||||
|
|
||||||
|
# decrypt data with the context prepared by topazCryptoInit()
|
||||||
|
def topazCryptoDecrypt(data, ctx):
|
||||||
|
return Topaz_Cipher().decrypt(data, ctx)
|
||||||
|
# ctx1 = ctx[0]
|
||||||
|
# ctx2 = ctx[1]
|
||||||
|
# plainText = ""
|
||||||
|
# for dataChar in data:
|
||||||
|
# dataByte = ord(dataChar)
|
||||||
|
# m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF
|
||||||
|
# ctx2 = ctx1
|
||||||
|
# ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF)
|
||||||
|
# plainText += chr(m)
|
||||||
|
# return plainText
|
||||||
|
|
||||||
|
# Decrypt data with the PID
|
||||||
|
def decryptRecord(data,PID):
|
||||||
|
ctx = topazCryptoInit(PID)
|
||||||
|
return topazCryptoDecrypt(data, ctx)
|
||||||
|
|
||||||
|
# Try to decrypt a dkey record (contains the bookPID)
|
||||||
|
def decryptDkeyRecord(data,PID):
|
||||||
|
record = decryptRecord(data,PID)
|
||||||
|
fields = unpack('3sB8sB8s3s',record)
|
||||||
|
if fields[0] != 'PID' or fields[5] != 'pid' :
|
||||||
|
raise DrmException(u"Didn't find PID magic numbers in record")
|
||||||
|
elif fields[1] != 8 or fields[3] != 8 :
|
||||||
|
raise DrmException(u"Record didn't contain correct length fields")
|
||||||
|
elif fields[2] != PID :
|
||||||
|
raise DrmException(u"Record didn't contain PID")
|
||||||
|
return fields[4]
|
||||||
|
|
||||||
|
# Decrypt all dkey records (contain the book PID)
|
||||||
|
def decryptDkeyRecords(data,PID):
|
||||||
|
nbKeyRecords = ord(data[0])
|
||||||
|
records = []
|
||||||
|
data = data[1:]
|
||||||
|
for i in range (0,nbKeyRecords):
|
||||||
|
length = ord(data[0])
|
||||||
|
try:
|
||||||
|
key = decryptDkeyRecord(data[1:length+1],PID)
|
||||||
|
records.append(key)
|
||||||
|
except DrmException:
|
||||||
|
pass
|
||||||
|
data = data[1+length:]
|
||||||
|
if len(records) == 0:
|
||||||
|
raise DrmException(u"BookKey Not Found")
|
||||||
|
return records
|
||||||
|
|
||||||
|
|
||||||
|
class TopazBook:
|
||||||
|
def __init__(self, filename):
|
||||||
|
self.fo = file(filename, 'rb')
|
||||||
|
self.outdir = tempfile.mkdtemp()
|
||||||
|
# self.outdir = 'rawdat'
|
||||||
|
self.bookPayloadOffset = 0
|
||||||
|
self.bookHeaderRecords = {}
|
||||||
|
self.bookMetadata = {}
|
||||||
|
self.bookKey = None
|
||||||
|
magic = unpack('4s',self.fo.read(4))[0]
|
||||||
|
if magic != 'TPZ0':
|
||||||
|
raise DrmException(u"Parse Error : Invalid Header, not a Topaz file")
|
||||||
|
self.parseTopazHeaders()
|
||||||
|
self.parseMetadata()
|
||||||
|
|
||||||
|
def parseTopazHeaders(self):
|
||||||
|
def bookReadHeaderRecordData():
|
||||||
|
# Read and return the data of one header record at the current book file position
|
||||||
|
# [[offset,decompressedLength,compressedLength],...]
|
||||||
|
nbValues = bookReadEncodedNumber(self.fo)
|
||||||
|
if debug: print "%d records in header " % nbValues,
|
||||||
|
values = []
|
||||||
|
for i in range (0,nbValues):
|
||||||
|
values.append([bookReadEncodedNumber(self.fo),bookReadEncodedNumber(self.fo),bookReadEncodedNumber(self.fo)])
|
||||||
|
return values
|
||||||
|
def parseTopazHeaderRecord():
|
||||||
|
# Read and parse one header record at the current book file position and return the associated data
|
||||||
|
# [[offset,decompressedLength,compressedLength],...]
|
||||||
|
if ord(self.fo.read(1)) != 0x63:
|
||||||
|
raise DrmException(u"Parse Error : Invalid Header")
|
||||||
|
tag = bookReadString(self.fo)
|
||||||
|
record = bookReadHeaderRecordData()
|
||||||
|
return [tag,record]
|
||||||
|
nbRecords = bookReadEncodedNumber(self.fo)
|
||||||
|
if debug: print "Headers: %d" % nbRecords
|
||||||
|
for i in range (0,nbRecords):
|
||||||
|
result = parseTopazHeaderRecord()
|
||||||
|
if debug: print result[0], ": ", result[1]
|
||||||
|
self.bookHeaderRecords[result[0]] = result[1]
|
||||||
|
if ord(self.fo.read(1)) != 0x64 :
|
||||||
|
raise DrmException(u"Parse Error : Invalid Header")
|
||||||
|
self.bookPayloadOffset = self.fo.tell()
|
||||||
|
|
||||||
|
def parseMetadata(self):
|
||||||
|
# Parse the metadata record from the book payload and return a list of [key,values]
|
||||||
|
self.fo.seek(self.bookPayloadOffset + self.bookHeaderRecords['metadata'][0][0])
|
||||||
|
tag = bookReadString(self.fo)
|
||||||
|
if tag != 'metadata' :
|
||||||
|
raise DrmException(u"Parse Error : Record Names Don't Match")
|
||||||
|
flags = ord(self.fo.read(1))
|
||||||
|
nbRecords = ord(self.fo.read(1))
|
||||||
|
if debug: print "Metadata Records: %d" % nbRecords
|
||||||
|
for i in range (0,nbRecords) :
|
||||||
|
keyval = bookReadString(self.fo)
|
||||||
|
content = bookReadString(self.fo)
|
||||||
|
if debug: print keyval
|
||||||
|
if debug: print content
|
||||||
|
self.bookMetadata[keyval] = content
|
||||||
|
return self.bookMetadata
|
||||||
|
|
||||||
|
def getPIDMetaInfo(self):
|
||||||
|
keysRecord = self.bookMetadata.get('keys','')
|
||||||
|
keysRecordRecord = ''
|
||||||
|
if keysRecord != '':
|
||||||
|
keylst = keysRecord.split(',')
|
||||||
|
for keyval in keylst:
|
||||||
|
keysRecordRecord += self.bookMetadata.get(keyval,'')
|
||||||
|
return keysRecord, keysRecordRecord
|
||||||
|
|
||||||
|
def getBookTitle(self):
|
||||||
|
title = ''
|
||||||
|
if 'Title' in self.bookMetadata:
|
||||||
|
title = self.bookMetadata['Title']
|
||||||
|
return title.decode('utf-8')
|
||||||
|
|
||||||
|
def setBookKey(self, key):
|
||||||
|
self.bookKey = key
|
||||||
|
|
||||||
|
def getBookPayloadRecord(self, name, index):
|
||||||
|
# Get a record in the book payload, given its name and index.
|
||||||
|
# decrypted and decompressed if necessary
|
||||||
|
encrypted = False
|
||||||
|
compressed = False
|
||||||
|
try:
|
||||||
|
recordOffset = self.bookHeaderRecords[name][index][0]
|
||||||
|
except:
|
||||||
|
raise DrmException("Parse Error : Invalid Record, record not found")
|
||||||
|
|
||||||
|
self.fo.seek(self.bookPayloadOffset + recordOffset)
|
||||||
|
|
||||||
|
tag = bookReadString(self.fo)
|
||||||
|
if tag != name :
|
||||||
|
raise DrmException("Parse Error : Invalid Record, record name doesn't match")
|
||||||
|
|
||||||
|
recordIndex = bookReadEncodedNumber(self.fo)
|
||||||
|
if recordIndex < 0 :
|
||||||
|
encrypted = True
|
||||||
|
recordIndex = -recordIndex -1
|
||||||
|
|
||||||
|
if recordIndex != index :
|
||||||
|
raise DrmException("Parse Error : Invalid Record, index doesn't match")
|
||||||
|
|
||||||
|
if (self.bookHeaderRecords[name][index][2] > 0):
|
||||||
|
compressed = True
|
||||||
|
record = self.fo.read(self.bookHeaderRecords[name][index][2])
|
||||||
|
else:
|
||||||
|
record = self.fo.read(self.bookHeaderRecords[name][index][1])
|
||||||
|
|
||||||
|
if encrypted:
|
||||||
|
if self.bookKey:
|
||||||
|
ctx = topazCryptoInit(self.bookKey)
|
||||||
|
record = topazCryptoDecrypt(record,ctx)
|
||||||
|
else :
|
||||||
|
raise DrmException("Error: Attempt to decrypt without bookKey")
|
||||||
|
|
||||||
|
if compressed:
|
||||||
|
record = zlib.decompress(record)
|
||||||
|
|
||||||
|
return record
|
||||||
|
|
||||||
|
def processBook(self, pidlst):
|
||||||
|
raw = 0
|
||||||
|
fixedimage=True
|
||||||
|
try:
|
||||||
|
keydata = self.getBookPayloadRecord('dkey', 0)
|
||||||
|
except DrmException, e:
|
||||||
|
print u"no dkey record found, book may not be encrypted"
|
||||||
|
print u"attempting to extrct files without a book key"
|
||||||
|
self.createBookDirectory()
|
||||||
|
self.extractFiles()
|
||||||
|
print u"Successfully Extracted Topaz contents"
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.k4mobidedrm import genbook
|
||||||
|
else:
|
||||||
|
import genbook
|
||||||
|
|
||||||
|
rv = genbook.generateBook(self.outdir, raw, fixedimage)
|
||||||
|
if rv == 0:
|
||||||
|
print u"Book Successfully generated."
|
||||||
|
return rv
|
||||||
|
|
||||||
|
# try each pid to decode the file
|
||||||
|
bookKey = None
|
||||||
|
for pid in pidlst:
|
||||||
|
# use 8 digit pids here
|
||||||
|
pid = pid[0:8]
|
||||||
|
print u"Trying: {0}".format(pid)
|
||||||
|
bookKeys = []
|
||||||
|
data = keydata
|
||||||
|
try:
|
||||||
|
bookKeys+=decryptDkeyRecords(data,pid)
|
||||||
|
except DrmException, e:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
bookKey = bookKeys[0]
|
||||||
|
print u"Book Key Found! ({0})".format(bookKey.encode('hex'))
|
||||||
|
break
|
||||||
|
|
||||||
|
if not bookKey:
|
||||||
|
raise DrmException(u"No key found in {0:d} keys tried. Read the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(len(pidlst)))
|
||||||
|
|
||||||
|
self.setBookKey(bookKey)
|
||||||
|
self.createBookDirectory()
|
||||||
|
self.extractFiles()
|
||||||
|
print u"Successfully Extracted Topaz contents"
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.k4mobidedrm import genbook
|
||||||
|
else:
|
||||||
|
import genbook
|
||||||
|
|
||||||
|
rv = genbook.generateBook(self.outdir, raw, fixedimage)
|
||||||
|
if rv == 0:
|
||||||
|
print u"Book Successfully generated"
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def createBookDirectory(self):
|
||||||
|
outdir = self.outdir
|
||||||
|
# create output directory structure
|
||||||
|
if not os.path.exists(outdir):
|
||||||
|
os.makedirs(outdir)
|
||||||
|
destdir = os.path.join(outdir,u"img")
|
||||||
|
if not os.path.exists(destdir):
|
||||||
|
os.makedirs(destdir)
|
||||||
|
destdir = os.path.join(outdir,u"color_img")
|
||||||
|
if not os.path.exists(destdir):
|
||||||
|
os.makedirs(destdir)
|
||||||
|
destdir = os.path.join(outdir,u"page")
|
||||||
|
if not os.path.exists(destdir):
|
||||||
|
os.makedirs(destdir)
|
||||||
|
destdir = os.path.join(outdir,u"glyphs")
|
||||||
|
if not os.path.exists(destdir):
|
||||||
|
os.makedirs(destdir)
|
||||||
|
|
||||||
|
def extractFiles(self):
|
||||||
|
outdir = self.outdir
|
||||||
|
for headerRecord in self.bookHeaderRecords:
|
||||||
|
name = headerRecord
|
||||||
|
if name != 'dkey':
|
||||||
|
ext = u".dat"
|
||||||
|
if name == 'img': ext = u".jpg"
|
||||||
|
if name == 'color' : ext = u".jpg"
|
||||||
|
print u"Processing Section: {0}\n. . .".format(name),
|
||||||
|
for index in range (0,len(self.bookHeaderRecords[name])) :
|
||||||
|
fname = u"{0}{1:04d}{2}".format(name,index,ext)
|
||||||
|
destdir = outdir
|
||||||
|
if name == 'img':
|
||||||
|
destdir = os.path.join(outdir,u"img")
|
||||||
|
if name == 'color':
|
||||||
|
destdir = os.path.join(outdir,u"color_img")
|
||||||
|
if name == 'page':
|
||||||
|
destdir = os.path.join(outdir,u"page")
|
||||||
|
if name == 'glyphs':
|
||||||
|
destdir = os.path.join(outdir,u"glyphs")
|
||||||
|
outputFile = os.path.join(destdir,fname)
|
||||||
|
print u".",
|
||||||
|
record = self.getBookPayloadRecord(name,index)
|
||||||
|
if record != '':
|
||||||
|
file(outputFile, 'wb').write(record)
|
||||||
|
print u" "
|
||||||
|
|
||||||
|
def getFile(self, zipname):
|
||||||
|
htmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||||
|
htmlzip.write(os.path.join(self.outdir,u"book.html"),u"book.html")
|
||||||
|
htmlzip.write(os.path.join(self.outdir,u"book.opf"),u"book.opf")
|
||||||
|
if os.path.isfile(os.path.join(self.outdir,u"cover.jpg")):
|
||||||
|
htmlzip.write(os.path.join(self.outdir,u"cover.jpg"),u"cover.jpg")
|
||||||
|
htmlzip.write(os.path.join(self.outdir,u"style.css"),u"style.css")
|
||||||
|
zipUpDir(htmlzip, self.outdir, u"img")
|
||||||
|
htmlzip.close()
|
||||||
|
|
||||||
|
def getBookType(self):
|
||||||
|
return u"Topaz"
|
||||||
|
|
||||||
|
def getBookExtension(self):
|
||||||
|
return u".htmlz"
|
||||||
|
|
||||||
|
def getSVGZip(self, zipname):
|
||||||
|
svgzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||||
|
svgzip.write(os.path.join(self.outdir,u"index_svg.xhtml"),u"index_svg.xhtml")
|
||||||
|
zipUpDir(svgzip, self.outdir, u"svg")
|
||||||
|
zipUpDir(svgzip, self.outdir, u"img")
|
||||||
|
svgzip.close()
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
if os.path.isdir(self.outdir):
|
||||||
|
shutil.rmtree(self.outdir, True)
|
||||||
|
|
||||||
|
def usage(progname):
|
||||||
|
print u"Removes DRM protection from Topaz ebooks and extracts the contents"
|
||||||
|
print u"Usage:"
|
||||||
|
print u" {0} [-k <kindle.info>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] <infile> <outdir>".format(progname)
|
||||||
|
|
||||||
|
# Main
|
||||||
|
def cli_main(argv=unicode_argv()):
|
||||||
|
progname = os.path.basename(argv[0])
|
||||||
|
print u"TopazExtract v{0}.".format(__version__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:x")
|
||||||
|
except getopt.GetoptError, err:
|
||||||
|
print u"Error in options or arguments: {0}".format(err.args[0])
|
||||||
|
usage(progname)
|
||||||
|
return 1
|
||||||
|
if len(args)<2:
|
||||||
|
usage(progname)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
infile = args[0]
|
||||||
|
outdir = args[1]
|
||||||
|
if not os.path.isfile(infile):
|
||||||
|
print u"Input File {0} Does Not Exist.".format(infile)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if not os.path.exists(outdir):
|
||||||
|
print u"Output Directory {0} Does Not Exist.".format(outdir)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
kInfoFiles = []
|
||||||
|
serials = []
|
||||||
|
pids = []
|
||||||
|
|
||||||
|
for o, a in opts:
|
||||||
|
if o == '-k':
|
||||||
|
if a == None :
|
||||||
|
raise DrmException("Invalid parameter for -k")
|
||||||
|
kInfoFiles.append(a)
|
||||||
|
if o == '-p':
|
||||||
|
if a == None :
|
||||||
|
raise DrmException("Invalid parameter for -p")
|
||||||
|
pids = a.split(',')
|
||||||
|
if o == '-s':
|
||||||
|
if a == None :
|
||||||
|
raise DrmException("Invalid parameter for -s")
|
||||||
|
serials = [serial.replace(" ","") for serial in a.split(',')]
|
||||||
|
|
||||||
|
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||||
|
|
||||||
|
tb = TopazBook(infile)
|
||||||
|
title = tb.getBookTitle()
|
||||||
|
print u"Processing Book: {0}".format(title)
|
||||||
|
md1, md2 = tb.getPIDMetaInfo()
|
||||||
|
pids.extend(kgenpids.getPidList(md1, md2, serials, kInfoFiles))
|
||||||
|
|
||||||
|
try:
|
||||||
|
print u"Decrypting Book"
|
||||||
|
tb.processBook(pids)
|
||||||
|
|
||||||
|
print u" Creating HTML ZIP Archive"
|
||||||
|
zipname = os.path.join(outdir, bookname + u"_nodrm.htmlz")
|
||||||
|
tb.getFile(zipname)
|
||||||
|
|
||||||
|
print u" Creating SVG ZIP Archive"
|
||||||
|
zipname = os.path.join(outdir, bookname + u"_SVG.zip")
|
||||||
|
tb.getSVGZip(zipname)
|
||||||
|
|
||||||
|
# removing internal temporary directory of pieces
|
||||||
|
tb.cleanup()
|
||||||
|
|
||||||
|
except DrmException, e:
|
||||||
|
print u"Decryption failed\n{0}".format(traceback.format_exc())
|
||||||
|
|
||||||
|
try:
|
||||||
|
tb.cleanup()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return 1
|
||||||
|
|
||||||
|
except Exception, e:
|
||||||
|
print u"Decryption failed\m{0}".format(traceback.format_exc())
|
||||||
|
try:
|
||||||
|
tb.cleanup()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||||
|
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||||
|
sys.exit(cli_main())
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
Plugin for K4PC, K4Mac and Mobi Books
|
|
||||||
|
|
||||||
Will work on Linux (standard DRM Mobi books only), Mac OS X (standard DRM Mobi books and "Kindle for Mac" books, and Windows (standard DRM Mobi books and "Kindle for PC" books.
|
|
||||||
|
|
||||||
This plugin supersedes MobiDeDRM, K4DeDRM, and K4PCDeDRM plugins. If you install this plugin, those plugins can be safely removed.
|
|
||||||
|
|
||||||
This plugin is meant to convert "Kindle for PC", "Kindle for Mac" and "Mobi" ebooks with DRM to unlocked Mobi files. Calibre can then convert them to whatever format you desire. It is meant to function without having to install any dependencies except for Calibre being on your same machine and in the same account as your "Kindle for PC" or "Kindle for Mac" application if you are going to remove the DRM from those types of books.
|
|
||||||
|
|
||||||
Installation:
|
|
||||||
Go to Calibre's Preferences page... click on the Plugins button. Use the file dialog button to select the plugin's zip file (k4mobidedrm_vXX_plugin.zip) and click the 'Add' button. You're done.
|
|
||||||
|
|
||||||
Configuration:
|
|
||||||
Highlight the plugin (K4MobiDeDRM under the "File type plugins" category) and click the "Customize Plugin" button on Calibre's Preferences->Plugins page. Enter a comma separated list of your 10 digit PIDs. This is not needed if you only want to decode "Kindle for PC" or "Kindle for Mac" books.
|
|
||||||
|
|
||||||
|
|
||||||
Troubleshooting:
|
|
||||||
If you find that it's not working for you (imported azw's are not converted to mobi format), you can save a lot of time and trouble by trying to add the azw file 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. ;)
|
|
||||||
|
|
||||||
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.azw". Don't type the quotes and obviously change the 'your_ebook.azw' to whatever the filename of your book is. Copy the resulting output and paste it into any online help request you make.
|
|
||||||
|
|
||||||
** 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.
|
|
||||||
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
eReader PDB2PML - eReaderPDB2PML_vXX_plugin.zip
|
|
||||||
|
|
||||||
All credit given to The Dark Reverser for the original standalone script. I had the much easier job of converting it to a Calibre 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. I've included the psyco libraries (compiled for each platform) for speed. If your system can use them, great! Otherwise, they won't be used and things will just work slower.
|
|
||||||
|
|
||||||
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 under the "File type plugins" category) and click the "Customize Plugin" button on Calibre's Preferences->Plugins page. Enter your name and 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!!)
|
|
||||||
|
|
||||||
Troubleshooting:
|
|
||||||
If you find that it's not working for you (imported pdb's are not converted to pmlz format), you can save a lot of time and trouble by trying to add the pdb 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. ;)
|
|
||||||
|
|
||||||
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.pdb". Don't type the quotes and obviously change the 'your_ebook.pdb' to whatever the filename of your book is. Copy the resulting output and paste it into any online help request you make.
|
|
||||||
|
|
||||||
** 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.
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
Ignoble Epub DeDRM - ignobleepub_vXX_plugin.zip
|
|
||||||
Requires Calibre version 0.6.44 or higher.
|
|
||||||
|
|
||||||
All credit given to I <3 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 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.
|
|
||||||
|
|
||||||
Installation:
|
|
||||||
|
|
||||||
Go to Calibre's Preferences page... click on the Plugins button. Use the file dialog button to select the plugin's zip file (ignobleepub_vXX_plugin.zip) and
|
|
||||||
click the 'Add' button. you're done.
|
|
||||||
|
|
||||||
Configuration:
|
|
||||||
|
|
||||||
1) The easiest way to configure the plugin is to enter your name (Barnes & Noble account name) and credit card number (the one used to purchase the books) into the plugin's customization window. It's the same info you would enter into the ignoblekeygen script. Highlight the plugin (Ignoble Epub DeDRM) and click the "Customize Plugin" button on
|
|
||||||
Calibre's Preferences->Plugins page. Enter the name and credit card number separated by a comma: Your Name,1234123412341234
|
|
||||||
|
|
||||||
If you've purchased books with more than one credit card, separate that other info with a colon: Your Name,1234123412341234:Other Name,2345234523452345
|
|
||||||
|
|
||||||
** NOTE ** The above method is your only option if you don't have/can't run the original I <3 Cabbages scripts on your particular machine.
|
|
||||||
|
|
||||||
** NOTE ** Your credit card number will be on display in Calibre's Plugin configuration page when using the above method. If other people have access to your computer, you may want to use the second configuration method below.
|
|
||||||
|
|
||||||
2) If you already have keyfiles generated with I <3 Cabbages' ignoblekeygen.pyw script, you can put those keyfiles into 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 '.b64' extension (like the ignoblekeygen script produces). This directory isn't touched when upgrading Calibre, so it's quite safe to leave them there.
|
|
||||||
|
|
||||||
All keyfiles from method 2 and all data entered from method 1 will be used to attempt to decrypt a book. You can use method 1 or method 2, or a combination of both.
|
|
||||||
|
|
||||||
Troubleshooting:
|
|
||||||
|
|
||||||
If you find that it's not working for you (imported 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. ;)
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
** 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.
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
Inept Epub DeDRM - ineptepub_vXX_plugin.zip
|
|
||||||
Requires Calibre version 0.6.44 or higher.
|
|
||||||
|
|
||||||
All credit given to I <3 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.
|
|
||||||
|
|
||||||
Installation:
|
|
||||||
|
|
||||||
Go to Calibre's Preferences page... click on the Plugins button. Use the file dialog button to select the plugin's zip file (ineptepub_vXX_plugin.zip) and click the 'Add' button. you're done.
|
|
||||||
|
|
||||||
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 an 'adeptkey.der' file and save it in Calibre's configuration directory. It will use that file on subsequent runs. If there are already '*.der' files in the directory, the plugin won't attempt to
|
|
||||||
find the Adobe Digital Editions installation installation.
|
|
||||||
|
|
||||||
So if you have Adobe Digital Editions installation installed on the same machine as Calibre... you are ready to go. If not... keep reading.
|
|
||||||
|
|
||||||
If you already have keyfiles generated with I <3 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 Epub DeDRM plugin.
|
|
||||||
|
|
||||||
Troubleshooting:
|
|
||||||
|
|
||||||
If you find that it's not working for you (imported 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. ;)
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
** 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.
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
Installing openssl on Windows 64-bit (Windows 2000 and higher)
|
|
||||||
|
|
||||||
Win64 OpenSSL v0.9.8o (8Mb)
|
|
||||||
http://www.slproweb.com/download/Win64OpenSSL-0_9_8o.exe
|
|
||||||
(if you get an error message about missing Visual C++ redistributables... cancel the install and install the below support program from Microsoft, THEN install OpenSSL)
|
|
||||||
|
|
||||||
Visual C++ 2008 Redistributables (x64) (1.7Mb)
|
|
||||||
http://www.microsoft.com/downloads/details.aspx?familyid=bd2a6171-e2d6-4230-b809-9a8d7548c1b6
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Installing openssl on Windows 32-bit (Windows 2000 and higher)
|
|
||||||
|
|
||||||
Win32 OpenSSL v0.9.8o (8Mb)
|
|
||||||
http://www.slproweb.com/download/Win32OpenSSL-0_9_8o.exe
|
|
||||||
(if you get an error message about missing Visual C++ redistributables... cancel the install and install the below support program from Microsoft, THEN install OpenSSL)
|
|
||||||
|
|
||||||
Visual C++ 2008 Redistributables (1.7Mb)
|
|
||||||
http://www.microsoft.com/downloads/details.aspx?familyid=9B2DA534-3E03-4391-8A4D-074B9F2BC1BF
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Other versions of OpenSSL (and versions for Windows older than Windows 2000) can be found on the following website.
|
|
||||||
|
|
||||||
Shining Light Productions
|
|
||||||
http://www.slproweb.com/products/Win32OpenSSL.html
|
|
||||||
42
Calibre_Plugins/eReaderPDB2PML ReadMe.txt
Normal file
42
Calibre_Plugins/eReaderPDB2PML ReadMe.txt
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
eReader PDB2PML - eReaderPDB2PML_v08_plugin.zip
|
||||||
|
===============================================
|
||||||
|
|
||||||
|
All credit given to The Dark Reverser for the original standalone script. I had the much easier job of converting it to a Calibre 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. I've included the psyco libraries (compiled for each platform) for speed. If your system can use them, great! Otherwise, they won't be used and things will just work slower.
|
||||||
|
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior" to go to Calibre's Preferences page. Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (eReaderPDB2PML_v08_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog.
|
||||||
|
|
||||||
|
|
||||||
|
Customization
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Highlight the plugin (eReader PDB 2 PML under the "File type plugins" category) and click the "Customize Plugin" button on Calibre's Preferences->Plugins page. Enter your name and 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
|
||||||
|
|
||||||
|
|
||||||
|
Troubleshooting
|
||||||
|
---------------
|
||||||
|
|
||||||
|
If you find that it's not working for you (imported ebooks still have DRM), you can save a lot of time and trouble by first deleting the DRMed ebook from calibre and then trying to add the ebook 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. ;)
|
||||||
|
|
||||||
|
On Macintosh only you must first run calibre, open Preferences, open Miscellaneous, and click on the “Install command line tools” button. (On Windows and Linux the command line tools are installed automatically.)
|
||||||
|
|
||||||
|
On Windows, open a terminal/command window. (Start/Run… and then type ‘cmd’ (without the ‘s) as the program to run).
|
||||||
|
On Macintosh, open the Terminal application (in your Utilities folder).
|
||||||
|
On Linux open a command window. Hopefully all Linux users know how to do this, as I do not.
|
||||||
|
|
||||||
|
You should now have a text-based command-line window open. Also have open the folder containing the ebook to be imported. Make sure that book isn’t already in calibre, and that calibre isn’t running.
|
||||||
|
|
||||||
|
Now type in "calibredb add " (without the " but don’t miss that final space) and now drag the book to be imported onto the window. The full path to the book should be inserted into the command line. Now press the return/enter key. The import routines will run and produce some logging information.
|
||||||
|
|
||||||
|
Now copy the output from the terminal window.
|
||||||
|
On Windows, you must use the window menu (little icon at left of window bar) to select all the text and then to copy it.
|
||||||
|
On Macintosh and Linux, just use the normal text select and copy commands.
|
||||||
|
|
||||||
|
Paste the information into a comment at my blog, http://apprenticealf.wordpress.com/ describing your problem.
|
||||||
Binary file not shown.
124
Calibre_Plugins/eReaderPDB2PML_plugin/__init__.py
Normal file
124
Calibre_Plugins/eReaderPDB2PML_plugin/__init__.py
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
#!/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)"
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
# eReaderPDB2PML_plugin.py
|
|
||||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
|
||||||
# later. <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. I've included the psyco libraries
|
|
||||||
# (compiled for each platform) for speed. If your system can use them, great!
|
|
||||||
# Otherwise, they won't be used and things will just work slower.
|
|
||||||
#
|
|
||||||
# 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
|
|
||||||
|
|
||||||
import sys, os
|
|
||||||
|
|
||||||
from calibre.customize import FileTypePlugin
|
|
||||||
|
|
||||||
class eRdrDeDRM(FileTypePlugin):
|
|
||||||
name = 'eReader PDB 2 PML' # Name of the plugin
|
|
||||||
description = '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 = 'DiapDealer' # The author of this plugin
|
|
||||||
version = (0, 0, 2) # The version number of this plugin
|
|
||||||
file_types = set(['pdb']) # The file types that this plugin will be applied to
|
|
||||||
on_import = True # Run this plugin during the import
|
|
||||||
|
|
||||||
def run(self, path_to_ebook):
|
|
||||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
|
||||||
from calibre.constants import iswindows, isosx
|
|
||||||
pdir = 'windows' if iswindows else 'osx' if isosx else 'linux'
|
|
||||||
ppath = os.path.join(self.sys_insertion_path, pdir)
|
|
||||||
sys.path.insert(0, ppath)
|
|
||||||
|
|
||||||
global bookname, erdr2pml
|
|
||||||
import erdr2pml
|
|
||||||
|
|
||||||
if 'psyco' in sys.modules:
|
|
||||||
print 'Using psyco acceleration for %s.' % pdir
|
|
||||||
else:
|
|
||||||
print 'NOT using psyco acceleration for %s. Conversion may be slow.' % pdir
|
|
||||||
|
|
||||||
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:
|
|
||||||
keydata = self.site_customization
|
|
||||||
ar = keydata.split(':')
|
|
||||||
for i in ar:
|
|
||||||
try:
|
|
||||||
name, cc = i.split(',')
|
|
||||||
except ValueError:
|
|
||||||
sys.path.remove(ppath)
|
|
||||||
print ' Error parsing user supplied data.'
|
|
||||||
return path_to_ebook
|
|
||||||
|
|
||||||
try:
|
|
||||||
print "Processing..."
|
|
||||||
import time
|
|
||||||
start_time = time.time()
|
|
||||||
pmlfilepath = self.convertEreaderToPml(infile, name, cc, outdir)
|
|
||||||
|
|
||||||
if pmlfilepath and pmlfilepath != 1:
|
|
||||||
import zipfile
|
|
||||||
import shutil
|
|
||||||
print " Creating PMLZ file"
|
|
||||||
myZipFile = zipfile.ZipFile(pmlzfile.name,'w',zipfile.ZIP_STORED, False)
|
|
||||||
list = os.listdir(outdir)
|
|
||||||
for file in list:
|
|
||||||
localname = file
|
|
||||||
filePath = os.path.join(outdir,file)
|
|
||||||
if os.path.isfile(filePath):
|
|
||||||
myZipFile.write(filePath, localname)
|
|
||||||
elif os.path.isdir(filePath):
|
|
||||||
imageList = os.listdir(filePath)
|
|
||||||
localimgdir = os.path.basename(filePath)
|
|
||||||
for image in imageList:
|
|
||||||
localname = os.path.join(localimgdir,image)
|
|
||||||
imagePath = os.path.join(filePath,image)
|
|
||||||
if os.path.isfile(imagePath):
|
|
||||||
myZipFile.write(imagePath, localname)
|
|
||||||
myZipFile.close()
|
|
||||||
end_time = time.time()
|
|
||||||
search_time = end_time - start_time
|
|
||||||
print 'elapsed time: %.2f seconds' % (search_time, )
|
|
||||||
print "done"
|
|
||||||
return pmlzfile.name
|
|
||||||
else:
|
|
||||||
raise ValueError('Error Creating PML file.')
|
|
||||||
except ValueError, e:
|
|
||||||
print "Error: %s" % e
|
|
||||||
pass
|
|
||||||
raise Exception('Couldn\'t decrypt pdb file.')
|
|
||||||
else:
|
|
||||||
raise Exception('No name and CC# provided.')
|
|
||||||
|
|
||||||
def convertEreaderToPml(self, infile, name, cc, outdir):
|
|
||||||
|
|
||||||
print " Decoding File"
|
|
||||||
sect = erdr2pml.Sectionizer(infile, 'PNRdPPrs')
|
|
||||||
er = erdr2pml.EreaderProcessor(sect.loadSection, name, cc)
|
|
||||||
|
|
||||||
if er.getNumImages() > 0:
|
|
||||||
print " Extracting images"
|
|
||||||
#imagedir = bookname + '_img/'
|
|
||||||
imagedir = 'images/'
|
|
||||||
imagedirpath = os.path.join(outdir,imagedir)
|
|
||||||
if not os.path.exists(imagedirpath):
|
|
||||||
os.makedirs(imagedirpath)
|
|
||||||
for i in xrange(er.getNumImages()):
|
|
||||||
name, contents = er.getImage(i)
|
|
||||||
file(os.path.join(imagedirpath, name), 'wb').write(contents)
|
|
||||||
|
|
||||||
print " Extracting pml"
|
|
||||||
pml_string = er.getText()
|
|
||||||
pmlfilename = bookname + ".pml"
|
|
||||||
try:
|
|
||||||
file(os.path.join(outdir, pmlfilename),'wb').write(erdr2pml.cleanPML(pml_string))
|
|
||||||
return os.path.join(outdir, pmlfilename)
|
|
||||||
except:
|
|
||||||
return 1
|
|
||||||
|
|
||||||
def customization_help(self, gui=False):
|
|
||||||
return 'Enter Account Name & Last 8 digits of Credit Card number (separate with a comma)'
|
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
# -*- coding: utf-8 -*-
|
||||||
#
|
|
||||||
# erdr2pml.py
|
# erdr2pml.py
|
||||||
|
# Copyright © 2008 The Dark Reverser
|
||||||
#
|
#
|
||||||
|
# Modified 2008–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||||
|
|
||||||
# This is a python script. You need a Python interpreter to run it.
|
# This is a python script. You need a Python interpreter to run it.
|
||||||
# For example, ActiveState Python, which exists for windows.
|
# For example, ActiveState Python, which exists for windows.
|
||||||
# Changelog
|
# Changelog
|
||||||
@@ -55,43 +58,128 @@
|
|||||||
# 0.14 - contributed enhancement to support --make-pmlz switch
|
# 0.14 - contributed enhancement to support --make-pmlz switch
|
||||||
# 0.15 - enabled high-ascii to pml character encoding. DropBook now works on Mac.
|
# 0.15 - enabled high-ascii to pml character encoding. DropBook now works on Mac.
|
||||||
# 0.16 - convert to use openssl DES (very very fast) or pure python DES if openssl's libcrypto is not available
|
# 0.16 - convert to use openssl DES (very very fast) or pure python DES if openssl's libcrypto is not available
|
||||||
|
# 0.17 - added support for pycrypto's DES as well
|
||||||
|
# 0.18 - on Windows try PyCrypto first and OpenSSL next
|
||||||
|
# 0.19 - Modify the interface to allow use of import
|
||||||
|
# 0.20 - modify to allow use inside new interface for calibre plugins
|
||||||
|
# 0.21 - Support eReader (drm) version 11.
|
||||||
|
# - Don't reject dictionary format.
|
||||||
|
# - Ignore sidebars for dictionaries (different format?)
|
||||||
|
# 0.22 - Unicode and plugin support, different image folders for PMLZ and source
|
||||||
|
|
||||||
Des = None
|
__version__='0.22'
|
||||||
|
|
||||||
import openssl_des
|
import sys, re
|
||||||
Des = openssl_des.load_libcrypto()
|
import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile
|
||||||
|
|
||||||
# if that did not work then use pure python implementation
|
if 'calibre' in sys.modules:
|
||||||
# of DES and try to speed it up with Psycho
|
inCalibre = True
|
||||||
if Des == None:
|
else:
|
||||||
import python_des
|
inCalibre = False
|
||||||
Des = python_des.Des
|
|
||||||
# Import Psyco if available
|
|
||||||
try:
|
|
||||||
# Dumb speed hack 1
|
|
||||||
# http://psyco.sourceforge.net
|
|
||||||
import psyco
|
|
||||||
psyco.full()
|
|
||||||
pass
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
# Wrap a stream so that output gets flushed immediately
|
||||||
__version__='0.16'
|
# and also make sure that any unicode strings get
|
||||||
|
# encoded using "replace" before writing them.
|
||||||
class Unbuffered:
|
class SafeUnbuffered:
|
||||||
def __init__(self, stream):
|
def __init__(self, stream):
|
||||||
self.stream = stream
|
self.stream = stream
|
||||||
|
self.encoding = stream.encoding
|
||||||
|
if self.encoding == None:
|
||||||
|
self.encoding = "utf-8"
|
||||||
def write(self, data):
|
def write(self, data):
|
||||||
|
if isinstance(data,unicode):
|
||||||
|
data = data.encode(self.encoding,"replace")
|
||||||
self.stream.write(data)
|
self.stream.write(data)
|
||||||
self.stream.flush()
|
self.stream.flush()
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
return getattr(self.stream, attr)
|
return getattr(self.stream, attr)
|
||||||
|
|
||||||
import sys
|
iswindows = sys.platform.startswith('win')
|
||||||
sys.stdout=Unbuffered(sys.stdout)
|
isosx = sys.platform.startswith('darwin')
|
||||||
|
|
||||||
import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile
|
def unicode_argv():
|
||||||
|
if iswindows:
|
||||||
|
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||||
|
# strings.
|
||||||
|
|
||||||
|
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||||
|
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||||
|
# characters with '?'.
|
||||||
|
|
||||||
|
|
||||||
|
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||||
|
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||||
|
|
||||||
|
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||||
|
GetCommandLineW.argtypes = []
|
||||||
|
GetCommandLineW.restype = LPCWSTR
|
||||||
|
|
||||||
|
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||||
|
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||||
|
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||||
|
|
||||||
|
cmd = GetCommandLineW()
|
||||||
|
argc = c_int(0)
|
||||||
|
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||||
|
if argc.value > 0:
|
||||||
|
# Remove Python executable and commands if present
|
||||||
|
start = argc.value - len(sys.argv)
|
||||||
|
return [argv[i] for i in
|
||||||
|
xrange(start, argc.value)]
|
||||||
|
# if we don't have any arguments at all, just pass back script name
|
||||||
|
# this should never happen
|
||||||
|
return [u"mobidedrm.py"]
|
||||||
|
else:
|
||||||
|
argvencoding = sys.stdin.encoding
|
||||||
|
if argvencoding == None:
|
||||||
|
argvencoding = "utf-8"
|
||||||
|
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||||
|
|
||||||
|
Des = None
|
||||||
|
if iswindows:
|
||||||
|
# first try with pycrypto
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.erdrpdb2pml import pycrypto_des
|
||||||
|
else:
|
||||||
|
import pycrypto_des
|
||||||
|
Des = pycrypto_des.load_pycrypto()
|
||||||
|
if Des == None:
|
||||||
|
# they try with openssl
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.erdrpdb2pml import openssl_des
|
||||||
|
else:
|
||||||
|
import openssl_des
|
||||||
|
Des = openssl_des.load_libcrypto()
|
||||||
|
else:
|
||||||
|
# first try with openssl
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.erdrpdb2pml import openssl_des
|
||||||
|
else:
|
||||||
|
import openssl_des
|
||||||
|
Des = openssl_des.load_libcrypto()
|
||||||
|
if Des == None:
|
||||||
|
# then try with pycrypto
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.erdrpdb2pml import pycrypto_des
|
||||||
|
else:
|
||||||
|
import pycrypto_des
|
||||||
|
Des = pycrypto_des.load_pycrypto()
|
||||||
|
|
||||||
|
# if that did not work then use pure python implementation
|
||||||
|
# of DES and try to speed it up with Psycho
|
||||||
|
if Des == None:
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.erdrpdb2pml import python_des
|
||||||
|
else:
|
||||||
|
import python_des
|
||||||
|
Des = python_des.Des
|
||||||
|
# Import Psyco if available
|
||||||
|
try:
|
||||||
|
# http://psyco.sourceforge.net
|
||||||
|
import psyco
|
||||||
|
psyco.full()
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
@@ -99,18 +187,26 @@ except ImportError:
|
|||||||
# older Python release
|
# older Python release
|
||||||
import sha
|
import sha
|
||||||
sha1 = lambda s: sha.new(s)
|
sha1 = lambda s: sha.new(s)
|
||||||
|
|
||||||
import cgi
|
import cgi
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logging.basicConfig()
|
logging.basicConfig()
|
||||||
#logging.basicConfig(level=logging.DEBUG)
|
#logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
class Sectionizer(object):
|
class Sectionizer(object):
|
||||||
|
bkType = "Book"
|
||||||
|
|
||||||
def __init__(self, filename, ident):
|
def __init__(self, filename, ident):
|
||||||
self.contents = file(filename, 'rb').read()
|
self.contents = file(filename, 'rb').read()
|
||||||
self.header = self.contents[0:72]
|
self.header = self.contents[0:72]
|
||||||
self.num_sections, = struct.unpack('>H', self.contents[76:78])
|
self.num_sections, = struct.unpack('>H', self.contents[76:78])
|
||||||
|
# Dictionary or normal content (TODO: Not hard-coded)
|
||||||
if self.header[0x3C:0x3C+8] != ident:
|
if self.header[0x3C:0x3C+8] != ident:
|
||||||
|
if self.header[0x3C:0x3C+8] == "PDctPPrs":
|
||||||
|
self.bkType = "Dict"
|
||||||
|
else:
|
||||||
raise ValueError('Invalid file format')
|
raise ValueError('Invalid file format')
|
||||||
self.sections = []
|
self.sections = []
|
||||||
for i in xrange(self.num_sections):
|
for i in xrange(self.num_sections):
|
||||||
@@ -125,12 +221,25 @@ class Sectionizer(object):
|
|||||||
off = self.sections[section][0]
|
off = self.sections[section][0]
|
||||||
return self.contents[off:end_off]
|
return self.contents[off:end_off]
|
||||||
|
|
||||||
def sanitizeFileName(s):
|
# cleanup unicode filenames
|
||||||
r = ''
|
# borrowed from calibre from calibre/src/calibre/__init__.py
|
||||||
for c in s:
|
# added in removal of control (<32) chars
|
||||||
if c in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-":
|
# and removal of . at start and end
|
||||||
r += c
|
# and with some (heavily edited) code from Paul Durrant's kindlenamer.py
|
||||||
return r
|
def sanitizeFileName(name):
|
||||||
|
# substitute filename unfriendly characters
|
||||||
|
name = name.replace(u"<",u"[").replace(u">",u"]").replace(u" : ",u" – ").replace(u": ",u" – ").replace(u":",u"—").replace(u"/",u"_").replace(u"\\",u"_").replace(u"|",u"_").replace(u"\"",u"\'")
|
||||||
|
# delete control characters
|
||||||
|
name = u"".join(char for char in name if ord(char)>=32)
|
||||||
|
# white space to single space, delete leading and trailing while space
|
||||||
|
name = re.sub(ur"\s", u" ", name).strip()
|
||||||
|
# remove leading dots
|
||||||
|
while len(name)>0 and name[0] == u".":
|
||||||
|
name = name[1:]
|
||||||
|
# remove trailing dots (Windows doesn't like them)
|
||||||
|
if name.endswith(u'.'):
|
||||||
|
name = name[:-1]
|
||||||
|
return name
|
||||||
|
|
||||||
def fixKey(key):
|
def fixKey(key):
|
||||||
def fixByte(b):
|
def fixByte(b):
|
||||||
@@ -148,15 +257,15 @@ def deXOR(text, sp, table):
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
class EreaderProcessor(object):
|
class EreaderProcessor(object):
|
||||||
def __init__(self, section_reader, username, creditcard):
|
def __init__(self, sect, user_key):
|
||||||
self.section_reader = section_reader
|
self.section_reader = sect.loadSection
|
||||||
data = section_reader(0)
|
data = self.section_reader(0)
|
||||||
version, = struct.unpack('>H', data[0:2])
|
version, = struct.unpack('>H', data[0:2])
|
||||||
self.version = version
|
self.version = version
|
||||||
logging.info('eReader file format version %s', version)
|
logging.info('eReader file format version %s', version)
|
||||||
if version != 272 and version != 260 and version != 259:
|
if version != 272 and version != 260 and version != 259:
|
||||||
raise ValueError('incorrect eReader version %d (error 1)' % version)
|
raise ValueError('incorrect eReader version %d (error 1)' % version)
|
||||||
data = section_reader(1)
|
data = self.section_reader(1)
|
||||||
self.data = data
|
self.data = data
|
||||||
des = Des(fixKey(data[0:8]))
|
des = Des(fixKey(data[0:8]))
|
||||||
cookie_shuf, cookie_size = struct.unpack('>LL', des.decrypt(data[-8:]))
|
cookie_shuf, cookie_size = struct.unpack('>LL', des.decrypt(data[-8:]))
|
||||||
@@ -173,21 +282,19 @@ class EreaderProcessor(object):
|
|||||||
return "".join(r)
|
return "".join(r)
|
||||||
r = unshuff(input[0:-8], cookie_shuf)
|
r = unshuff(input[0:-8], cookie_shuf)
|
||||||
|
|
||||||
def fixUsername(s):
|
|
||||||
r = ''
|
|
||||||
for c in s.lower():
|
|
||||||
if (c >= 'a' and c <= 'z' or c >= '0' and c <= '9'):
|
|
||||||
r += c
|
|
||||||
return r
|
|
||||||
|
|
||||||
user_key = struct.pack('>LL', binascii.crc32(fixUsername(username)) & 0xffffffff, binascii.crc32(creditcard[-8:])& 0xffffffff)
|
|
||||||
drm_sub_version = struct.unpack('>H', r[0:2])[0]
|
drm_sub_version = struct.unpack('>H', r[0:2])[0]
|
||||||
self.num_text_pages = struct.unpack('>H', r[2:4])[0] - 1
|
self.num_text_pages = struct.unpack('>H', r[2:4])[0] - 1
|
||||||
self.num_image_pages = struct.unpack('>H', r[26:26+2])[0]
|
self.num_image_pages = struct.unpack('>H', r[26:26+2])[0]
|
||||||
self.first_image_page = struct.unpack('>H', r[24:24+2])[0]
|
self.first_image_page = struct.unpack('>H', r[24:24+2])[0]
|
||||||
|
# Default values
|
||||||
|
self.num_footnote_pages = 0
|
||||||
|
self.num_sidebar_pages = 0
|
||||||
|
self.first_footnote_page = -1
|
||||||
|
self.first_sidebar_page = -1
|
||||||
if self.version == 272:
|
if self.version == 272:
|
||||||
self.num_footnote_pages = struct.unpack('>H', r[46:46+2])[0]
|
self.num_footnote_pages = struct.unpack('>H', r[46:46+2])[0]
|
||||||
self.first_footnote_page = struct.unpack('>H', r[44:44+2])[0]
|
self.first_footnote_page = struct.unpack('>H', r[44:44+2])[0]
|
||||||
|
if (sect.bkType == "Book"):
|
||||||
self.num_sidebar_pages = struct.unpack('>H', r[38:38+2])[0]
|
self.num_sidebar_pages = struct.unpack('>H', r[38:38+2])[0]
|
||||||
self.first_sidebar_page = struct.unpack('>H', r[36:36+2])[0]
|
self.first_sidebar_page = struct.unpack('>H', r[36:36+2])[0]
|
||||||
# self.num_bookinfo_pages = struct.unpack('>H', r[34:34+2])[0]
|
# self.num_bookinfo_pages = struct.unpack('>H', r[34:34+2])[0]
|
||||||
@@ -205,10 +312,8 @@ class EreaderProcessor(object):
|
|||||||
self.xortable_size = struct.unpack('>H', r[42:42+2])[0]
|
self.xortable_size = struct.unpack('>H', r[42:42+2])[0]
|
||||||
self.xortable = self.data[self.xortable_offset:self.xortable_offset + self.xortable_size]
|
self.xortable = self.data[self.xortable_offset:self.xortable_offset + self.xortable_size]
|
||||||
else:
|
else:
|
||||||
self.num_footnote_pages = 0
|
# Nothing needs to be done
|
||||||
self.num_sidebar_pages = 0
|
pass
|
||||||
self.first_footnote_page = -1
|
|
||||||
self.first_sidebar_page = -1
|
|
||||||
# self.num_bookinfo_pages = 0
|
# self.num_bookinfo_pages = 0
|
||||||
# self.num_chapter_pages = 0
|
# self.num_chapter_pages = 0
|
||||||
# self.num_link_pages = 0
|
# self.num_link_pages = 0
|
||||||
@@ -233,10 +338,14 @@ class EreaderProcessor(object):
|
|||||||
encrypted_key_sha = r[44:44+20]
|
encrypted_key_sha = r[44:44+20]
|
||||||
encrypted_key = r[64:64+8]
|
encrypted_key = r[64:64+8]
|
||||||
elif version == 260:
|
elif version == 260:
|
||||||
if drm_sub_version != 13:
|
if drm_sub_version != 13 and drm_sub_version != 11:
|
||||||
raise ValueError('incorrect eReader version %d (error 3)' % drm_sub_version)
|
raise ValueError('incorrect eReader version %d (error 3)' % drm_sub_version)
|
||||||
|
if drm_sub_version == 13:
|
||||||
encrypted_key = r[44:44+8]
|
encrypted_key = r[44:44+8]
|
||||||
encrypted_key_sha = r[52:52+20]
|
encrypted_key_sha = r[52:52+20]
|
||||||
|
else:
|
||||||
|
encrypted_key = r[64:64+8]
|
||||||
|
encrypted_key_sha = r[44:44+20]
|
||||||
elif version == 272:
|
elif version == 272:
|
||||||
encrypted_key = r[172:172+8]
|
encrypted_key = r[172:172+8]
|
||||||
encrypted_key_sha = r[56:56+20]
|
encrypted_key_sha = r[56:56+20]
|
||||||
@@ -251,7 +360,7 @@ class EreaderProcessor(object):
|
|||||||
sect = self.section_reader(self.first_image_page + i)
|
sect = self.section_reader(self.first_image_page + i)
|
||||||
name = sect[4:4+32].strip('\0')
|
name = sect[4:4+32].strip('\0')
|
||||||
data = sect[62:]
|
data = sect[62:]
|
||||||
return sanitizeFileName(name), data
|
return sanitizeFileName(unicode(name,'windows-1252')), data
|
||||||
|
|
||||||
|
|
||||||
# def getChapterNamePMLOffsetData(self):
|
# def getChapterNamePMLOffsetData(self):
|
||||||
@@ -322,6 +431,12 @@ class EreaderProcessor(object):
|
|||||||
r += fmarker
|
r += fmarker
|
||||||
fnote_ids = fnote_ids[id_len+4:]
|
fnote_ids = fnote_ids[id_len+4:]
|
||||||
|
|
||||||
|
# TODO: Handle dictionary index (?) pages - which are also marked as
|
||||||
|
# sidebar_pages (?). For now dictionary sidebars are ignored
|
||||||
|
# For dictionaries - record 0 is null terminated strings, followed by
|
||||||
|
# blocks of around 62000 bytes and a final block. Not sure of the
|
||||||
|
# encoding
|
||||||
|
|
||||||
# now handle sidebar pages
|
# now handle sidebar pages
|
||||||
if self.num_sidebar_pages > 0:
|
if self.num_sidebar_pages > 0:
|
||||||
r += '\n'
|
r += '\n'
|
||||||
@@ -334,7 +449,7 @@ class EreaderProcessor(object):
|
|||||||
id_len = ord(sbar_ids[2])
|
id_len = ord(sbar_ids[2])
|
||||||
id = sbar_ids[3:3+id_len]
|
id = sbar_ids[3:3+id_len]
|
||||||
smarker = '<sidebar id="%s">\n' % id
|
smarker = '<sidebar id="%s">\n' % id
|
||||||
smarker += zlib.decompress(des.decrypt(self.section_reader(self.first_footnote_page + i)))
|
smarker += zlib.decompress(des.decrypt(self.section_reader(self.first_sidebar_page + i)))
|
||||||
smarker += '\n</sidebar>\n'
|
smarker += '\n</sidebar>\n'
|
||||||
r += smarker
|
r += smarker
|
||||||
sbar_ids = sbar_ids[id_len+4:]
|
sbar_ids = sbar_ids[id_len+4:]
|
||||||
@@ -349,101 +464,46 @@ def cleanPML(pml):
|
|||||||
pml2 = pml2.replace(badChar, '\\a%03d' % k)
|
pml2 = pml2.replace(badChar, '\\a%03d' % k)
|
||||||
return pml2
|
return pml2
|
||||||
|
|
||||||
def convertEreaderToPml(infile, name, cc, outdir):
|
def decryptBook(infile, outpath, make_pmlz, user_key):
|
||||||
|
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||||
|
if make_pmlz:
|
||||||
|
# outpath is actually pmlz name
|
||||||
|
pmlzname = outpath
|
||||||
|
outdir = tempfile.mkdtemp()
|
||||||
|
imagedirpath = os.path.join(outdir,u"images")
|
||||||
|
else:
|
||||||
|
pmlzname = None
|
||||||
|
outdir = outpath
|
||||||
|
imagedirpath = os.path.join(outdir,bookname + u"_img")
|
||||||
|
|
||||||
|
try:
|
||||||
if not os.path.exists(outdir):
|
if not os.path.exists(outdir):
|
||||||
os.makedirs(outdir)
|
os.makedirs(outdir)
|
||||||
|
print u"Decoding File"
|
||||||
print " Decoding File"
|
|
||||||
sect = Sectionizer(infile, 'PNRdPPrs')
|
sect = Sectionizer(infile, 'PNRdPPrs')
|
||||||
er = EreaderProcessor(sect.loadSection, name, cc)
|
er = EreaderProcessor(sect, user_key)
|
||||||
|
|
||||||
if er.getNumImages() > 0:
|
if er.getNumImages() > 0:
|
||||||
print " Extracting images"
|
print u"Extracting images"
|
||||||
imagedir = bookname + '_img/'
|
|
||||||
imagedirpath = os.path.join(outdir,imagedir)
|
|
||||||
if not os.path.exists(imagedirpath):
|
if not os.path.exists(imagedirpath):
|
||||||
os.makedirs(imagedirpath)
|
os.makedirs(imagedirpath)
|
||||||
for i in xrange(er.getNumImages()):
|
for i in xrange(er.getNumImages()):
|
||||||
name, contents = er.getImage(i)
|
name, contents = er.getImage(i)
|
||||||
file(os.path.join(imagedirpath, name), 'wb').write(contents)
|
file(os.path.join(imagedirpath, name), 'wb').write(contents)
|
||||||
|
|
||||||
print " Extracting pml"
|
print u"Extracting pml"
|
||||||
pml_string = er.getText()
|
pml_string = er.getText()
|
||||||
pmlfilename = bookname + ".pml"
|
pmlfilename = bookname + ".pml"
|
||||||
file(os.path.join(outdir, pmlfilename),'wb').write(cleanPML(pml_string))
|
file(os.path.join(outdir, pmlfilename),'wb').write(cleanPML(pml_string))
|
||||||
|
if pmlzname is not None:
|
||||||
# bkinfo = er.getBookInfo()
|
|
||||||
# if bkinfo != '':
|
|
||||||
# print " Extracting book meta information"
|
|
||||||
# file(os.path.join(outdir, 'bookinfo.txt'),'wb').write(bkinfo)
|
|
||||||
|
|
||||||
|
|
||||||
def usage():
|
|
||||||
print "Converts DRMed eReader books to PML Source"
|
|
||||||
print "Usage:"
|
|
||||||
print " erdr2pml [options] infile.pdb [outdir] \"your name\" credit_card_number "
|
|
||||||
print " "
|
|
||||||
print "Options: "
|
|
||||||
print " -h prints this message"
|
|
||||||
print " --make-pmlz create PMLZ instead of using output directory"
|
|
||||||
print " "
|
|
||||||
print "Note:"
|
|
||||||
print " if ommitted, outdir defaults based on 'infile.pdb'"
|
|
||||||
print " It's enough to enter the last 8 digits of the credit card number"
|
|
||||||
return
|
|
||||||
|
|
||||||
def main(argv=None):
|
|
||||||
global bookname
|
|
||||||
try:
|
|
||||||
opts, args = getopt.getopt(sys.argv[1:], "h", ["make-pmlz"])
|
|
||||||
except getopt.GetoptError, err:
|
|
||||||
print str(err)
|
|
||||||
usage()
|
|
||||||
return 1
|
|
||||||
make_pmlz = False
|
|
||||||
zipname = None
|
|
||||||
for o, a in opts:
|
|
||||||
if o == "-h":
|
|
||||||
usage()
|
|
||||||
return 0
|
|
||||||
elif o == "--make-pmlz":
|
|
||||||
make_pmlz = True
|
|
||||||
zipname = ''
|
|
||||||
|
|
||||||
print "eRdr2Pml v%s. Copyright (c) 2009 The Dark Reverser" % __version__
|
|
||||||
|
|
||||||
if len(args)!=3 and len(args)!=4:
|
|
||||||
usage()
|
|
||||||
return 1
|
|
||||||
else:
|
|
||||||
if len(args)==3:
|
|
||||||
infile, name, cc = args[0], args[1], args[2]
|
|
||||||
outdir = infile[:-4] + '_Source'
|
|
||||||
elif len(args)==4:
|
|
||||||
infile, outdir, name, cc = args[0], args[1], args[2], args[3]
|
|
||||||
|
|
||||||
if make_pmlz :
|
|
||||||
# ignore specified outdir, use tempdir instead
|
|
||||||
outdir = tempfile.mkdtemp()
|
|
||||||
|
|
||||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
|
||||||
|
|
||||||
try:
|
|
||||||
print "Processing..."
|
|
||||||
import time
|
|
||||||
start_time = time.time()
|
|
||||||
convertEreaderToPml(infile, name, cc, outdir)
|
|
||||||
|
|
||||||
if make_pmlz :
|
|
||||||
import zipfile
|
import zipfile
|
||||||
import shutil
|
import shutil
|
||||||
print " Creating PMLZ file"
|
print u"Creating PMLZ file {0}".format(os.path.basename(pmlzname))
|
||||||
zipname = infile[:-4] + '.pmlz'
|
myZipFile = zipfile.ZipFile(pmlzname,'w',zipfile.ZIP_STORED, False)
|
||||||
myZipFile = zipfile.ZipFile(zipname,'w',zipfile.ZIP_STORED, False)
|
|
||||||
list = os.listdir(outdir)
|
list = os.listdir(outdir)
|
||||||
for file in list:
|
for filename in list:
|
||||||
localname = file
|
localname = filename
|
||||||
filePath = os.path.join(outdir,file)
|
filePath = os.path.join(outdir,filename)
|
||||||
if os.path.isfile(filePath):
|
if os.path.isfile(filePath):
|
||||||
myZipFile.write(filePath, localname)
|
myZipFile.write(filePath, localname)
|
||||||
elif os.path.isdir(filePath):
|
elif os.path.isdir(filePath):
|
||||||
@@ -456,21 +516,79 @@ def main(argv=None):
|
|||||||
myZipFile.write(imagePath, localname)
|
myZipFile.write(imagePath, localname)
|
||||||
myZipFile.close()
|
myZipFile.close()
|
||||||
# remove temporary directory
|
# remove temporary directory
|
||||||
shutil.rmtree(outdir)
|
shutil.rmtree(outdir, True)
|
||||||
|
print u"Output is {0}".format(pmlzname)
|
||||||
end_time = time.time()
|
|
||||||
search_time = end_time - start_time
|
|
||||||
print 'elapsed time: %.2f seconds' % (search_time, )
|
|
||||||
if make_pmlz :
|
|
||||||
print 'output is %s' % zipname
|
|
||||||
else :
|
else :
|
||||||
print 'output in %s' % outdir
|
print u"Output is in {0}".format(outdir)
|
||||||
print "done"
|
print "done"
|
||||||
except ValueError, e:
|
except ValueError, e:
|
||||||
print "Error: %s" % e
|
print u"Error: {0}".format(e.args[0])
|
||||||
return 1
|
return 1
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
def usage():
|
||||||
|
print u"Converts DRMed eReader books to PML Source"
|
||||||
|
print u"Usage:"
|
||||||
|
print u" erdr2pml [options] infile.pdb [outpath] \"your name\" credit_card_number"
|
||||||
|
print u" "
|
||||||
|
print u"Options: "
|
||||||
|
print u" -h prints this message"
|
||||||
|
print u" -p create PMLZ instead of source folder"
|
||||||
|
print u" --make-pmlz create PMLZ instead of source folder"
|
||||||
|
print u" "
|
||||||
|
print u"Note:"
|
||||||
|
print u" if outpath is ommitted, creates source in 'infile_Source' folder"
|
||||||
|
print u" if outpath is ommitted and pmlz option, creates PMLZ 'infile.pmlz'"
|
||||||
|
print u" if source folder created, images are in infile_img folder"
|
||||||
|
print u" if pmlz file created, images are in images folder"
|
||||||
|
print u" It's enough to enter the last 8 digits of the credit card number"
|
||||||
|
return
|
||||||
|
|
||||||
|
def getuser_key(name,cc):
|
||||||
|
newname = "".join(c for c in name.lower() if c >= 'a' and c <= 'z' or c >= '0' and c <= '9')
|
||||||
|
cc = cc.replace(" ","")
|
||||||
|
return struct.pack('>LL', binascii.crc32(newname) & 0xffffffff,binascii.crc32(cc[-8:])& 0xffffffff)
|
||||||
|
|
||||||
|
def cli_main(argv=unicode_argv()):
|
||||||
|
print u"eRdr2Pml v{0}. Copyright © 2009–2012 The Dark Reverser et al.".format(__version__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
opts, args = getopt.getopt(argv[1:], "hp", ["make-pmlz"])
|
||||||
|
except getopt.GetoptError, err:
|
||||||
|
print err.args[0]
|
||||||
|
usage()
|
||||||
|
return 1
|
||||||
|
make_pmlz = False
|
||||||
|
for o, a in opts:
|
||||||
|
if o == "-h":
|
||||||
|
usage()
|
||||||
|
return 0
|
||||||
|
elif o == "-p":
|
||||||
|
make_pmlz = True
|
||||||
|
elif o == "--make-pmlz":
|
||||||
|
make_pmlz = True
|
||||||
|
|
||||||
|
if len(args)!=3 and len(args)!=4:
|
||||||
|
usage()
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if len(args)==3:
|
||||||
|
infile, name, cc = args
|
||||||
|
if make_pmlz:
|
||||||
|
outpath = os.path.splitext(infile)[0] + u".pmlz"
|
||||||
|
else:
|
||||||
|
outpath = os.path.splitext(infile)[0] + u"_Source"
|
||||||
|
elif len(args)==4:
|
||||||
|
infile, outpath, name, cc = args
|
||||||
|
|
||||||
|
print getuser_key(name,cc).encode('hex')
|
||||||
|
|
||||||
|
return decryptBook(infile, outpath, make_pmlz, getuser_key(name,cc))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||||
|
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||||
|
sys.exit(cli_main())
|
||||||
|
|
||||||
|
|||||||
@@ -87,4 +87,3 @@ def load_libcrypto():
|
|||||||
return ''.join(result)
|
return ''.join(result)
|
||||||
|
|
||||||
return DES
|
return DES
|
||||||
|
|
||||||
|
|||||||
30
Calibre_Plugins/eReaderPDB2PML_plugin/pycrypto_des.py
Normal file
30
Calibre_Plugins/eReaderPDB2PML_plugin/pycrypto_des.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||||
|
|
||||||
|
|
||||||
|
def load_pycrypto():
|
||||||
|
try :
|
||||||
|
from Crypto.Cipher import DES as _DES
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
class DES(object):
|
||||||
|
def __init__(self, key):
|
||||||
|
if len(key) != 8 :
|
||||||
|
raise Error('DES improper key used')
|
||||||
|
self.key = key
|
||||||
|
self._des = _DES.new(key,_DES.MODE_ECB)
|
||||||
|
def desdecrypt(self, data):
|
||||||
|
return self._des.decrypt(data)
|
||||||
|
def decrypt(self, data):
|
||||||
|
if not data:
|
||||||
|
return ''
|
||||||
|
i = 0
|
||||||
|
result = []
|
||||||
|
while i < len(data):
|
||||||
|
block = data[i:i+8]
|
||||||
|
processed_block = self.desdecrypt(block)
|
||||||
|
result.append(processed_block)
|
||||||
|
i += 8
|
||||||
|
return ''.join(result)
|
||||||
|
return DES
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
ECB = 0
|
ECB = 0
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,95 @@
|
|||||||
|
<?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.4)</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.
|
||||||
|
</pre>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
199
Calibre_Plugins/ignobleepub_plugin/__init__.py
Normal file
199
Calibre_Plugins/ignobleepub_plugin/__init__.py
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
#!/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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Decrypt Barnes & Noble ADEPT encrypted EPUB books.
|
||||||
|
"""
|
||||||
|
|
||||||
|
PLUGIN_NAME = u"Ignoble Epub DeDRM"
|
||||||
|
PLUGIN_VERSION_TUPLE = (0, 2, 5)
|
||||||
|
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):
|
||||||
|
print u"{0} v{1}: {2} is not a secure Barnes & Noble ePub.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))
|
||||||
|
# return the original file, so that no error message is generated in the GUI
|
||||||
|
return 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)
|
||||||
|
|
||||||
|
# Ebook is not a B&N epub... do nothing and pass it on.
|
||||||
|
# This allows a non-encrypted epub to be imported without error messages.
|
||||||
|
if result[0] == 1:
|
||||||
|
print u"{0} v{1}: {2}".format(PLUGIN_NAME, PLUGIN_VERSION, result[1])
|
||||||
|
of.close()
|
||||||
|
return path_to_ebook
|
||||||
|
break
|
||||||
|
|
||||||
|
# Decryption was successful return the modified PersistentTemporary
|
||||||
|
# file to Calibre's import process.
|
||||||
|
if result[0] == 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}: {2}".format(PLUGIN_NAME, PLUGIN_VERSION, result[1])
|
||||||
|
of.close()
|
||||||
|
|
||||||
|
|
||||||
|
# Something went wrong with decryption.
|
||||||
|
# Import the original unmolested epub.
|
||||||
|
print(u"{0} v{1}: Ultimately failed to decrypt".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||||
|
return path_to_ebook
|
||||||
|
|
||||||
|
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()
|
||||||
305
Calibre_Plugins/ignobleepub_plugin/config.py
Normal file
305
Calibre_Plugins/ignobleepub_plugin/config.py
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
#!/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()
|
||||||
160
Calibre_Plugins/ignobleepub_plugin/dialogs.py
Normal file
160
Calibre_Plugins/ignobleepub_plugin/dialogs.py
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
#!/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')
|
||||||
420
Calibre_Plugins/ignobleepub_plugin/ignobleepub.py
Normal file
420
Calibre_Plugins/ignobleepub_plugin/ignobleepub.py
Normal file
@@ -0,0 +1,420 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
# ignobleepub.pyw, version 3.6
|
||||||
|
# Copyright © 2009-2010 by i♥cabbages
|
||||||
|
|
||||||
|
# Released under the terms of the GNU General Public Licence, version 3
|
||||||
|
# <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
|
# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||||
|
|
||||||
|
# Windows users: Before running this program, you must first install Python 2.6
|
||||||
|
# from <http://www.python.org/download/> and PyCrypto from
|
||||||
|
# <http://www.voidspace.org.uk/python/modules.shtml#pycrypto> (make sure to
|
||||||
|
# install the version for Python 2.6). Save this script file as
|
||||||
|
# ineptepub.pyw and double-click on it to run it.
|
||||||
|
#
|
||||||
|
# Mac OS X users: Save this script file as ineptepub.pyw. You can run this
|
||||||
|
# program from the command line (pythonw ineptepub.pyw) or by double-clicking
|
||||||
|
# it when it has been associated with PythonLauncher.
|
||||||
|
|
||||||
|
# Revision history:
|
||||||
|
# 1 - Initial release
|
||||||
|
# 2 - Added OS X support by using OpenSSL when available
|
||||||
|
# 3 - screen out improper key lengths to prevent segfaults on Linux
|
||||||
|
# 3.1 - Allow Windows versions of libcrypto to be found
|
||||||
|
# 3.2 - add support for encoding to 'utf-8' when building up list of files to cecrypt from encryption.xml
|
||||||
|
# 3.3 - On Windows try PyCrypto first and OpenSSL next
|
||||||
|
# 3.4 - Modify interace to allow use with import
|
||||||
|
# 3.5 - Fix for potential problem with PyCrypto
|
||||||
|
# 3.6 - Revised to allow use in calibre plugins to eliminate need for duplicate code
|
||||||
|
|
||||||
|
"""
|
||||||
|
Decrypt Barnes & Noble encrypted ePub books.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__version__ = "3.6"
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
import zlib
|
||||||
|
import zipfile
|
||||||
|
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||||
|
from contextlib import closing
|
||||||
|
import xml.etree.ElementTree as etree
|
||||||
|
|
||||||
|
# Wrap a stream so that output gets flushed immediately
|
||||||
|
# and also make sure that any unicode strings get
|
||||||
|
# encoded using "replace" before writing them.
|
||||||
|
class SafeUnbuffered:
|
||||||
|
def __init__(self, stream):
|
||||||
|
self.stream = stream
|
||||||
|
self.encoding = stream.encoding
|
||||||
|
if self.encoding == None:
|
||||||
|
self.encoding = "utf-8"
|
||||||
|
def write(self, data):
|
||||||
|
if isinstance(data,unicode):
|
||||||
|
data = data.encode(self.encoding,"replace")
|
||||||
|
self.stream.write(data)
|
||||||
|
self.stream.flush()
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return getattr(self.stream, attr)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from calibre.constants import iswindows, isosx
|
||||||
|
except:
|
||||||
|
iswindows = sys.platform.startswith('win')
|
||||||
|
isosx = sys.platform.startswith('darwin')
|
||||||
|
|
||||||
|
def unicode_argv():
|
||||||
|
if iswindows:
|
||||||
|
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||||
|
# strings.
|
||||||
|
|
||||||
|
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||||
|
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||||
|
# characters with '?'.
|
||||||
|
|
||||||
|
|
||||||
|
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||||
|
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||||
|
|
||||||
|
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||||
|
GetCommandLineW.argtypes = []
|
||||||
|
GetCommandLineW.restype = LPCWSTR
|
||||||
|
|
||||||
|
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||||
|
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||||
|
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||||
|
|
||||||
|
cmd = GetCommandLineW()
|
||||||
|
argc = c_int(0)
|
||||||
|
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||||
|
if argc.value > 0:
|
||||||
|
# Remove Python executable and commands if present
|
||||||
|
start = argc.value - len(sys.argv)
|
||||||
|
return [argv[i] for i in
|
||||||
|
xrange(start, argc.value)]
|
||||||
|
return [u"ineptepub.py"]
|
||||||
|
else:
|
||||||
|
argvencoding = sys.stdin.encoding
|
||||||
|
if argvencoding == None:
|
||||||
|
argvencoding = "utf-8"
|
||||||
|
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||||
|
|
||||||
|
|
||||||
|
class IGNOBLEError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _load_crypto_libcrypto():
|
||||||
|
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
||||||
|
Structure, c_ulong, create_string_buffer, cast
|
||||||
|
from ctypes.util import find_library
|
||||||
|
|
||||||
|
if iswindows:
|
||||||
|
libcrypto = find_library('libeay32')
|
||||||
|
else:
|
||||||
|
libcrypto = find_library('crypto')
|
||||||
|
|
||||||
|
if libcrypto is None:
|
||||||
|
raise IGNOBLEError('libcrypto not found')
|
||||||
|
libcrypto = CDLL(libcrypto)
|
||||||
|
|
||||||
|
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_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
|
||||||
|
[c_char_p, c_int, AES_KEY_p])
|
||||||
|
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||||
|
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
||||||
|
c_int])
|
||||||
|
|
||||||
|
class AES(object):
|
||||||
|
def __init__(self, userkey):
|
||||||
|
self._blocksize = len(userkey)
|
||||||
|
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
||||||
|
raise IGNOBLEError('AES improper key used')
|
||||||
|
return
|
||||||
|
key = self._key = AES_KEY()
|
||||||
|
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
|
||||||
|
if rv < 0:
|
||||||
|
raise IGNOBLEError('Failed to initialize AES key')
|
||||||
|
|
||||||
|
def decrypt(self, data):
|
||||||
|
out = create_string_buffer(len(data))
|
||||||
|
iv = ("\x00" * self._blocksize)
|
||||||
|
rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
|
||||||
|
if rv == 0:
|
||||||
|
raise IGNOBLEError('AES decryption failed')
|
||||||
|
return out.raw
|
||||||
|
|
||||||
|
return AES
|
||||||
|
|
||||||
|
def _load_crypto_pycrypto():
|
||||||
|
from Crypto.Cipher import AES as _AES
|
||||||
|
|
||||||
|
class AES(object):
|
||||||
|
def __init__(self, key):
|
||||||
|
self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16)
|
||||||
|
|
||||||
|
def decrypt(self, data):
|
||||||
|
return self._aes.decrypt(data)
|
||||||
|
|
||||||
|
return AES
|
||||||
|
|
||||||
|
def _load_crypto():
|
||||||
|
AES = None
|
||||||
|
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
||||||
|
for loader in cryptolist:
|
||||||
|
try:
|
||||||
|
AES = loader()
|
||||||
|
break
|
||||||
|
except (ImportError, IGNOBLEError):
|
||||||
|
pass
|
||||||
|
return AES
|
||||||
|
|
||||||
|
AES = _load_crypto()
|
||||||
|
|
||||||
|
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
|
||||||
|
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
||||||
|
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
||||||
|
|
||||||
|
class ZipInfo(zipfile.ZipInfo):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
if 'compress_type' in kwargs:
|
||||||
|
compress_type = kwargs.pop('compress_type')
|
||||||
|
super(ZipInfo, self).__init__(*args, **kwargs)
|
||||||
|
self.compress_type = compress_type
|
||||||
|
|
||||||
|
class Decryptor(object):
|
||||||
|
def __init__(self, bookkey, encryption):
|
||||||
|
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
|
||||||
|
self._aes = AES(bookkey)
|
||||||
|
encryption = etree.fromstring(encryption)
|
||||||
|
self._encrypted = encrypted = set()
|
||||||
|
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
|
||||||
|
enc('CipherReference'))
|
||||||
|
for elem in encryption.findall(expr):
|
||||||
|
path = elem.get('URI', None)
|
||||||
|
if path is not None:
|
||||||
|
path = path.encode('utf-8')
|
||||||
|
encrypted.add(path)
|
||||||
|
|
||||||
|
def decompress(self, bytes):
|
||||||
|
dc = zlib.decompressobj(-15)
|
||||||
|
bytes = dc.decompress(bytes)
|
||||||
|
ex = dc.decompress('Z') + dc.flush()
|
||||||
|
if ex:
|
||||||
|
bytes = bytes + ex
|
||||||
|
return bytes
|
||||||
|
|
||||||
|
def decrypt(self, path, data):
|
||||||
|
if path in self._encrypted:
|
||||||
|
data = self._aes.decrypt(data)[16:]
|
||||||
|
data = data[:-ord(data[-1])]
|
||||||
|
data = self.decompress(data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
# check file to make check whether it's probably an Adobe Adept encrypted ePub
|
||||||
|
def ignobleBook(inpath):
|
||||||
|
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||||
|
namelist = set(inf.namelist())
|
||||||
|
if 'META-INF/rights.xml' not in namelist or \
|
||||||
|
'META-INF/encryption.xml' not in namelist:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||||
|
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||||
|
expr = './/%s' % (adept('encryptedKey'),)
|
||||||
|
bookkey = ''.join(rights.findtext(expr))
|
||||||
|
if len(bookkey) == 64:
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
# if we couldn't check, assume it is
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# return error code and error message duple
|
||||||
|
def decryptBook(keyb64, inpath, outpath):
|
||||||
|
if AES is None:
|
||||||
|
# 1 means don't try again
|
||||||
|
return (1, u"PyCrypto or OpenSSL must be installed.")
|
||||||
|
key = keyb64.decode('base64')[:16]
|
||||||
|
aes = AES(key)
|
||||||
|
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||||
|
namelist = set(inf.namelist())
|
||||||
|
if 'META-INF/rights.xml' not in namelist or \
|
||||||
|
'META-INF/encryption.xml' not in namelist:
|
||||||
|
return (1, u"Not a secure Barnes & Noble ePub.")
|
||||||
|
for name in META_NAMES:
|
||||||
|
namelist.remove(name)
|
||||||
|
try:
|
||||||
|
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||||
|
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||||
|
expr = './/%s' % (adept('encryptedKey'),)
|
||||||
|
bookkey = ''.join(rights.findtext(expr))
|
||||||
|
if len(bookkey) != 64:
|
||||||
|
return (1, u"Not a secure Barnes & Noble ePub.")
|
||||||
|
bookkey = aes.decrypt(bookkey.decode('base64'))
|
||||||
|
bookkey = bookkey[:-ord(bookkey[-1])]
|
||||||
|
encryption = inf.read('META-INF/encryption.xml')
|
||||||
|
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||||
|
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||||
|
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||||
|
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
||||||
|
outf.writestr(zi, inf.read('mimetype'))
|
||||||
|
for path in namelist:
|
||||||
|
data = inf.read(path)
|
||||||
|
outf.writestr(path, decryptor.decrypt(path, data))
|
||||||
|
except Exception, e:
|
||||||
|
return (2, u"{0}.".format(e.args[0]))
|
||||||
|
return (0, u"Success")
|
||||||
|
|
||||||
|
|
||||||
|
def cli_main(argv=unicode_argv()):
|
||||||
|
progname = os.path.basename(argv[0])
|
||||||
|
if len(argv) != 4:
|
||||||
|
print u"usage: {0} <keyfile.der> <inbook.epub> <outbook.epub>".format(progname)
|
||||||
|
return 1
|
||||||
|
keypath, inpath, outpath = argv[1:]
|
||||||
|
userkey = open(keypath,'rb').read()
|
||||||
|
result = decryptBook(userkey, inpath, outpath)
|
||||||
|
print result[1]
|
||||||
|
return result[0]
|
||||||
|
|
||||||
|
def gui_main():
|
||||||
|
import Tkinter
|
||||||
|
import Tkconstants
|
||||||
|
import tkFileDialog
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
class DecryptionDialog(Tkinter.Frame):
|
||||||
|
def __init__(self, root):
|
||||||
|
Tkinter.Frame.__init__(self, root, border=5)
|
||||||
|
self.status = Tkinter.Label(self, text=u"Select files for decryption")
|
||||||
|
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||||
|
body = Tkinter.Frame(self)
|
||||||
|
body.pack(fill=Tkconstants.X, expand=1)
|
||||||
|
sticky = Tkconstants.E + Tkconstants.W
|
||||||
|
body.grid_columnconfigure(1, weight=2)
|
||||||
|
Tkinter.Label(body, text=u"Key file").grid(row=0)
|
||||||
|
self.keypath = Tkinter.Entry(body, width=30)
|
||||||
|
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||||
|
if os.path.exists(u"bnepubkey.b64"):
|
||||||
|
self.keypath.insert(0, u"bnepubkey.b64")
|
||||||
|
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
|
||||||
|
button.grid(row=0, column=2)
|
||||||
|
Tkinter.Label(body, text=u"Input file").grid(row=1)
|
||||||
|
self.inpath = Tkinter.Entry(body, width=30)
|
||||||
|
self.inpath.grid(row=1, column=1, sticky=sticky)
|
||||||
|
button = Tkinter.Button(body, text=u"...", command=self.get_inpath)
|
||||||
|
button.grid(row=1, column=2)
|
||||||
|
Tkinter.Label(body, text=u"Output file").grid(row=2)
|
||||||
|
self.outpath = Tkinter.Entry(body, width=30)
|
||||||
|
self.outpath.grid(row=2, column=1, sticky=sticky)
|
||||||
|
button = Tkinter.Button(body, text=u"...", command=self.get_outpath)
|
||||||
|
button.grid(row=2, column=2)
|
||||||
|
buttons = Tkinter.Frame(self)
|
||||||
|
buttons.pack()
|
||||||
|
botton = Tkinter.Button(
|
||||||
|
buttons, text=u"Decrypt", width=10, command=self.decrypt)
|
||||||
|
botton.pack(side=Tkconstants.LEFT)
|
||||||
|
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||||
|
button = Tkinter.Button(
|
||||||
|
buttons, text=u"Quit", width=10, command=self.quit)
|
||||||
|
button.pack(side=Tkconstants.RIGHT)
|
||||||
|
|
||||||
|
def get_keypath(self):
|
||||||
|
keypath = tkFileDialog.askopenfilename(
|
||||||
|
parent=None, title=u"Select Barnes & Noble \'.b64\' key file",
|
||||||
|
defaultextension=u".b64",
|
||||||
|
filetypes=[('base64-encoded files', '.b64'),
|
||||||
|
('All Files', '.*')])
|
||||||
|
if keypath:
|
||||||
|
keypath = os.path.normpath(keypath)
|
||||||
|
self.keypath.delete(0, Tkconstants.END)
|
||||||
|
self.keypath.insert(0, keypath)
|
||||||
|
return
|
||||||
|
|
||||||
|
def get_inpath(self):
|
||||||
|
inpath = tkFileDialog.askopenfilename(
|
||||||
|
parent=None, title=u"Select B&N-encrypted ePub file to decrypt",
|
||||||
|
defaultextension=u".epub", filetypes=[('ePub files', '.epub')])
|
||||||
|
if inpath:
|
||||||
|
inpath = os.path.normpath(inpath)
|
||||||
|
self.inpath.delete(0, Tkconstants.END)
|
||||||
|
self.inpath.insert(0, inpath)
|
||||||
|
return
|
||||||
|
|
||||||
|
def get_outpath(self):
|
||||||
|
outpath = tkFileDialog.asksaveasfilename(
|
||||||
|
parent=None, title=u"Select unencrypted ePub file to produce",
|
||||||
|
defaultextension=u".epub", filetypes=[('ePub files', '.epub')])
|
||||||
|
if outpath:
|
||||||
|
outpath = os.path.normpath(outpath)
|
||||||
|
self.outpath.delete(0, Tkconstants.END)
|
||||||
|
self.outpath.insert(0, outpath)
|
||||||
|
return
|
||||||
|
|
||||||
|
def decrypt(self):
|
||||||
|
keypath = self.keypath.get()
|
||||||
|
inpath = self.inpath.get()
|
||||||
|
outpath = self.outpath.get()
|
||||||
|
if not keypath or not os.path.exists(keypath):
|
||||||
|
self.status['text'] = u"Specified key file does not exist"
|
||||||
|
return
|
||||||
|
if not inpath or not os.path.exists(inpath):
|
||||||
|
self.status['text'] = u"Specified input file does not exist"
|
||||||
|
return
|
||||||
|
if not outpath:
|
||||||
|
self.status['text'] = u"Output file not specified"
|
||||||
|
return
|
||||||
|
if inpath == outpath:
|
||||||
|
self.status['text'] = u"Must have different input and output files"
|
||||||
|
return
|
||||||
|
userkey = open(keypath,'rb').read()
|
||||||
|
self.status['text'] = u"Decrypting..."
|
||||||
|
try:
|
||||||
|
decrypt_status = decryptBook(userkey, inpath, outpath)
|
||||||
|
except Exception, e:
|
||||||
|
self.status['text'] = u"Error: {0}".format(e.args[0])
|
||||||
|
return
|
||||||
|
if decrypt_status[0] == 0:
|
||||||
|
self.status['text'] = u"File successfully decrypted"
|
||||||
|
else:
|
||||||
|
self.status['text'] = decrypt_status[1]
|
||||||
|
|
||||||
|
root = Tkinter.Tk()
|
||||||
|
root.title(u"Barnes & Noble ePub Decrypter v.{0}".format(__version__))
|
||||||
|
root.resizable(True, False)
|
||||||
|
root.minsize(300, 0)
|
||||||
|
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||||
|
root.mainloop()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||||
|
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||||
|
sys.exit(cli_main())
|
||||||
|
sys.exit(gui_main())
|
||||||
@@ -1,388 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
# ignobleepub_plugin.py
|
|
||||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
|
||||||
# later. <http://www.gnu.org/licenses/>
|
|
||||||
#
|
|
||||||
# Requires Calibre version 0.6.44 or higher.
|
|
||||||
#
|
|
||||||
# All credit given to I <3 Cabbages for the original standalone scripts.
|
|
||||||
# I had the much easier job of converting them to Calibre a plugin.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
#
|
|
||||||
# Configuration:
|
|
||||||
# 1) The easiest way to configure the plugin is to enter your name (Barnes & Noble account
|
|
||||||
# name) and credit card number (the one used to purchase the books) into the plugin's
|
|
||||||
# customization window. Highlight the plugin (Ignoble Epub DeDRM) and click the
|
|
||||||
# "Customize Plugin" button on Calibre's Preferences->Plugins page.
|
|
||||||
# Enter the name and credit card number separated by a comma: Your Name,1234123412341234
|
|
||||||
#
|
|
||||||
# If you've purchased books with more than one credit card, separate the info with
|
|
||||||
# a colon: Your Name,1234123412341234:Other Name,2345234523452345
|
|
||||||
#
|
|
||||||
# ** Method 1 is your only option if you don't have/can't run the original
|
|
||||||
# I <3 Cabbages scripts on your particular machine. **
|
|
||||||
#
|
|
||||||
# 2) If you already have keyfiles generated with I <3 Cabbages' ignoblekeygen.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 '.b64' extension (like the ignoblekeygen
|
|
||||||
# script produces). This directory isn't touched when upgrading Calibre, so it's quite safe
|
|
||||||
# to leave then there.
|
|
||||||
#
|
|
||||||
# All keyfiles from option 2 and all data entered from option 1 will be used to attempt
|
|
||||||
# to decrypt a book. You can use option 1 or option 2, or a combination of both.
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
Decrypt Barnes & Noble ADEPT encrypted EPUB books.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import hashlib
|
|
||||||
import zlib
|
|
||||||
import zipfile
|
|
||||||
import re
|
|
||||||
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
|
||||||
import xml.etree.ElementTree as etree
|
|
||||||
from contextlib import closing
|
|
||||||
|
|
||||||
global AES
|
|
||||||
global AES2
|
|
||||||
|
|
||||||
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
|
|
||||||
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
|
||||||
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
|
||||||
|
|
||||||
class IGNOBLEError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _load_crypto_libcrypto():
|
|
||||||
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
|
||||||
Structure, c_ulong, create_string_buffer, cast
|
|
||||||
from ctypes.util import find_library
|
|
||||||
|
|
||||||
if sys.platform.startswith('win'):
|
|
||||||
libcrypto = find_library('libeay32')
|
|
||||||
else:
|
|
||||||
libcrypto = find_library('crypto')
|
|
||||||
if libcrypto is None:
|
|
||||||
raise IGNOBLEError('libcrypto not found')
|
|
||||||
libcrypto = CDLL(libcrypto)
|
|
||||||
|
|
||||||
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_set_encrypt_key = F(c_int, 'AES_set_encrypt_key',
|
|
||||||
[c_char_p, c_int, AES_KEY_p])
|
|
||||||
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
|
|
||||||
[c_char_p, c_int, AES_KEY_p])
|
|
||||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
|
||||||
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
|
||||||
c_int])
|
|
||||||
|
|
||||||
class AES(object):
|
|
||||||
def __init__(self, userkey):
|
|
||||||
self._blocksize = len(userkey)
|
|
||||||
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
|
||||||
raise IGNOBLEError('AES improper key used')
|
|
||||||
return
|
|
||||||
key = self._key = AES_KEY()
|
|
||||||
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
|
|
||||||
if rv < 0:
|
|
||||||
raise IGNOBLEError('Failed to initialize AES key')
|
|
||||||
|
|
||||||
def decrypt(self, data):
|
|
||||||
out = create_string_buffer(len(data))
|
|
||||||
iv = ("\x00" * self._blocksize)
|
|
||||||
rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
|
|
||||||
if rv == 0:
|
|
||||||
raise IGNOBLEError('AES decryption failed')
|
|
||||||
return out.raw
|
|
||||||
|
|
||||||
class AES2(object):
|
|
||||||
def __init__(self, userkey, iv):
|
|
||||||
self._blocksize = len(userkey)
|
|
||||||
self._iv = iv
|
|
||||||
key = self._key = AES_KEY()
|
|
||||||
rv = AES_set_encrypt_key(userkey, len(userkey) * 8, key)
|
|
||||||
if rv < 0:
|
|
||||||
raise IGNOBLEError('Failed to initialize AES Encrypt key')
|
|
||||||
|
|
||||||
def encrypt(self, data):
|
|
||||||
out = create_string_buffer(len(data))
|
|
||||||
rv = AES_cbc_encrypt(data, out, len(data), self._key, self._iv, 1)
|
|
||||||
if rv == 0:
|
|
||||||
raise IGNOBLEError('AES encryption failed')
|
|
||||||
return out.raw
|
|
||||||
print 'IgnobleEpub: Using libcrypto.'
|
|
||||||
return (AES, AES2)
|
|
||||||
|
|
||||||
def _load_crypto_pycrypto():
|
|
||||||
from Crypto.Cipher import AES as _AES
|
|
||||||
|
|
||||||
class AES(object):
|
|
||||||
def __init__(self, key):
|
|
||||||
self._aes = _AES.new(key, _AES.MODE_CBC)
|
|
||||||
|
|
||||||
def decrypt(self, data):
|
|
||||||
return self._aes.decrypt(data)
|
|
||||||
|
|
||||||
class AES2(object):
|
|
||||||
def __init__(self, key, iv):
|
|
||||||
self._aes = _AES.new(key, _AES.MODE_CBC, iv)
|
|
||||||
|
|
||||||
def encrypt(self, data):
|
|
||||||
return self._aes.encrypt(data)
|
|
||||||
print 'IgnobleEpub: Using PyCrypto.'
|
|
||||||
return (AES, AES2)
|
|
||||||
|
|
||||||
def _load_crypto():
|
|
||||||
_aes = _aes2 = None
|
|
||||||
for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto):
|
|
||||||
try:
|
|
||||||
_aes, _aes2 = loader()
|
|
||||||
break
|
|
||||||
except (ImportError, IGNOBLEError):
|
|
||||||
pass
|
|
||||||
return (_aes, _aes2)
|
|
||||||
|
|
||||||
def normalize_name(name): # Strip spaces and convert to lowercase.
|
|
||||||
return ''.join(x for x in name.lower() if x != ' ')
|
|
||||||
|
|
||||||
def generate_keyfile(name, ccn):
|
|
||||||
name = normalize_name(name) + '\x00'
|
|
||||||
ccn = ccn + '\x00'
|
|
||||||
name_sha = hashlib.sha1(name).digest()[:16]
|
|
||||||
ccn_sha = hashlib.sha1(ccn).digest()[:16]
|
|
||||||
both_sha = hashlib.sha1(name + ccn).digest()
|
|
||||||
aes = AES2(ccn_sha, name_sha)
|
|
||||||
crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c))
|
|
||||||
userkey = hashlib.sha1(crypt).digest()
|
|
||||||
|
|
||||||
return userkey.encode('base64')
|
|
||||||
|
|
||||||
class ZipInfo(zipfile.ZipInfo):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
if 'compress_type' in kwargs:
|
|
||||||
compress_type = kwargs.pop('compress_type')
|
|
||||||
super(ZipInfo, self).__init__(*args, **kwargs)
|
|
||||||
self.compress_type = compress_type
|
|
||||||
|
|
||||||
class Decryptor(object):
|
|
||||||
def __init__(self, bookkey, encryption):
|
|
||||||
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
|
|
||||||
self._aes = AES(bookkey)
|
|
||||||
encryption = etree.fromstring(encryption)
|
|
||||||
self._encrypted = encrypted = set()
|
|
||||||
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
|
|
||||||
enc('CipherReference'))
|
|
||||||
for elem in encryption.findall(expr):
|
|
||||||
path = elem.get('URI', None)
|
|
||||||
if path is not None:
|
|
||||||
encrypted.add(path)
|
|
||||||
|
|
||||||
def decompress(self, bytes):
|
|
||||||
dc = zlib.decompressobj(-15)
|
|
||||||
bytes = dc.decompress(bytes)
|
|
||||||
ex = dc.decompress('Z') + dc.flush()
|
|
||||||
if ex:
|
|
||||||
bytes = bytes + ex
|
|
||||||
return bytes
|
|
||||||
|
|
||||||
def decrypt(self, path, data):
|
|
||||||
if path in self._encrypted:
|
|
||||||
data = self._aes.decrypt(data)[16:]
|
|
||||||
data = data[:-ord(data[-1])]
|
|
||||||
data = self.decompress(data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def plugin_main(userkey, inpath, outpath):
|
|
||||||
key = userkey.decode('base64')[:16]
|
|
||||||
aes = AES(key)
|
|
||||||
|
|
||||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
|
||||||
namelist = set(inf.namelist())
|
|
||||||
if 'META-INF/rights.xml' not in namelist or \
|
|
||||||
'META-INF/encryption.xml' not in namelist:
|
|
||||||
return 1
|
|
||||||
for name in META_NAMES:
|
|
||||||
namelist.remove(name)
|
|
||||||
try: # If the generated keyfile doesn't match the bookkey, this is where it's likely to blow up.
|
|
||||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
|
||||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
|
||||||
expr = './/%s' % (adept('encryptedKey'),)
|
|
||||||
bookkey = ''.join(rights.findtext(expr))
|
|
||||||
bookkey = aes.decrypt(bookkey.decode('base64'))
|
|
||||||
bookkey = bookkey[:-ord(bookkey[-1])]
|
|
||||||
encryption = inf.read('META-INF/encryption.xml')
|
|
||||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
|
||||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
|
||||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
|
||||||
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
|
||||||
outf.writestr(zi, inf.read('mimetype'))
|
|
||||||
for path in namelist:
|
|
||||||
data = inf.read(path)
|
|
||||||
outf.writestr(path, decryptor.decrypt(path, data))
|
|
||||||
except:
|
|
||||||
return 2
|
|
||||||
return 0
|
|
||||||
|
|
||||||
from calibre.customize import FileTypePlugin
|
|
||||||
|
|
||||||
class IgnobleDeDRM(FileTypePlugin):
|
|
||||||
name = 'Ignoble Epub DeDRM'
|
|
||||||
description = 'Removes DRM from secure Barnes & Noble epub files. \
|
|
||||||
Credit given to I <3 Cabbages for the original stand-alone scripts.'
|
|
||||||
supported_platforms = ['linux', 'osx', 'windows']
|
|
||||||
author = 'DiapDealer'
|
|
||||||
version = (0, 1, 1)
|
|
||||||
minimum_calibre_version = (0, 6, 44) # Compiled python libraries cannot be imported in earlier versions.
|
|
||||||
file_types = set(['epub'])
|
|
||||||
on_import = True
|
|
||||||
|
|
||||||
def run(self, path_to_ebook):
|
|
||||||
global AES
|
|
||||||
global AES2
|
|
||||||
|
|
||||||
from calibre.gui2 import is_ok_to_use_qt
|
|
||||||
from PyQt4.Qt import QMessageBox
|
|
||||||
from calibre.constants import iswindows, isosx
|
|
||||||
|
|
||||||
# Add the included pycrypto import directory for Windows users.
|
|
||||||
pdir = 'windows' if iswindows else 'osx' if isosx else 'linux'
|
|
||||||
ppath = os.path.join(self.sys_insertion_path, pdir)
|
|
||||||
sys.path.append(ppath)
|
|
||||||
|
|
||||||
AES, AES2 = _load_crypto()
|
|
||||||
|
|
||||||
if AES == None or AES2 == None:
|
|
||||||
# Failed to load libcrypto or PyCrypto... Adobe Epubs can't be decrypted.'
|
|
||||||
sys.path.remove(ppath)
|
|
||||||
raise IGNOBLEError('IgnobleEpub - Failed to load crypto libs.')
|
|
||||||
return
|
|
||||||
|
|
||||||
# Load any keyfiles (*.b64) included Calibre's config directory.
|
|
||||||
userkeys = []
|
|
||||||
try:
|
|
||||||
# Find Calibre's configuration directory.
|
|
||||||
confpath = os.path.split(os.path.split(self.plugin_path)[0])[0]
|
|
||||||
print 'IgnobleEpub: Calibre configuration directory = %s' % confpath
|
|
||||||
files = os.listdir(confpath)
|
|
||||||
filefilter = re.compile("\.b64$", re.IGNORECASE)
|
|
||||||
files = filter(filefilter.search, files)
|
|
||||||
|
|
||||||
if files:
|
|
||||||
for filename in files:
|
|
||||||
fpath = os.path.join(confpath, filename)
|
|
||||||
with open(fpath, 'rb') as f:
|
|
||||||
userkeys.append(f.read())
|
|
||||||
print 'IgnobleEpub: Keyfile %s found in config folder.' % filename
|
|
||||||
else:
|
|
||||||
print 'IgnobleEpub: No keyfiles found. Checking plugin customization string.'
|
|
||||||
except IOError:
|
|
||||||
print 'IgnobleEpub: Error reading keyfiles from config directory.'
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Get name and credit card number from Plugin Customization
|
|
||||||
if not userkeys and not self.site_customization:
|
|
||||||
# Plugin hasn't been configured... do nothing.
|
|
||||||
sys.path.remove(ppath)
|
|
||||||
raise IGNOBLEError('IgnobleEpub - No keys found. Plugin not configured.')
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.site_customization:
|
|
||||||
keystuff = self.site_customization
|
|
||||||
ar = keystuff.split(':')
|
|
||||||
keycount = 0
|
|
||||||
for i in ar:
|
|
||||||
try:
|
|
||||||
name, ccn = i.split(',')
|
|
||||||
keycount += 1
|
|
||||||
except ValueError:
|
|
||||||
sys.path.remove(ppath)
|
|
||||||
raise IGNOBLEError('IgnobleEpub - Error parsing user supplied data.')
|
|
||||||
return
|
|
||||||
|
|
||||||
# Generate Barnes & Noble EPUB user key from name and credit card number.
|
|
||||||
userkeys.append( generate_keyfile(name, ccn) )
|
|
||||||
print 'IgnobleEpub: %d userkey(s) generated from customization data.' % keycount
|
|
||||||
|
|
||||||
# Attempt to decrypt epub with each encryption key (generated or provided).
|
|
||||||
for userkey in userkeys:
|
|
||||||
# Create a TemporaryPersistent file to work with.
|
|
||||||
# Check original epub archive for zip errors.
|
|
||||||
import zipfix
|
|
||||||
inf = self.temporary_file('.epub')
|
|
||||||
try:
|
|
||||||
fr = zipfix.fixZip(path_to_ebook, inf.name)
|
|
||||||
fr.fix()
|
|
||||||
except Exception, e:
|
|
||||||
raise Exception(e)
|
|
||||||
return
|
|
||||||
of = self.temporary_file('.epub')
|
|
||||||
|
|
||||||
# Give the user key, ebook and TemporaryPersistent file to the Stripper function.
|
|
||||||
result = plugin_main(userkey, inf.name, of.name)
|
|
||||||
|
|
||||||
# Ebook is not a B&N Adept epub... do nothing and pass it on.
|
|
||||||
# This allows a non-encrypted epub to be imported without error messages.
|
|
||||||
if result == 1:
|
|
||||||
print 'IgnobleEpub: Not a B&N Adept Epub... punting.'
|
|
||||||
of.close()
|
|
||||||
sys.path.remove(ppath)
|
|
||||||
return path_to_ebook
|
|
||||||
break
|
|
||||||
|
|
||||||
# Decryption was successful return the modified PersistentTemporary
|
|
||||||
# file to Calibre's import process.
|
|
||||||
if result == 0:
|
|
||||||
print 'IgnobleEpub: Encryption successfully removed.'
|
|
||||||
of.close()
|
|
||||||
sys.path.remove(ppath)
|
|
||||||
return of.name
|
|
||||||
break
|
|
||||||
|
|
||||||
print 'IgnobleEpub: Encryption key invalid... trying others.'
|
|
||||||
of.close()
|
|
||||||
|
|
||||||
# Something went wrong with decryption.
|
|
||||||
# Import the original unmolested epub.
|
|
||||||
of.close
|
|
||||||
sys.path.remove(ppath)
|
|
||||||
raise IGNOBLEError('IgnobleEpub - Ultimately failed to decrypt.')
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def customization_help(self, gui=False):
|
|
||||||
return 'Enter B&N Account name and CC# (separate name and CC# with a comma)'
|
|
||||||
319
Calibre_Plugins/ignobleepub_plugin/ignoblekeygen.py
Normal file
319
Calibre_Plugins/ignobleepub_plugin/ignoblekeygen.py
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
# ignoblekeygen.pyw, version 2.5
|
||||||
|
# Copyright © 2009-2010 by i♥cabbages
|
||||||
|
|
||||||
|
# Released under the terms of the GNU General Public Licence, version 3
|
||||||
|
# <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
|
# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||||
|
|
||||||
|
# Windows users: Before running this program, you must first install Python 2.6
|
||||||
|
# from <http://www.python.org/download/> and PyCrypto from
|
||||||
|
# <http://www.voidspace.org.uk/python/modules.shtml#pycrypto> (make sure to
|
||||||
|
# install the version for Python 2.6). Save this script file as
|
||||||
|
# ignoblekeygen.pyw and double-click on it to run it.
|
||||||
|
#
|
||||||
|
# Mac OS X users: Save this script file as ignoblekeygen.pyw. You can run this
|
||||||
|
# program from the command line (pythonw ignoblekeygen.pyw) or by double-clicking
|
||||||
|
# it when it has been associated with PythonLauncher.
|
||||||
|
|
||||||
|
# Revision history:
|
||||||
|
# 1 - Initial release
|
||||||
|
# 2 - Add OS X support by using OpenSSL when available (taken/modified from ineptepub v5)
|
||||||
|
# 2.1 - Allow Windows versions of libcrypto to be found
|
||||||
|
# 2.2 - On Windows try PyCrypto first and then OpenSSL next
|
||||||
|
# 2.3 - Modify interface to allow use of import
|
||||||
|
# 2.4 - Improvements to UI and now works in plugins
|
||||||
|
# 2.5 - Additional improvement for unicode and plugin support
|
||||||
|
|
||||||
|
"""
|
||||||
|
Generate Barnes & Noble EPUB user key from name and credit card number.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__version__ = "2.5"
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
iswindows = sys.platform.startswith('win')
|
||||||
|
isosx = sys.platform.startswith('darwin')
|
||||||
|
|
||||||
|
def unicode_argv():
|
||||||
|
if iswindows:
|
||||||
|
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||||
|
# strings.
|
||||||
|
|
||||||
|
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||||
|
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||||
|
# characters with '?'.
|
||||||
|
|
||||||
|
|
||||||
|
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||||
|
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||||
|
|
||||||
|
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||||
|
GetCommandLineW.argtypes = []
|
||||||
|
GetCommandLineW.restype = LPCWSTR
|
||||||
|
|
||||||
|
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||||
|
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||||
|
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||||
|
|
||||||
|
cmd = GetCommandLineW()
|
||||||
|
argc = c_int(0)
|
||||||
|
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||||
|
if argc.value > 0:
|
||||||
|
# Remove Python executable and commands if present
|
||||||
|
start = argc.value - len(sys.argv)
|
||||||
|
return [argv[i] for i in
|
||||||
|
xrange(start, argc.value)]
|
||||||
|
# if we don't have any arguments at all, just pass back script name
|
||||||
|
# this should never happen
|
||||||
|
return [u"ignoblekeygen.py"]
|
||||||
|
else:
|
||||||
|
argvencoding = sys.stdin.encoding
|
||||||
|
if argvencoding == None:
|
||||||
|
argvencoding = "utf-8"
|
||||||
|
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||||
|
|
||||||
|
|
||||||
|
class IGNOBLEError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _load_crypto_libcrypto():
|
||||||
|
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
||||||
|
Structure, c_ulong, create_string_buffer, cast
|
||||||
|
from ctypes.util import find_library
|
||||||
|
|
||||||
|
if iswindows:
|
||||||
|
libcrypto = find_library('libeay32')
|
||||||
|
else:
|
||||||
|
libcrypto = find_library('crypto')
|
||||||
|
|
||||||
|
if libcrypto is None:
|
||||||
|
raise IGNOBLEError('libcrypto not found')
|
||||||
|
libcrypto = CDLL(libcrypto)
|
||||||
|
|
||||||
|
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_set_encrypt_key = F(c_int, 'AES_set_encrypt_key',
|
||||||
|
[c_char_p, c_int, AES_KEY_p])
|
||||||
|
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||||
|
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
||||||
|
c_int])
|
||||||
|
|
||||||
|
class AES(object):
|
||||||
|
def __init__(self, userkey, iv):
|
||||||
|
self._blocksize = len(userkey)
|
||||||
|
self._iv = iv
|
||||||
|
key = self._key = AES_KEY()
|
||||||
|
rv = AES_set_encrypt_key(userkey, len(userkey) * 8, key)
|
||||||
|
if rv < 0:
|
||||||
|
raise IGNOBLEError('Failed to initialize AES Encrypt key')
|
||||||
|
|
||||||
|
def encrypt(self, data):
|
||||||
|
out = create_string_buffer(len(data))
|
||||||
|
rv = AES_cbc_encrypt(data, out, len(data), self._key, self._iv, 1)
|
||||||
|
if rv == 0:
|
||||||
|
raise IGNOBLEError('AES encryption failed')
|
||||||
|
return out.raw
|
||||||
|
|
||||||
|
return AES
|
||||||
|
|
||||||
|
def _load_crypto_pycrypto():
|
||||||
|
from Crypto.Cipher import AES as _AES
|
||||||
|
|
||||||
|
class AES(object):
|
||||||
|
def __init__(self, key, iv):
|
||||||
|
self._aes = _AES.new(key, _AES.MODE_CBC, iv)
|
||||||
|
|
||||||
|
def encrypt(self, data):
|
||||||
|
return self._aes.encrypt(data)
|
||||||
|
|
||||||
|
return AES
|
||||||
|
|
||||||
|
def _load_crypto():
|
||||||
|
AES = None
|
||||||
|
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
||||||
|
for loader in cryptolist:
|
||||||
|
try:
|
||||||
|
AES = loader()
|
||||||
|
break
|
||||||
|
except (ImportError, IGNOBLEError):
|
||||||
|
pass
|
||||||
|
return AES
|
||||||
|
|
||||||
|
AES = _load_crypto()
|
||||||
|
|
||||||
|
def normalize_name(name):
|
||||||
|
return ''.join(x for x in name.lower() if x != ' ')
|
||||||
|
|
||||||
|
|
||||||
|
def generate_key(name, ccn):
|
||||||
|
# remove spaces and case from name and CC numbers.
|
||||||
|
if type(name)==unicode:
|
||||||
|
name = name.encode('utf-8')
|
||||||
|
if type(ccn)==unicode:
|
||||||
|
ccn = ccn.encode('utf-8')
|
||||||
|
|
||||||
|
name = normalize_name(name) + '\x00'
|
||||||
|
ccn = normalize_name(ccn) + '\x00'
|
||||||
|
|
||||||
|
name_sha = hashlib.sha1(name).digest()[:16]
|
||||||
|
ccn_sha = hashlib.sha1(ccn).digest()[:16]
|
||||||
|
both_sha = hashlib.sha1(name + ccn).digest()
|
||||||
|
aes = AES(ccn_sha, name_sha)
|
||||||
|
crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c))
|
||||||
|
userkey = hashlib.sha1(crypt).digest()
|
||||||
|
return userkey.encode('base64')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def cli_main(argv=unicode_argv()):
|
||||||
|
progname = os.path.basename(argv[0])
|
||||||
|
if AES is None:
|
||||||
|
print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \
|
||||||
|
"separately. Read the top-of-script comment for details." % \
|
||||||
|
(progname,)
|
||||||
|
return 1
|
||||||
|
if len(argv) != 4:
|
||||||
|
print u"usage: {0} <Name> <CC#> <keyfileout.b64>".format(progname)
|
||||||
|
return 1
|
||||||
|
name, ccn, keypath = argv[1:]
|
||||||
|
userkey = generate_key(name, ccn)
|
||||||
|
open(keypath,'wb').write(userkey)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def gui_main():
|
||||||
|
import Tkinter
|
||||||
|
import Tkconstants
|
||||||
|
import tkFileDialog
|
||||||
|
import tkMessageBox
|
||||||
|
|
||||||
|
class DecryptionDialog(Tkinter.Frame):
|
||||||
|
def __init__(self, root):
|
||||||
|
Tkinter.Frame.__init__(self, root, border=5)
|
||||||
|
self.status = Tkinter.Label(self, text=u"Enter parameters")
|
||||||
|
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||||
|
body = Tkinter.Frame(self)
|
||||||
|
body.pack(fill=Tkconstants.X, expand=1)
|
||||||
|
sticky = Tkconstants.E + Tkconstants.W
|
||||||
|
body.grid_columnconfigure(1, weight=2)
|
||||||
|
Tkinter.Label(body, text=u"Account Name").grid(row=0)
|
||||||
|
self.name = Tkinter.Entry(body, width=40)
|
||||||
|
self.name.grid(row=0, column=1, sticky=sticky)
|
||||||
|
Tkinter.Label(body, text=u"CC#").grid(row=1)
|
||||||
|
self.ccn = Tkinter.Entry(body, width=40)
|
||||||
|
self.ccn.grid(row=1, column=1, sticky=sticky)
|
||||||
|
Tkinter.Label(body, text=u"Output file").grid(row=2)
|
||||||
|
self.keypath = Tkinter.Entry(body, width=40)
|
||||||
|
self.keypath.grid(row=2, column=1, sticky=sticky)
|
||||||
|
self.keypath.insert(2, u"bnepubkey.b64")
|
||||||
|
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
|
||||||
|
button.grid(row=2, column=2)
|
||||||
|
buttons = Tkinter.Frame(self)
|
||||||
|
buttons.pack()
|
||||||
|
botton = Tkinter.Button(
|
||||||
|
buttons, text=u"Generate", width=10, command=self.generate)
|
||||||
|
botton.pack(side=Tkconstants.LEFT)
|
||||||
|
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||||
|
button = Tkinter.Button(
|
||||||
|
buttons, text=u"Quit", width=10, command=self.quit)
|
||||||
|
button.pack(side=Tkconstants.RIGHT)
|
||||||
|
|
||||||
|
def get_keypath(self):
|
||||||
|
keypath = tkFileDialog.asksaveasfilename(
|
||||||
|
parent=None, title=u"Select B&N ePub key file to produce",
|
||||||
|
defaultextension=u".b64",
|
||||||
|
filetypes=[('base64-encoded files', '.b64'),
|
||||||
|
('All Files', '.*')])
|
||||||
|
if keypath:
|
||||||
|
keypath = os.path.normpath(keypath)
|
||||||
|
self.keypath.delete(0, Tkconstants.END)
|
||||||
|
self.keypath.insert(0, keypath)
|
||||||
|
return
|
||||||
|
|
||||||
|
def generate(self):
|
||||||
|
name = self.name.get()
|
||||||
|
ccn = self.ccn.get()
|
||||||
|
keypath = self.keypath.get()
|
||||||
|
if not name:
|
||||||
|
self.status['text'] = u"Name not specified"
|
||||||
|
return
|
||||||
|
if not ccn:
|
||||||
|
self.status['text'] = u"Credit card number not specified"
|
||||||
|
return
|
||||||
|
if not keypath:
|
||||||
|
self.status['text'] = u"Output keyfile path not specified"
|
||||||
|
return
|
||||||
|
self.status['text'] = u"Generating..."
|
||||||
|
try:
|
||||||
|
userkey = generate_key(name, ccn)
|
||||||
|
except Exception, e:
|
||||||
|
self.status['text'] = u"Error: (0}".format(e.args[0])
|
||||||
|
return
|
||||||
|
open(keypath,'wb').write(userkey)
|
||||||
|
self.status['text'] = u"Keyfile successfully generated"
|
||||||
|
|
||||||
|
root = Tkinter.Tk()
|
||||||
|
if AES is None:
|
||||||
|
root.withdraw()
|
||||||
|
tkMessageBox.showerror(
|
||||||
|
"Ignoble EPUB Keyfile Generator",
|
||||||
|
"This script requires OpenSSL or PyCrypto, which must be installed "
|
||||||
|
"separately. Read the top-of-script comment for details.")
|
||||||
|
return 1
|
||||||
|
root.title(u"Barnes & Noble ePub Keyfile Generator v.{0}".format(__version__))
|
||||||
|
root.resizable(True, False)
|
||||||
|
root.minsize(300, 0)
|
||||||
|
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||||
|
root.mainloop()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||||
|
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||||
|
sys.exit(cli_main())
|
||||||
|
sys.exit(gui_main())
|
||||||
39
Calibre_Plugins/ignobleepub_plugin/utilities.py
Normal file
39
Calibre_Plugins/ignobleepub_plugin/utilities.py
Normal file
@@ -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.
@@ -1,51 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# ===================================================================
|
|
||||||
# The contents of this file are dedicated to the public domain. To
|
|
||||||
# the extent that dedication to the public domain is not available,
|
|
||||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
|
||||||
# non-exclusive license to exercise all rights associated with the
|
|
||||||
# contents of this file for any purpose whatsoever.
|
|
||||||
# No rights are reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
# ===================================================================
|
|
||||||
|
|
||||||
"""Secret-key encryption algorithms.
|
|
||||||
|
|
||||||
Secret-key encryption algorithms transform plaintext in some way that
|
|
||||||
is dependent on a key, producing ciphertext. This transformation can
|
|
||||||
easily be reversed, if (and, hopefully, only if) one knows the key.
|
|
||||||
|
|
||||||
The encryption modules here all support the interface described in PEP
|
|
||||||
272, "API for Block Encryption Algorithms".
|
|
||||||
|
|
||||||
If you don't know which algorithm to choose, use AES because it's
|
|
||||||
standard and has undergone a fair bit of examination.
|
|
||||||
|
|
||||||
Crypto.Cipher.AES Advanced Encryption Standard
|
|
||||||
Crypto.Cipher.ARC2 Alleged RC2
|
|
||||||
Crypto.Cipher.ARC4 Alleged RC4
|
|
||||||
Crypto.Cipher.Blowfish
|
|
||||||
Crypto.Cipher.CAST
|
|
||||||
Crypto.Cipher.DES The Data Encryption Standard. Very commonly used
|
|
||||||
in the past, but today its 56-bit keys are too small.
|
|
||||||
Crypto.Cipher.DES3 Triple DES.
|
|
||||||
Crypto.Cipher.XOR The simple XOR cipher.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__all__ = ['AES', 'ARC2', 'ARC4',
|
|
||||||
'Blowfish', 'CAST', 'DES', 'DES3',
|
|
||||||
'XOR'
|
|
||||||
]
|
|
||||||
|
|
||||||
__revision__ = "$Id$"
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# ===================================================================
|
|
||||||
# The contents of this file are dedicated to the public domain. To
|
|
||||||
# the extent that dedication to the public domain is not available,
|
|
||||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
|
||||||
# non-exclusive license to exercise all rights associated with the
|
|
||||||
# contents of this file for any purpose whatsoever.
|
|
||||||
# No rights are reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
# ===================================================================
|
|
||||||
|
|
||||||
"""Python Cryptography Toolkit
|
|
||||||
|
|
||||||
A collection of cryptographic modules implementing various algorithms
|
|
||||||
and protocols.
|
|
||||||
|
|
||||||
Subpackages:
|
|
||||||
Crypto.Cipher Secret-key encryption algorithms (AES, DES, ARC4)
|
|
||||||
Crypto.Hash Hashing algorithms (MD5, SHA, HMAC)
|
|
||||||
Crypto.Protocol Cryptographic protocols (Chaffing, all-or-nothing
|
|
||||||
transform). This package does not contain any
|
|
||||||
network protocols.
|
|
||||||
Crypto.PublicKey Public-key encryption and signature algorithms
|
|
||||||
(RSA, DSA)
|
|
||||||
Crypto.Util Various useful modules and functions (long-to-string
|
|
||||||
conversion, random number generation, number
|
|
||||||
theoretic functions)
|
|
||||||
"""
|
|
||||||
|
|
||||||
__all__ = ['Cipher', 'Hash', 'Protocol', 'PublicKey', 'Util']
|
|
||||||
|
|
||||||
__version__ = '2.3' # See also below and setup.py
|
|
||||||
__revision__ = "$Id$"
|
|
||||||
|
|
||||||
# New software should look at this instead of at __version__ above.
|
|
||||||
version_info = (2, 1, 0, 'final', 0) # See also above and setup.py
|
|
||||||
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
# -*- coding: ascii -*-
|
|
||||||
#
|
|
||||||
# pct_warnings.py : PyCrypto warnings file
|
|
||||||
#
|
|
||||||
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
|
|
||||||
#
|
|
||||||
# ===================================================================
|
|
||||||
# The contents of this file are dedicated to the public domain. To
|
|
||||||
# the extent that dedication to the public domain is not available,
|
|
||||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
|
||||||
# non-exclusive license to exercise all rights associated with the
|
|
||||||
# contents of this file for any purpose whatsoever.
|
|
||||||
# No rights are reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
# ===================================================================
|
|
||||||
|
|
||||||
#
|
|
||||||
# Base classes. All our warnings inherit from one of these in order to allow
|
|
||||||
# the user to specifically filter them.
|
|
||||||
#
|
|
||||||
|
|
||||||
class CryptoWarning(Warning):
|
|
||||||
"""Base class for PyCrypto warnings"""
|
|
||||||
|
|
||||||
class CryptoDeprecationWarning(DeprecationWarning, CryptoWarning):
|
|
||||||
"""Base PyCrypto DeprecationWarning class"""
|
|
||||||
|
|
||||||
class CryptoRuntimeWarning(RuntimeWarning, CryptoWarning):
|
|
||||||
"""Base PyCrypto RuntimeWarning class"""
|
|
||||||
|
|
||||||
#
|
|
||||||
# Warnings that we might actually use
|
|
||||||
#
|
|
||||||
|
|
||||||
class RandomPool_DeprecationWarning(CryptoDeprecationWarning):
|
|
||||||
"""Issued when Crypto.Util.randpool.RandomPool is instantiated."""
|
|
||||||
|
|
||||||
class ClockRewindWarning(CryptoRuntimeWarning):
|
|
||||||
"""Warning for when the system clock moves backwards."""
|
|
||||||
|
|
||||||
class GetRandomNumber_DeprecationWarning(CryptoDeprecationWarning):
|
|
||||||
"""Issued when Crypto.Util.number.getRandomNumber is invoked."""
|
|
||||||
|
|
||||||
# By default, we want this warning to be shown every time we compensate for
|
|
||||||
# clock rewinding.
|
|
||||||
import warnings as _warnings
|
|
||||||
_warnings.filterwarnings('always', category=ClockRewindWarning, append=1)
|
|
||||||
|
|
||||||
# vim:set ts=4 sw=4 sts=4 expandtab:
|
|
||||||
1400
Calibre_Plugins/ignobleepub_plugin/zipfilerugged.py
Normal file
1400
Calibre_Plugins/ignobleepub_plugin/zipfilerugged.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import zlib
|
import zlib
|
||||||
import zipfile
|
import zipfilerugged
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import getopt
|
import getopt
|
||||||
@@ -13,11 +14,22 @@ _FILENAME_LEN_OFFSET = 26
|
|||||||
_EXTRA_LEN_OFFSET = 28
|
_EXTRA_LEN_OFFSET = 28
|
||||||
_FILENAME_OFFSET = 30
|
_FILENAME_OFFSET = 30
|
||||||
_MAX_SIZE = 64 * 1024
|
_MAX_SIZE = 64 * 1024
|
||||||
|
_MIMETYPE = 'application/epub+zip'
|
||||||
|
|
||||||
|
class ZipInfo(zipfilerugged.ZipInfo):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
if 'compress_type' in kwargs:
|
||||||
|
compress_type = kwargs.pop('compress_type')
|
||||||
|
super(ZipInfo, self).__init__(*args, **kwargs)
|
||||||
|
self.compress_type = compress_type
|
||||||
|
|
||||||
class fixZip:
|
class fixZip:
|
||||||
def __init__(self, zinput, zoutput):
|
def __init__(self, zinput, zoutput):
|
||||||
self.inzip = zipfile.ZipFile(zinput,'r')
|
self.ztype = 'zip'
|
||||||
self.outzip = zipfile.ZipFile(zoutput,'w')
|
if zinput.lower().find('.epub') >= 0 :
|
||||||
|
self.ztype = 'epub'
|
||||||
|
self.inzip = zipfilerugged.ZipFile(zinput,'r')
|
||||||
|
self.outzip = zipfilerugged.ZipFile(zoutput,'w')
|
||||||
# open the input zip for reading only as a raw file
|
# open the input zip for reading only as a raw file
|
||||||
self.bzf = file(zinput,'rb')
|
self.bzf = file(zinput,'rb')
|
||||||
|
|
||||||
@@ -65,11 +77,11 @@ class fixZip:
|
|||||||
data = None
|
data = None
|
||||||
|
|
||||||
# if not compressed we are good to go
|
# if not compressed we are good to go
|
||||||
if zi.compress_type == zipfile.ZIP_STORED:
|
if zi.compress_type == zipfilerugged.ZIP_STORED:
|
||||||
data = self.bzf.read(zi.file_size)
|
data = self.bzf.read(zi.file_size)
|
||||||
|
|
||||||
# if compressed we must decompress it using zlib
|
# if compressed we must decompress it using zlib
|
||||||
if zi.compress_type == zipfile.ZIP_DEFLATED:
|
if zi.compress_type == zipfilerugged.ZIP_DEFLATED:
|
||||||
cmpdata = self.bzf.read(zi.compress_size)
|
cmpdata = self.bzf.read(zi.compress_size)
|
||||||
data = self.uncompress(cmpdata)
|
data = self.uncompress(cmpdata)
|
||||||
|
|
||||||
@@ -82,13 +94,19 @@ class fixZip:
|
|||||||
# and copy member over to output archive
|
# and copy member over to output archive
|
||||||
# if problems exist with local vs central filename, fix them
|
# if problems exist with local vs central filename, fix them
|
||||||
|
|
||||||
for i, zinfo in enumerate(self.inzip.infolist()):
|
# if epub write mimetype file first, with no compression
|
||||||
|
if self.ztype == 'epub':
|
||||||
|
nzinfo = ZipInfo('mimetype', compress_type=zipfilerugged.ZIP_STORED)
|
||||||
|
self.outzip.writestr(nzinfo, _MIMETYPE)
|
||||||
|
|
||||||
|
# write the rest of the files
|
||||||
|
for zinfo in self.inzip.infolist():
|
||||||
|
if zinfo.filename != "mimetype" or self.ztype == '.zip':
|
||||||
data = None
|
data = None
|
||||||
nzinfo = zinfo
|
nzinfo = zinfo
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = self.inzip.read(zinfo)
|
data = self.inzip.read(zinfo.filename)
|
||||||
except zipfile.BadZipfile or zipfile.error:
|
except zipfilerugged.BadZipfile or zipfilerugged.error:
|
||||||
local_name = self.getlocalname(zinfo)
|
local_name = self.getlocalname(zinfo)
|
||||||
data = self.getfiledata(zinfo)
|
data = self.getfiledata(zinfo)
|
||||||
nzinfo.filename = local_name
|
nzinfo.filename = local_name
|
||||||
@@ -111,14 +129,7 @@ def usage():
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def main(argv=sys.argv):
|
def repairBook(infile, outfile):
|
||||||
if len(argv)!=3:
|
|
||||||
usage()
|
|
||||||
return 1
|
|
||||||
infile = None
|
|
||||||
outfile = None
|
|
||||||
infile = argv[1]
|
|
||||||
outfile = argv[2]
|
|
||||||
if not os.path.exists(infile):
|
if not os.path.exists(infile):
|
||||||
print "Error: Input Zip File does not exist"
|
print "Error: Input Zip File does not exist"
|
||||||
return 1
|
return 1
|
||||||
@@ -130,7 +141,15 @@ def main(argv=sys.argv):
|
|||||||
print "Error Occurred ", e
|
print "Error Occurred ", e
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=sys.argv):
|
||||||
|
if len(argv)!=3:
|
||||||
|
usage()
|
||||||
|
return 1
|
||||||
|
infile = argv[1]
|
||||||
|
outfile = argv[2]
|
||||||
|
return repairBook(infile, outfile)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__' :
|
if __name__ == '__main__' :
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
213
Calibre_Plugins/ineptepub_plugin/__init__.py
Normal file
213
Calibre_Plugins/ineptepub_plugin/__init__.py
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
#!/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 or
|
||||||
|
# later. <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.
|
||||||
|
|
||||||
|
|
||||||
|
PLUGIN_NAME = u"Inept Epub DeDRM"
|
||||||
|
PLUGIN_VERSION_TUPLE = (0, 2, 0)
|
||||||
|
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 when checking zip archive.".format(PLUGIN_NAME, PLUGIN_VERSION)
|
||||||
|
raise Exception(e)
|
||||||
|
return
|
||||||
|
|
||||||
|
#check the book
|
||||||
|
from calibre_plugins.ineptepub import ineptepub
|
||||||
|
if not ineptepub.adeptBook(inf.name):
|
||||||
|
print u"{0} v{1}: {2} is not a secure Adobe Adept ePub.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))
|
||||||
|
# return the original file, so that no error message is generated in the GUI
|
||||||
|
return 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:
|
||||||
|
print u"{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, userkeyinfo[1])
|
||||||
|
of = self.temporary_file(u".epub")
|
||||||
|
|
||||||
|
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
|
||||||
|
result = ineptepub.decryptBook(userkeyinfo[0], inf.name, of.name)
|
||||||
|
|
||||||
|
# Ebook is not an Adobe Adept epub... do nothing and pass it on.
|
||||||
|
# This allows a non-encrypted epub to be imported without error messages.
|
||||||
|
if result == 1:
|
||||||
|
print u"{0} v{1}: {2} is not a secure Adobe Adept ePub.".format(PLUGIN_NAME, PLUGIN_VERSION,os.path.basename(path_to_ebook))
|
||||||
|
of.close()
|
||||||
|
return path_to_ebook
|
||||||
|
break
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
# Import the original unmolested epub.
|
||||||
|
raise ADEPTError(u"{0} v{1}: Ultimately failed to decrypt".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||||
|
return
|
||||||
|
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
#! /usr/bin/python
|
#! /usr/bin/python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# ineptepub.pyw, version 5.2
|
from __future__ import with_statement
|
||||||
# Copyright © 2009-2010 i♥cabbages
|
|
||||||
|
|
||||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
# ineptepub.pyw, version 5.8
|
||||||
# later. <http://www.gnu.org/licenses/>
|
# Copyright © 2009-2010 by i♥cabbages
|
||||||
|
|
||||||
|
# Released under the terms of the GNU General Public Licence, version 3
|
||||||
|
# <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
|
# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||||
|
|
||||||
# Windows users: Before running this program, you must first install Python 2.6
|
# Windows users: Before running this program, you must first install Python 2.6
|
||||||
# from <http://www.python.org/download/> and PyCrypto from
|
# from <http://www.python.org/download/> and PyCrypto from
|
||||||
@@ -25,25 +29,87 @@
|
|||||||
# 5.1 - Improve OpenSSL error checking
|
# 5.1 - Improve OpenSSL error checking
|
||||||
# 5.2 - Fix ctypes error causing segfaults on some systems
|
# 5.2 - Fix ctypes error causing segfaults on some systems
|
||||||
# 5.3 - add support for OpenSSL on Windows, fix bug with some versions of libcrypto 0.9.8 prior to path level o
|
# 5.3 - add support for OpenSSL on Windows, fix bug with some versions of libcrypto 0.9.8 prior to path level o
|
||||||
"""
|
# 5.4 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml
|
||||||
Decrypt Adobe ADEPT-encrypted EPUB books.
|
# 5.5 - On Windows try PyCrypto first, OpenSSL next
|
||||||
"""
|
# 5.6 - Modify interface to allow use with import
|
||||||
|
# 5.7 - Fix for potential problem with PyCrypto
|
||||||
|
# 5.8 - Revised to allow use in calibre plugins to eliminate need for duplicate code
|
||||||
|
|
||||||
from __future__ import with_statement
|
"""
|
||||||
|
Decrypt Adobe Digital Editions encrypted ePub books.
|
||||||
|
"""
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
|
__version__ = "5.8"
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import traceback
|
||||||
import zlib
|
import zlib
|
||||||
import zipfile
|
import zipfile
|
||||||
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
import xml.etree.ElementTree as etree
|
import xml.etree.ElementTree as etree
|
||||||
import Tkinter
|
|
||||||
import Tkconstants
|
# Wrap a stream so that output gets flushed immediately
|
||||||
import tkFileDialog
|
# and also make sure that any unicode strings get
|
||||||
import tkMessageBox
|
# encoded using "replace" before writing them.
|
||||||
|
class SafeUnbuffered:
|
||||||
|
def __init__(self, stream):
|
||||||
|
self.stream = stream
|
||||||
|
self.encoding = stream.encoding
|
||||||
|
if self.encoding == None:
|
||||||
|
self.encoding = "utf-8"
|
||||||
|
def write(self, data):
|
||||||
|
if isinstance(data,unicode):
|
||||||
|
data = data.encode(self.encoding,"replace")
|
||||||
|
self.stream.write(data)
|
||||||
|
self.stream.flush()
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return getattr(self.stream, attr)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from calibre.constants import iswindows, isosx
|
||||||
|
except:
|
||||||
|
iswindows = sys.platform.startswith('win')
|
||||||
|
isosx = sys.platform.startswith('darwin')
|
||||||
|
|
||||||
|
def unicode_argv():
|
||||||
|
if iswindows:
|
||||||
|
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||||
|
# strings.
|
||||||
|
|
||||||
|
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||||
|
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||||
|
# characters with '?'.
|
||||||
|
|
||||||
|
|
||||||
|
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||||
|
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||||
|
|
||||||
|
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||||
|
GetCommandLineW.argtypes = []
|
||||||
|
GetCommandLineW.restype = LPCWSTR
|
||||||
|
|
||||||
|
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||||
|
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||||
|
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||||
|
|
||||||
|
cmd = GetCommandLineW()
|
||||||
|
argc = c_int(0)
|
||||||
|
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||||
|
if argc.value > 0:
|
||||||
|
# Remove Python executable and commands if present
|
||||||
|
start = argc.value - len(sys.argv)
|
||||||
|
return [argv[i] for i in
|
||||||
|
xrange(start, argc.value)]
|
||||||
|
return [u"ineptepub.py"]
|
||||||
|
else:
|
||||||
|
argvencoding = sys.stdin.encoding
|
||||||
|
if argvencoding == None:
|
||||||
|
argvencoding = "utf-8"
|
||||||
|
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||||
|
|
||||||
|
|
||||||
class ADEPTError(Exception):
|
class ADEPTError(Exception):
|
||||||
pass
|
pass
|
||||||
@@ -53,7 +119,7 @@ def _load_crypto_libcrypto():
|
|||||||
Structure, c_ulong, create_string_buffer, cast
|
Structure, c_ulong, create_string_buffer, cast
|
||||||
from ctypes.util import find_library
|
from ctypes.util import find_library
|
||||||
|
|
||||||
if sys.platform.startswith('win'):
|
if iswindows:
|
||||||
libcrypto = find_library('libeay32')
|
libcrypto = find_library('libeay32')
|
||||||
else:
|
else:
|
||||||
libcrypto = find_library('crypto')
|
libcrypto = find_library('crypto')
|
||||||
@@ -232,7 +298,7 @@ def _load_crypto_pycrypto():
|
|||||||
|
|
||||||
class AES(object):
|
class AES(object):
|
||||||
def __init__(self, key):
|
def __init__(self, key):
|
||||||
self._aes = _AES.new(key, _AES.MODE_CBC)
|
self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16)
|
||||||
|
|
||||||
def decrypt(self, data):
|
def decrypt(self, data):
|
||||||
return self._aes.decrypt(data)
|
return self._aes.decrypt(data)
|
||||||
@@ -257,13 +323,17 @@ def _load_crypto_pycrypto():
|
|||||||
|
|
||||||
def _load_crypto():
|
def _load_crypto():
|
||||||
AES = RSA = None
|
AES = RSA = None
|
||||||
for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto):
|
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
||||||
|
for loader in cryptolist:
|
||||||
try:
|
try:
|
||||||
AES, RSA = loader()
|
AES, RSA = loader()
|
||||||
break
|
break
|
||||||
except (ImportError, ADEPTError):
|
except (ImportError, ADEPTError):
|
||||||
pass
|
pass
|
||||||
return (AES, RSA)
|
return (AES, RSA)
|
||||||
|
|
||||||
AES, RSA = _load_crypto()
|
AES, RSA = _load_crypto()
|
||||||
|
|
||||||
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
|
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
|
||||||
@@ -288,6 +358,7 @@ class Decryptor(object):
|
|||||||
for elem in encryption.findall(expr):
|
for elem in encryption.findall(expr):
|
||||||
path = elem.get('URI', None)
|
path = elem.get('URI', None)
|
||||||
if path is not None:
|
if path is not None:
|
||||||
|
path = path.encode('utf-8')
|
||||||
encrypted.add(path)
|
encrypted.add(path)
|
||||||
|
|
||||||
def decompress(self, bytes):
|
def decompress(self, bytes):
|
||||||
@@ -305,35 +376,50 @@ class Decryptor(object):
|
|||||||
data = self.decompress(data)
|
data = self.decompress(data)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def cli_main(argv=sys.argv):
|
# check file to make check whether it's probably an Adobe Adept encrypted ePub
|
||||||
progname = os.path.basename(argv[0])
|
def adeptBook(inpath):
|
||||||
if AES is None:
|
|
||||||
print "%s: This script requires OpenSSL or PyCrypto, which must be" \
|
|
||||||
" installed separately. Read the top-of-script comment for" \
|
|
||||||
" details." % (progname,)
|
|
||||||
return 1
|
|
||||||
if len(argv) != 4:
|
|
||||||
print "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,)
|
|
||||||
return 1
|
|
||||||
keypath, inpath, outpath = argv[1:]
|
|
||||||
with open(keypath, 'rb') as f:
|
|
||||||
keyder = f.read()
|
|
||||||
rsa = RSA(keyder)
|
|
||||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||||
namelist = set(inf.namelist())
|
namelist = set(inf.namelist())
|
||||||
if 'META-INF/rights.xml' not in namelist or \
|
if 'META-INF/rights.xml' not in namelist or \
|
||||||
'META-INF/encryption.xml' not in namelist:
|
'META-INF/encryption.xml' not in namelist:
|
||||||
raise ADEPTError('%s: not an ADEPT EPUB' % (inpath,))
|
return False
|
||||||
for name in META_NAMES:
|
try:
|
||||||
namelist.remove(name)
|
|
||||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||||
expr = './/%s' % (adept('encryptedKey'),)
|
expr = './/%s' % (adept('encryptedKey'),)
|
||||||
bookkey = ''.join(rights.findtext(expr))
|
bookkey = ''.join(rights.findtext(expr))
|
||||||
|
if len(bookkey) == 172:
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
# if we couldn't check, assume it is
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def decryptBook(userkey, inpath, outpath):
|
||||||
|
if AES is None:
|
||||||
|
raise ADEPTError(u"PyCrypto or OpenSSL must be installed.")
|
||||||
|
rsa = RSA(userkey)
|
||||||
|
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||||
|
namelist = set(inf.namelist())
|
||||||
|
if 'META-INF/rights.xml' not in namelist or \
|
||||||
|
'META-INF/encryption.xml' not in namelist:
|
||||||
|
print u"{0:s} is DRM-free.".format(os.path.basename(inpath))
|
||||||
|
return 1
|
||||||
|
for name in META_NAMES:
|
||||||
|
namelist.remove(name)
|
||||||
|
try:
|
||||||
|
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||||
|
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||||
|
expr = './/%s' % (adept('encryptedKey'),)
|
||||||
|
bookkey = ''.join(rights.findtext(expr))
|
||||||
|
if len(bookkey) != 172:
|
||||||
|
print u"{0:s} is not a secure Adobe Adept ePub.".format(os.path.basename(inpath))
|
||||||
|
return 1
|
||||||
bookkey = rsa.decrypt(bookkey.decode('base64'))
|
bookkey = rsa.decrypt(bookkey.decode('base64'))
|
||||||
# Padded as per RSAES-PKCS1-v1_5
|
# Padded as per RSAES-PKCS1-v1_5
|
||||||
if bookkey[-17] != '\x00':
|
if bookkey[-17] != '\x00':
|
||||||
raise ADEPTError('problem decrypting session key')
|
print u"Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath))
|
||||||
|
return 2
|
||||||
encryption = inf.read('META-INF/encryption.xml')
|
encryption = inf.read('META-INF/encryption.xml')
|
||||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||||
@@ -343,48 +429,71 @@ def cli_main(argv=sys.argv):
|
|||||||
for path in namelist:
|
for path in namelist:
|
||||||
data = inf.read(path)
|
data = inf.read(path)
|
||||||
outf.writestr(path, decryptor.decrypt(path, data))
|
outf.writestr(path, decryptor.decrypt(path, data))
|
||||||
|
except:
|
||||||
|
print u"Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc())
|
||||||
|
return 2
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cli_main(argv=unicode_argv()):
|
||||||
|
progname = os.path.basename(argv[0])
|
||||||
|
if len(argv) != 4:
|
||||||
|
print u"usage: {0} <keyfile.der> <inbook.epub> <outbook.epub>".format(progname)
|
||||||
|
return 1
|
||||||
|
keypath, inpath, outpath = argv[1:]
|
||||||
|
userkey = open(keypath,'rb').read()
|
||||||
|
result = decryptBook(userkey, inpath, outpath)
|
||||||
|
if result == 0:
|
||||||
|
print u"Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def gui_main():
|
||||||
|
import Tkinter
|
||||||
|
import Tkconstants
|
||||||
|
import tkFileDialog
|
||||||
|
import traceback
|
||||||
|
|
||||||
class DecryptionDialog(Tkinter.Frame):
|
class DecryptionDialog(Tkinter.Frame):
|
||||||
def __init__(self, root):
|
def __init__(self, root):
|
||||||
Tkinter.Frame.__init__(self, root, border=5)
|
Tkinter.Frame.__init__(self, root, border=5)
|
||||||
self.status = Tkinter.Label(self, text='Select files for decryption')
|
self.status = Tkinter.Label(self, text=u"Select files for decryption")
|
||||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||||
body = Tkinter.Frame(self)
|
body = Tkinter.Frame(self)
|
||||||
body.pack(fill=Tkconstants.X, expand=1)
|
body.pack(fill=Tkconstants.X, expand=1)
|
||||||
sticky = Tkconstants.E + Tkconstants.W
|
sticky = Tkconstants.E + Tkconstants.W
|
||||||
body.grid_columnconfigure(1, weight=2)
|
body.grid_columnconfigure(1, weight=2)
|
||||||
Tkinter.Label(body, text='Key file').grid(row=0)
|
Tkinter.Label(body, text=u"Key file").grid(row=0)
|
||||||
self.keypath = Tkinter.Entry(body, width=30)
|
self.keypath = Tkinter.Entry(body, width=30)
|
||||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||||
if os.path.exists('adeptkey.der'):
|
if os.path.exists(u"adeptkey.der"):
|
||||||
self.keypath.insert(0, 'adeptkey.der')
|
self.keypath.insert(0, u"adeptkey.der")
|
||||||
button = Tkinter.Button(body, text="...", command=self.get_keypath)
|
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
|
||||||
button.grid(row=0, column=2)
|
button.grid(row=0, column=2)
|
||||||
Tkinter.Label(body, text='Input file').grid(row=1)
|
Tkinter.Label(body, text=u"Input file").grid(row=1)
|
||||||
self.inpath = Tkinter.Entry(body, width=30)
|
self.inpath = Tkinter.Entry(body, width=30)
|
||||||
self.inpath.grid(row=1, column=1, sticky=sticky)
|
self.inpath.grid(row=1, column=1, sticky=sticky)
|
||||||
button = Tkinter.Button(body, text="...", command=self.get_inpath)
|
button = Tkinter.Button(body, text=u"...", command=self.get_inpath)
|
||||||
button.grid(row=1, column=2)
|
button.grid(row=1, column=2)
|
||||||
Tkinter.Label(body, text='Output file').grid(row=2)
|
Tkinter.Label(body, text=u"Output file").grid(row=2)
|
||||||
self.outpath = Tkinter.Entry(body, width=30)
|
self.outpath = Tkinter.Entry(body, width=30)
|
||||||
self.outpath.grid(row=2, column=1, sticky=sticky)
|
self.outpath.grid(row=2, column=1, sticky=sticky)
|
||||||
button = Tkinter.Button(body, text="...", command=self.get_outpath)
|
button = Tkinter.Button(body, text=u"...", command=self.get_outpath)
|
||||||
button.grid(row=2, column=2)
|
button.grid(row=2, column=2)
|
||||||
buttons = Tkinter.Frame(self)
|
buttons = Tkinter.Frame(self)
|
||||||
buttons.pack()
|
buttons.pack()
|
||||||
botton = Tkinter.Button(
|
botton = Tkinter.Button(
|
||||||
buttons, text="Decrypt", width=10, command=self.decrypt)
|
buttons, text=u"Decrypt", width=10, command=self.decrypt)
|
||||||
botton.pack(side=Tkconstants.LEFT)
|
botton.pack(side=Tkconstants.LEFT)
|
||||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||||
button = Tkinter.Button(
|
button = Tkinter.Button(
|
||||||
buttons, text="Quit", width=10, command=self.quit)
|
buttons, text=u"Quit", width=10, command=self.quit)
|
||||||
button.pack(side=Tkconstants.RIGHT)
|
button.pack(side=Tkconstants.RIGHT)
|
||||||
|
|
||||||
def get_keypath(self):
|
def get_keypath(self):
|
||||||
keypath = tkFileDialog.askopenfilename(
|
keypath = tkFileDialog.askopenfilename(
|
||||||
parent=None, title='Select ADEPT key file',
|
parent=None, title=u"Select Adobe Adept \'.der\' key file",
|
||||||
defaultextension='.der', filetypes=[('DER-encoded files', '.der'),
|
defaultextension=u".der",
|
||||||
|
filetypes=[('Adobe Adept DER-encoded files', '.der'),
|
||||||
('All Files', '.*')])
|
('All Files', '.*')])
|
||||||
if keypath:
|
if keypath:
|
||||||
keypath = os.path.normpath(keypath)
|
keypath = os.path.normpath(keypath)
|
||||||
@@ -394,9 +503,8 @@ class DecryptionDialog(Tkinter.Frame):
|
|||||||
|
|
||||||
def get_inpath(self):
|
def get_inpath(self):
|
||||||
inpath = tkFileDialog.askopenfilename(
|
inpath = tkFileDialog.askopenfilename(
|
||||||
parent=None, title='Select ADEPT-encrypted EPUB file to decrypt',
|
parent=None, title=u"Select ADEPT-encrypted ePub file to decrypt",
|
||||||
defaultextension='.epub', filetypes=[('EPUB files', '.epub'),
|
defaultextension=u".epub", filetypes=[('ePub files', '.epub')])
|
||||||
('All files', '.*')])
|
|
||||||
if inpath:
|
if inpath:
|
||||||
inpath = os.path.normpath(inpath)
|
inpath = os.path.normpath(inpath)
|
||||||
self.inpath.delete(0, Tkconstants.END)
|
self.inpath.delete(0, Tkconstants.END)
|
||||||
@@ -405,9 +513,8 @@ class DecryptionDialog(Tkinter.Frame):
|
|||||||
|
|
||||||
def get_outpath(self):
|
def get_outpath(self):
|
||||||
outpath = tkFileDialog.asksaveasfilename(
|
outpath = tkFileDialog.asksaveasfilename(
|
||||||
parent=None, title='Select unencrypted EPUB file to produce',
|
parent=None, title=u"Select unencrypted ePub file to produce",
|
||||||
defaultextension='.epub', filetypes=[('EPUB files', '.epub'),
|
defaultextension=u".epub", filetypes=[('ePub files', '.epub')])
|
||||||
('All files', '.*')])
|
|
||||||
if outpath:
|
if outpath:
|
||||||
outpath = os.path.normpath(outpath)
|
outpath = os.path.normpath(outpath)
|
||||||
self.outpath.delete(0, Tkconstants.END)
|
self.outpath.delete(0, Tkconstants.END)
|
||||||
@@ -419,37 +526,31 @@ class DecryptionDialog(Tkinter.Frame):
|
|||||||
inpath = self.inpath.get()
|
inpath = self.inpath.get()
|
||||||
outpath = self.outpath.get()
|
outpath = self.outpath.get()
|
||||||
if not keypath or not os.path.exists(keypath):
|
if not keypath or not os.path.exists(keypath):
|
||||||
self.status['text'] = 'Specified key file does not exist'
|
self.status['text'] = u"Specified key file does not exist"
|
||||||
return
|
return
|
||||||
if not inpath or not os.path.exists(inpath):
|
if not inpath or not os.path.exists(inpath):
|
||||||
self.status['text'] = 'Specified input file does not exist'
|
self.status['text'] = u"Specified input file does not exist"
|
||||||
return
|
return
|
||||||
if not outpath:
|
if not outpath:
|
||||||
self.status['text'] = 'Output file not specified'
|
self.status['text'] = u"Output file not specified"
|
||||||
return
|
return
|
||||||
if inpath == outpath:
|
if inpath == outpath:
|
||||||
self.status['text'] = 'Must have different input and output files'
|
self.status['text'] = u"Must have different input and output files"
|
||||||
return
|
return
|
||||||
argv = [sys.argv[0], keypath, inpath, outpath]
|
userkey = open(keypath,'rb').read()
|
||||||
self.status['text'] = 'Decrypting...'
|
self.status['text'] = u"Decrypting..."
|
||||||
try:
|
try:
|
||||||
cli_main(argv)
|
decrypt_status = decryptBook(userkey, inpath, outpath)
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
self.status['text'] = 'Error: ' + str(e)
|
self.status['text'] = u"Error; {0}".format(e)
|
||||||
return
|
return
|
||||||
self.status['text'] = 'File successfully decrypted'
|
if decrypt_status == 0:
|
||||||
|
self.status['text'] = u"File successfully decrypted"
|
||||||
|
else:
|
||||||
|
self.status['text'] = u"The was an error decrypting the file."
|
||||||
|
|
||||||
def gui_main():
|
|
||||||
root = Tkinter.Tk()
|
root = Tkinter.Tk()
|
||||||
if AES is None:
|
root.title(u"Adobe Adept ePub Decrypter v.{0}".format(__version__))
|
||||||
root.withdraw()
|
|
||||||
tkMessageBox.showerror(
|
|
||||||
"INEPT EPUB Decrypter",
|
|
||||||
"This script requires OpenSSL or PyCrypto, which must be"
|
|
||||||
" installed separately. Read the top-of-script comment for"
|
|
||||||
" details.")
|
|
||||||
return 1
|
|
||||||
root.title('INEPT EPUB Decrypter')
|
|
||||||
root.resizable(True, False)
|
root.resizable(True, False)
|
||||||
root.minsize(300, 0)
|
root.minsize(300, 0)
|
||||||
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||||
@@ -458,5 +559,7 @@ def gui_main():
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
|
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||||
|
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||||
sys.exit(cli_main())
|
sys.exit(cli_main())
|
||||||
sys.exit(gui_main())
|
sys.exit(gui_main())
|
||||||
@@ -1,481 +0,0 @@
|
|||||||
#! /usr/bin/python
|
|
||||||
|
|
||||||
# ineptepub_plugin.py
|
|
||||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
|
||||||
# later. <http://www.gnu.org/licenses/>
|
|
||||||
#
|
|
||||||
# Requires Calibre version 0.6.44 or higher.
|
|
||||||
#
|
|
||||||
# All credit given to I <3 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 an 'adeptkey.der' file and
|
|
||||||
# save it in Calibre's configuration directory. It will use that file on subsequent runs.
|
|
||||||
# If there are already '*.der' files 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 <3 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 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.
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
Decrypt Adobe ADEPT-encrypted EPUB books.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import zlib
|
|
||||||
import zipfile
|
|
||||||
import re
|
|
||||||
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
|
||||||
from contextlib import closing
|
|
||||||
import xml.etree.ElementTree as etree
|
|
||||||
|
|
||||||
global AES
|
|
||||||
global RSA
|
|
||||||
|
|
||||||
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
|
|
||||||
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
|
||||||
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
|
||||||
|
|
||||||
|
|
||||||
class ADEPTError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _load_crypto_libcrypto():
|
|
||||||
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
|
||||||
Structure, c_ulong, create_string_buffer, cast
|
|
||||||
from ctypes.util import find_library
|
|
||||||
|
|
||||||
if sys.platform.startswith('win'):
|
|
||||||
libcrypto = find_library('libeay32')
|
|
||||||
else:
|
|
||||||
libcrypto = find_library('crypto')
|
|
||||||
if libcrypto is None:
|
|
||||||
raise ADEPTError('libcrypto not found')
|
|
||||||
libcrypto = CDLL(libcrypto)
|
|
||||||
|
|
||||||
RSA_NO_PADDING = 3
|
|
||||||
AES_MAXNR = 14
|
|
||||||
|
|
||||||
c_char_pp = POINTER(c_char_p)
|
|
||||||
c_int_p = POINTER(c_int)
|
|
||||||
|
|
||||||
class RSA(Structure):
|
|
||||||
pass
|
|
||||||
RSA_p = POINTER(RSA)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
d2i_RSAPrivateKey = F(RSA_p, 'd2i_RSAPrivateKey',
|
|
||||||
[RSA_p, c_char_pp, c_long])
|
|
||||||
RSA_size = F(c_int, 'RSA_size', [RSA_p])
|
|
||||||
RSA_private_decrypt = F(c_int, 'RSA_private_decrypt',
|
|
||||||
[c_int, c_char_p, c_char_p, RSA_p, c_int])
|
|
||||||
RSA_free = F(None, 'RSA_free', [RSA_p])
|
|
||||||
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
|
|
||||||
[c_char_p, c_int, AES_KEY_p])
|
|
||||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
|
||||||
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
|
||||||
c_int])
|
|
||||||
|
|
||||||
class RSA(object):
|
|
||||||
def __init__(self, der):
|
|
||||||
buf = create_string_buffer(der)
|
|
||||||
pp = c_char_pp(cast(buf, c_char_p))
|
|
||||||
rsa = self._rsa = d2i_RSAPrivateKey(None, pp, len(der))
|
|
||||||
if rsa is None:
|
|
||||||
raise ADEPTError('Error parsing ADEPT user key DER')
|
|
||||||
|
|
||||||
def decrypt(self, from_):
|
|
||||||
rsa = self._rsa
|
|
||||||
to = create_string_buffer(RSA_size(rsa))
|
|
||||||
dlen = RSA_private_decrypt(len(from_), from_, to, rsa,
|
|
||||||
RSA_NO_PADDING)
|
|
||||||
if dlen < 0:
|
|
||||||
raise ADEPTError('RSA decryption failed')
|
|
||||||
return to[:dlen]
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
if self._rsa is not None:
|
|
||||||
RSA_free(self._rsa)
|
|
||||||
self._rsa = None
|
|
||||||
|
|
||||||
class AES(object):
|
|
||||||
def __init__(self, userkey):
|
|
||||||
self._blocksize = len(userkey)
|
|
||||||
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
|
||||||
raise ADEPTError('AES improper key used')
|
|
||||||
return
|
|
||||||
key = self._key = AES_KEY()
|
|
||||||
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
|
|
||||||
if rv < 0:
|
|
||||||
raise ADEPTError('Failed to initialize AES key')
|
|
||||||
|
|
||||||
def decrypt(self, data):
|
|
||||||
out = create_string_buffer(len(data))
|
|
||||||
iv = ("\x00" * self._blocksize)
|
|
||||||
rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
|
|
||||||
if rv == 0:
|
|
||||||
raise ADEPTError('AES decryption failed')
|
|
||||||
return out.raw
|
|
||||||
print 'IneptEpub: Using libcrypto.'
|
|
||||||
return (AES, RSA)
|
|
||||||
|
|
||||||
def _load_crypto_pycrypto():
|
|
||||||
from Crypto.Cipher import AES as _AES
|
|
||||||
from Crypto.PublicKey import RSA as _RSA
|
|
||||||
|
|
||||||
# ASN.1 parsing code from tlslite
|
|
||||||
class ASN1Error(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ASN1Parser(object):
|
|
||||||
class Parser(object):
|
|
||||||
def __init__(self, bytes):
|
|
||||||
self.bytes = bytes
|
|
||||||
self.index = 0
|
|
||||||
|
|
||||||
def get(self, length):
|
|
||||||
if self.index + length > len(self.bytes):
|
|
||||||
raise ASN1Error("Error decoding ASN.1")
|
|
||||||
x = 0
|
|
||||||
for count in range(length):
|
|
||||||
x <<= 8
|
|
||||||
x |= self.bytes[self.index]
|
|
||||||
self.index += 1
|
|
||||||
return x
|
|
||||||
|
|
||||||
def getFixBytes(self, lengthBytes):
|
|
||||||
bytes = self.bytes[self.index : self.index+lengthBytes]
|
|
||||||
self.index += lengthBytes
|
|
||||||
return bytes
|
|
||||||
|
|
||||||
def getVarBytes(self, lengthLength):
|
|
||||||
lengthBytes = self.get(lengthLength)
|
|
||||||
return self.getFixBytes(lengthBytes)
|
|
||||||
|
|
||||||
def getFixList(self, length, lengthList):
|
|
||||||
l = [0] * lengthList
|
|
||||||
for x in range(lengthList):
|
|
||||||
l[x] = self.get(length)
|
|
||||||
return l
|
|
||||||
|
|
||||||
def getVarList(self, length, lengthLength):
|
|
||||||
lengthList = self.get(lengthLength)
|
|
||||||
if lengthList % length != 0:
|
|
||||||
raise ASN1Error("Error decoding ASN.1")
|
|
||||||
lengthList = int(lengthList/length)
|
|
||||||
l = [0] * lengthList
|
|
||||||
for x in range(lengthList):
|
|
||||||
l[x] = self.get(length)
|
|
||||||
return l
|
|
||||||
|
|
||||||
def startLengthCheck(self, lengthLength):
|
|
||||||
self.lengthCheck = self.get(lengthLength)
|
|
||||||
self.indexCheck = self.index
|
|
||||||
|
|
||||||
def setLengthCheck(self, length):
|
|
||||||
self.lengthCheck = length
|
|
||||||
self.indexCheck = self.index
|
|
||||||
|
|
||||||
def stopLengthCheck(self):
|
|
||||||
if (self.index - self.indexCheck) != self.lengthCheck:
|
|
||||||
raise ASN1Error("Error decoding ASN.1")
|
|
||||||
|
|
||||||
def atLengthCheck(self):
|
|
||||||
if (self.index - self.indexCheck) < self.lengthCheck:
|
|
||||||
return False
|
|
||||||
elif (self.index - self.indexCheck) == self.lengthCheck:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
raise ASN1Error("Error decoding ASN.1")
|
|
||||||
|
|
||||||
def __init__(self, bytes):
|
|
||||||
p = self.Parser(bytes)
|
|
||||||
p.get(1)
|
|
||||||
self.length = self._getASN1Length(p)
|
|
||||||
self.value = p.getFixBytes(self.length)
|
|
||||||
|
|
||||||
def getChild(self, which):
|
|
||||||
p = self.Parser(self.value)
|
|
||||||
for x in range(which+1):
|
|
||||||
markIndex = p.index
|
|
||||||
p.get(1)
|
|
||||||
length = self._getASN1Length(p)
|
|
||||||
p.getFixBytes(length)
|
|
||||||
return ASN1Parser(p.bytes[markIndex:p.index])
|
|
||||||
|
|
||||||
def _getASN1Length(self, p):
|
|
||||||
firstLength = p.get(1)
|
|
||||||
if firstLength<=127:
|
|
||||||
return firstLength
|
|
||||||
else:
|
|
||||||
lengthLength = firstLength & 0x7F
|
|
||||||
return p.get(lengthLength)
|
|
||||||
|
|
||||||
class AES(object):
|
|
||||||
def __init__(self, key):
|
|
||||||
self._aes = _AES.new(key, _AES.MODE_CBC)
|
|
||||||
|
|
||||||
def decrypt(self, data):
|
|
||||||
return self._aes.decrypt(data)
|
|
||||||
|
|
||||||
class RSA(object):
|
|
||||||
def __init__(self, der):
|
|
||||||
key = ASN1Parser([ord(x) for x in der])
|
|
||||||
key = [key.getChild(x).value for x in xrange(1, 4)]
|
|
||||||
key = [self.bytesToNumber(v) for v in key]
|
|
||||||
self._rsa = _RSA.construct(key)
|
|
||||||
|
|
||||||
def bytesToNumber(self, bytes):
|
|
||||||
total = 0L
|
|
||||||
for byte in bytes:
|
|
||||||
total = (total << 8) + byte
|
|
||||||
return total
|
|
||||||
|
|
||||||
def decrypt(self, data):
|
|
||||||
return self._rsa.decrypt(data)
|
|
||||||
print 'IneptEpub: Using pycrypto.'
|
|
||||||
return (AES, RSA)
|
|
||||||
|
|
||||||
def _load_crypto():
|
|
||||||
_aes = _rsa = None
|
|
||||||
for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto):
|
|
||||||
try:
|
|
||||||
_aes, _rsa = loader()
|
|
||||||
break
|
|
||||||
except (ImportError, ADEPTError):
|
|
||||||
pass
|
|
||||||
return (_aes, _rsa)
|
|
||||||
|
|
||||||
class ZipInfo(zipfile.ZipInfo):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
if 'compress_type' in kwargs:
|
|
||||||
compress_type = kwargs.pop('compress_type')
|
|
||||||
super(ZipInfo, self).__init__(*args, **kwargs)
|
|
||||||
self.compress_type = compress_type
|
|
||||||
|
|
||||||
class Decryptor(object):
|
|
||||||
def __init__(self, bookkey, encryption):
|
|
||||||
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
|
|
||||||
self._aes = AES(bookkey)
|
|
||||||
encryption = etree.fromstring(encryption)
|
|
||||||
self._encrypted = encrypted = set()
|
|
||||||
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
|
|
||||||
enc('CipherReference'))
|
|
||||||
for elem in encryption.findall(expr):
|
|
||||||
path = elem.get('URI', None)
|
|
||||||
if path is not None:
|
|
||||||
encrypted.add(path)
|
|
||||||
|
|
||||||
def decompress(self, bytes):
|
|
||||||
dc = zlib.decompressobj(-15)
|
|
||||||
bytes = dc.decompress(bytes)
|
|
||||||
ex = dc.decompress('Z') + dc.flush()
|
|
||||||
if ex:
|
|
||||||
bytes = bytes + ex
|
|
||||||
return bytes
|
|
||||||
|
|
||||||
def decrypt(self, path, data):
|
|
||||||
if path in self._encrypted:
|
|
||||||
data = self._aes.decrypt(data)[16:]
|
|
||||||
data = data[:-ord(data[-1])]
|
|
||||||
data = self.decompress(data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def plugin_main(userkey, inpath, outpath):
|
|
||||||
rsa = RSA(userkey)
|
|
||||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
|
||||||
namelist = set(inf.namelist())
|
|
||||||
if 'META-INF/rights.xml' not in namelist or \
|
|
||||||
'META-INF/encryption.xml' not in namelist:
|
|
||||||
return 1
|
|
||||||
for name in META_NAMES:
|
|
||||||
namelist.remove(name)
|
|
||||||
try:
|
|
||||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
|
||||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
|
||||||
expr = './/%s' % (adept('encryptedKey'),)
|
|
||||||
bookkey = ''.join(rights.findtext(expr))
|
|
||||||
bookkey = rsa.decrypt(bookkey.decode('base64'))
|
|
||||||
# Padded as per RSAES-PKCS1-v1_5
|
|
||||||
if bookkey[-17] != '\x00':
|
|
||||||
raise ADEPTError('problem decrypting session key')
|
|
||||||
encryption = inf.read('META-INF/encryption.xml')
|
|
||||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
|
||||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
|
||||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
|
||||||
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
|
||||||
outf.writestr(zi, inf.read('mimetype'))
|
|
||||||
for path in namelist:
|
|
||||||
data = inf.read(path)
|
|
||||||
outf.writestr(path, decryptor.decrypt(path, data))
|
|
||||||
except:
|
|
||||||
return 2
|
|
||||||
return 0
|
|
||||||
|
|
||||||
from calibre.customize import FileTypePlugin
|
|
||||||
|
|
||||||
class IneptDeDRM(FileTypePlugin):
|
|
||||||
name = 'Inept Epub DeDRM'
|
|
||||||
description = 'Removes DRM from secure Adobe epub files. \
|
|
||||||
Credit given to I <3 Cabbages for the original stand-alone scripts.'
|
|
||||||
supported_platforms = ['linux', 'osx', 'windows']
|
|
||||||
author = 'DiapDealer'
|
|
||||||
version = (0, 1, 1)
|
|
||||||
minimum_calibre_version = (0, 6, 44) # Compiled python libraries cannot be imported in earlier versions.
|
|
||||||
file_types = set(['epub'])
|
|
||||||
on_import = True
|
|
||||||
priority = 100
|
|
||||||
|
|
||||||
def run(self, path_to_ebook):
|
|
||||||
global AES
|
|
||||||
global RSA
|
|
||||||
|
|
||||||
from calibre.gui2 import is_ok_to_use_qt
|
|
||||||
from PyQt4.Qt import QMessageBox
|
|
||||||
from calibre.constants import iswindows, isosx
|
|
||||||
|
|
||||||
# Add the included pycrypto import directory for Windows users.
|
|
||||||
# Add the included Carbon import directory for Mac users.
|
|
||||||
pdir = 'windows' if iswindows else 'osx' if isosx else 'linux'
|
|
||||||
ppath = os.path.join(self.sys_insertion_path, pdir)
|
|
||||||
sys.path.append(ppath)
|
|
||||||
|
|
||||||
AES, RSA = _load_crypto()
|
|
||||||
|
|
||||||
if AES == None or RSA == None:
|
|
||||||
# Failed to load libcrypto or PyCrypto... Adobe Epubs can\'t be decrypted.'
|
|
||||||
sys.path.remove(ppath)
|
|
||||||
raise ADEPTError('IneptEpub: Failed to load crypto libs... Adobe Epubs can\'t be decrypted.')
|
|
||||||
return
|
|
||||||
|
|
||||||
# Load any keyfiles (*.der) included Calibre's config directory.
|
|
||||||
userkeys = []
|
|
||||||
|
|
||||||
# Find Calibre's configuration directory.
|
|
||||||
confpath = os.path.split(os.path.split(self.plugin_path)[0])[0]
|
|
||||||
print 'IneptEpub: Calibre configuration directory = %s' % confpath
|
|
||||||
files = os.listdir(confpath)
|
|
||||||
filefilter = re.compile("\.der$", re.IGNORECASE)
|
|
||||||
files = filter(filefilter.search, files)
|
|
||||||
|
|
||||||
if files:
|
|
||||||
try:
|
|
||||||
for filename in files:
|
|
||||||
fpath = os.path.join(confpath, filename)
|
|
||||||
with open(fpath, 'rb') as f:
|
|
||||||
userkeys.append(f.read())
|
|
||||||
print 'IneptEpub: Keyfile %s found in config folder.' % filename
|
|
||||||
except IOError:
|
|
||||||
print 'IneptEpub: Error reading keyfiles from config directory.'
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
# Try to find key from ADE install and save the key in
|
|
||||||
# Calibre's configuration directory for future use.
|
|
||||||
if iswindows or isosx:
|
|
||||||
# ADE key retrieval script included in respective OS folder.
|
|
||||||
from ade_key import retrieve_key
|
|
||||||
try:
|
|
||||||
keydata = retrieve_key()
|
|
||||||
userkeys.append(keydata)
|
|
||||||
keypath = os.path.join(confpath, 'adeptkey.der')
|
|
||||||
with open(keypath, 'wb') as f:
|
|
||||||
f.write(keydata)
|
|
||||||
print 'IneptEpub: Created keyfile from ADE install.'
|
|
||||||
except:
|
|
||||||
print 'IneptEpub: Couldn\'t Retrieve key from ADE install.'
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not userkeys:
|
|
||||||
# No user keys found... bail out.
|
|
||||||
sys.path.remove(ppath)
|
|
||||||
raise ADEPTError('IneptEpub - No keys found. Check keyfile(s)/ADE install')
|
|
||||||
return
|
|
||||||
|
|
||||||
# Attempt to decrypt epub with each encryption key found.
|
|
||||||
for userkey in userkeys:
|
|
||||||
# Create a TemporaryPersistent file to work with.
|
|
||||||
# Check original epub archive for zip errors.
|
|
||||||
import zipfix
|
|
||||||
inf = self.temporary_file('.epub')
|
|
||||||
try:
|
|
||||||
fr = zipfix.fixZip(path_to_ebook, inf.name)
|
|
||||||
fr.fix()
|
|
||||||
except Exception, e:
|
|
||||||
raise Exception(e)
|
|
||||||
return
|
|
||||||
of = self.temporary_file('.epub')
|
|
||||||
|
|
||||||
# Give the user key, ebook and TemporaryPersistent file to the plugin_main function.
|
|
||||||
result = plugin_main(userkey, inf.name, of.name)
|
|
||||||
|
|
||||||
# Ebook is not an Adobe Adept epub... do nothing and pass it on.
|
|
||||||
# This allows a non-encrypted epub to be imported without error messages.
|
|
||||||
if result == 1:
|
|
||||||
print 'IneptEpub: Not an Adobe Adept Epub... punting.'
|
|
||||||
of.close()
|
|
||||||
sys.path.remove(ppath)
|
|
||||||
return path_to_ebook
|
|
||||||
break
|
|
||||||
|
|
||||||
# Decryption was successful return the modified PersistentTemporary
|
|
||||||
# file to Calibre's import process.
|
|
||||||
if result == 0:
|
|
||||||
print 'IneptEpub: Encryption successfully removed.'
|
|
||||||
of.close
|
|
||||||
sys.path.remove(ppath)
|
|
||||||
return of.name
|
|
||||||
break
|
|
||||||
|
|
||||||
print 'IneptEpub: Encryption key invalid... trying others.'
|
|
||||||
of.close()
|
|
||||||
|
|
||||||
# Something went wrong with decryption.
|
|
||||||
# Import the original unmolested epub.
|
|
||||||
of.close
|
|
||||||
sys.path.remove(ppath)
|
|
||||||
raise ADEPTError('IneptEpub - Ultimately failed to decrypt')
|
|
||||||
return
|
|
||||||
|
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
#! /usr/bin/python
|
#! /usr/bin/python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# ineptkey.pyw, version 5
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
# ineptkey.pyw, version 5.6
|
||||||
# Copyright © 2009-2010 i♥cabbages
|
# Copyright © 2009-2010 i♥cabbages
|
||||||
|
|
||||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
# Released under the terms of the GNU General Public Licence, version 3
|
||||||
# later. <http://www.gnu.org/licenses/>
|
# <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
# Windows users: Before running this program, you must first install Python 2.6
|
# Windows users: Before running this program, you must first install Python 2.6
|
||||||
# from <http://www.python.org/download/> and PyCrypto from
|
# from <http://www.python.org/download/> and PyCrypto from
|
||||||
@@ -31,27 +33,85 @@
|
|||||||
# 5 - Clean up and improve 4.x changes;
|
# 5 - Clean up and improve 4.x changes;
|
||||||
# Clean up and merge OS X support by unknown
|
# Clean up and merge OS X support by unknown
|
||||||
# 5.1 - add support for using OpenSSL on Windows in place of PyCrypto
|
# 5.1 - add support for using OpenSSL on Windows in place of PyCrypto
|
||||||
|
# 5.2 - added support for output of key to a particular file
|
||||||
|
# 5.3 - On Windows try PyCrypto first, OpenSSL next
|
||||||
|
# 5.4 - Modify interface to allow use of import
|
||||||
|
# 5.5 - Fix for potential problem with PyCrypto
|
||||||
|
# 5.6 - Revised to allow use in Plugins to eliminate need for duplicate code
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Retrieve Adobe ADEPT user key.
|
Retrieve Adobe ADEPT user key.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import struct
|
import struct
|
||||||
import Tkinter
|
|
||||||
import Tkconstants
|
# Wrap a stream so that output gets flushed immediately
|
||||||
import tkMessageBox
|
# and also make sure that any unicode strings get
|
||||||
import traceback
|
# encoded using "replace" before writing them.
|
||||||
|
class SafeUnbuffered:
|
||||||
|
def __init__(self, stream):
|
||||||
|
self.stream = stream
|
||||||
|
self.encoding = stream.encoding
|
||||||
|
if self.encoding == None:
|
||||||
|
self.encoding = "utf-8"
|
||||||
|
def write(self, data):
|
||||||
|
if isinstance(data,unicode):
|
||||||
|
data = data.encode(self.encoding,"replace")
|
||||||
|
self.stream.write(data)
|
||||||
|
self.stream.flush()
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return getattr(self.stream, attr)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from calibre.constants import iswindows, isosx
|
||||||
|
except:
|
||||||
|
iswindows = sys.platform.startswith('win')
|
||||||
|
isosx = sys.platform.startswith('darwin')
|
||||||
|
|
||||||
|
def unicode_argv():
|
||||||
|
if iswindows:
|
||||||
|
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||||
|
# strings.
|
||||||
|
|
||||||
|
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||||
|
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||||
|
# characters with '?'.
|
||||||
|
|
||||||
|
|
||||||
|
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||||
|
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||||
|
|
||||||
|
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||||
|
GetCommandLineW.argtypes = []
|
||||||
|
GetCommandLineW.restype = LPCWSTR
|
||||||
|
|
||||||
|
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||||
|
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||||
|
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||||
|
|
||||||
|
cmd = GetCommandLineW()
|
||||||
|
argc = c_int(0)
|
||||||
|
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||||
|
if argc.value > 0:
|
||||||
|
# Remove Python executable and commands if present
|
||||||
|
start = argc.value - len(sys.argv)
|
||||||
|
return [argv[i] for i in
|
||||||
|
xrange(start, argc.value)]
|
||||||
|
return [u"ineptkey.py"]
|
||||||
|
else:
|
||||||
|
argvencoding = sys.stdin.encoding
|
||||||
|
if argvencoding == None:
|
||||||
|
argvencoding = "utf-8"
|
||||||
|
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||||
|
|
||||||
class ADEPTError(Exception):
|
class ADEPTError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if sys.platform.startswith('win'):
|
if iswindows:
|
||||||
from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
|
from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
|
||||||
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
|
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
|
||||||
string_at, Structure, c_void_p, cast, c_size_t, memmove, CDLL, c_int, \
|
string_at, Structure, c_void_p, cast, c_size_t, memmove, CDLL, c_int, \
|
||||||
@@ -107,14 +167,14 @@ if sys.platform.startswith('win'):
|
|||||||
from Crypto.Cipher import AES as _AES
|
from Crypto.Cipher import AES as _AES
|
||||||
class AES(object):
|
class AES(object):
|
||||||
def __init__(self, key):
|
def __init__(self, key):
|
||||||
self._aes = _AES.new(key, _AES.MODE_CBC)
|
self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16)
|
||||||
def decrypt(self, data):
|
def decrypt(self, data):
|
||||||
return self._aes.decrypt(data)
|
return self._aes.decrypt(data)
|
||||||
return AES
|
return AES
|
||||||
|
|
||||||
def _load_crypto():
|
def _load_crypto():
|
||||||
AES = None
|
AES = None
|
||||||
for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto):
|
for loader in (_load_crypto_pycrypto, _load_crypto_libcrypto):
|
||||||
try:
|
try:
|
||||||
AES = loader()
|
AES = loader()
|
||||||
break
|
break
|
||||||
@@ -289,13 +349,9 @@ if sys.platform.startswith('win'):
|
|||||||
return CryptUnprotectData
|
return CryptUnprotectData
|
||||||
CryptUnprotectData = CryptUnprotectData()
|
CryptUnprotectData = CryptUnprotectData()
|
||||||
|
|
||||||
def retrieve_key(keypath):
|
def retrieve_keys():
|
||||||
if AES is None:
|
if AES is None:
|
||||||
tkMessageBox.showerror(
|
raise ADEPTError("PyCrypto or OpenSSL must be installed")
|
||||||
"ADEPT Key",
|
|
||||||
"This script requires PyCrypto or OpenSSL which must be installed "
|
|
||||||
"separately. Read the top-of-script comment for details.")
|
|
||||||
return False
|
|
||||||
root = GetSystemDirectory().split('\\')[0] + '\\'
|
root = GetSystemDirectory().split('\\')[0] + '\\'
|
||||||
serial = GetVolumeSerialNumber(root)
|
serial = GetVolumeSerialNumber(root)
|
||||||
vendor = cpuid0()
|
vendor = cpuid0()
|
||||||
@@ -305,11 +361,12 @@ if sys.platform.startswith('win'):
|
|||||||
cuser = winreg.HKEY_CURRENT_USER
|
cuser = winreg.HKEY_CURRENT_USER
|
||||||
try:
|
try:
|
||||||
regkey = winreg.OpenKey(cuser, DEVICE_KEY_PATH)
|
regkey = winreg.OpenKey(cuser, DEVICE_KEY_PATH)
|
||||||
|
device = winreg.QueryValueEx(regkey, 'key')[0]
|
||||||
except WindowsError:
|
except WindowsError:
|
||||||
raise ADEPTError("Adobe Digital Editions not activated")
|
raise ADEPTError("Adobe Digital Editions not activated")
|
||||||
device = winreg.QueryValueEx(regkey, 'key')[0]
|
|
||||||
keykey = CryptUnprotectData(device, entropy)
|
keykey = CryptUnprotectData(device, entropy)
|
||||||
userkey = None
|
userkey = None
|
||||||
|
keys = []
|
||||||
try:
|
try:
|
||||||
plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH)
|
plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH)
|
||||||
except WindowsError:
|
except WindowsError:
|
||||||
@@ -331,50 +388,43 @@ if sys.platform.startswith('win'):
|
|||||||
if ktype != 'privateLicenseKey':
|
if ktype != 'privateLicenseKey':
|
||||||
continue
|
continue
|
||||||
userkey = winreg.QueryValueEx(plkkey, 'value')[0]
|
userkey = winreg.QueryValueEx(plkkey, 'value')[0]
|
||||||
break
|
|
||||||
if userkey is not None:
|
|
||||||
break
|
|
||||||
if userkey is None:
|
|
||||||
raise ADEPTError('Could not locate privateLicenseKey')
|
|
||||||
userkey = userkey.decode('base64')
|
userkey = userkey.decode('base64')
|
||||||
aes = AES(keykey)
|
aes = AES(keykey)
|
||||||
userkey = aes.decrypt(userkey)
|
userkey = aes.decrypt(userkey)
|
||||||
userkey = userkey[26:-ord(userkey[-1])]
|
userkey = userkey[26:-ord(userkey[-1])]
|
||||||
with open(keypath, 'wb') as f:
|
keys.append(userkey)
|
||||||
f.write(userkey)
|
if len(keys) == 0:
|
||||||
return True
|
raise ADEPTError('Could not locate privateLicenseKey')
|
||||||
|
return keys
|
||||||
|
|
||||||
elif sys.platform.startswith('darwin'):
|
|
||||||
|
elif isosx:
|
||||||
import xml.etree.ElementTree as etree
|
import xml.etree.ElementTree as etree
|
||||||
import Carbon.File
|
import subprocess
|
||||||
import Carbon.Folder
|
|
||||||
import Carbon.Folders
|
|
||||||
import MacOS
|
|
||||||
|
|
||||||
ACTIVATION_PATH = 'Adobe/Digital Editions/activation.dat'
|
|
||||||
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
||||||
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
||||||
|
|
||||||
def find_folder(domain, dtype):
|
def findActivationDat():
|
||||||
try:
|
home = os.getenv('HOME')
|
||||||
fsref = Carbon.Folder.FSFindFolder(domain, dtype, False)
|
cmdline = 'find "' + home + '/Library/Application Support/Adobe/Digital Editions" -name "activation.dat"'
|
||||||
return Carbon.File.pathname(fsref)
|
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||||
except MacOS.Error:
|
p2 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||||
|
out1, out2 = p2.communicate()
|
||||||
|
reslst = out1.split('\n')
|
||||||
|
cnt = len(reslst)
|
||||||
|
for j in xrange(cnt):
|
||||||
|
resline = reslst[j]
|
||||||
|
pp = resline.find('activation.dat')
|
||||||
|
if pp >= 0:
|
||||||
|
ActDatPath = resline
|
||||||
|
break
|
||||||
|
if os.path.exists(ActDatPath):
|
||||||
|
return ActDatPath
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def find_app_support_file(subpath):
|
def retrieve_keys():
|
||||||
dtype = Carbon.Folders.kApplicationSupportFolderType
|
actpath = findActivationDat()
|
||||||
for domain in Carbon.Folders.kUserDomain, Carbon.Folders.kLocalDomain:
|
|
||||||
path = find_folder(domain, dtype)
|
|
||||||
if path is None:
|
|
||||||
continue
|
|
||||||
path = os.path.join(path, subpath)
|
|
||||||
if os.path.isfile(path):
|
|
||||||
return path
|
|
||||||
return None
|
|
||||||
|
|
||||||
def retrieve_key(keypath):
|
|
||||||
actpath = find_app_support_file(ACTIVATION_PATH)
|
|
||||||
if actpath is None:
|
if actpath is None:
|
||||||
raise ADEPTError("Could not locate ADE activation")
|
raise ADEPTError("Could not locate ADE activation")
|
||||||
tree = etree.parse(actpath)
|
tree = etree.parse(actpath)
|
||||||
@@ -383,48 +433,65 @@ elif sys.platform.startswith('darwin'):
|
|||||||
userkey = tree.findtext(expr)
|
userkey = tree.findtext(expr)
|
||||||
userkey = userkey.decode('base64')
|
userkey = userkey.decode('base64')
|
||||||
userkey = userkey[26:]
|
userkey = userkey[26:]
|
||||||
with open(keypath, 'wb') as f:
|
return [userkey]
|
||||||
f.write(userkey)
|
|
||||||
return True
|
|
||||||
|
|
||||||
elif sys.platform.startswith('cygwin'):
|
|
||||||
def retrieve_key(keypath):
|
|
||||||
tkMessageBox.showerror(
|
|
||||||
"ADEPT Key",
|
|
||||||
"This script requires a Windows-native Python, and cannot be run "
|
|
||||||
"under Cygwin. Please install a Windows-native Python and/or "
|
|
||||||
"check your file associations.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
def retrieve_keys(keypath):
|
||||||
|
raise ADEPTError("This script only supports Windows and Mac OS X.")
|
||||||
|
return []
|
||||||
|
|
||||||
def retrieve_key(keypath):
|
def retrieve_key(keypath):
|
||||||
tkMessageBox.showerror(
|
keys = retrieve_keys()
|
||||||
"ADEPT Key",
|
with open(keypath, 'wb') as f:
|
||||||
"This script only supports Windows and Mac OS X. For Linux "
|
f.write(keys[0])
|
||||||
"you should be able to run ADE and this script under Wine (with "
|
return True
|
||||||
"an appropriate version of Windows Python installed).")
|
|
||||||
return False
|
def extractKeyfile(keypath):
|
||||||
|
try:
|
||||||
|
success = retrieve_key(keypath)
|
||||||
|
except ADEPTError, e:
|
||||||
|
print u"Key generation Error: {0}".format(e.args[0])
|
||||||
|
return 1
|
||||||
|
except Exception, e:
|
||||||
|
print "General Error: {0}".format(e.args[0])
|
||||||
|
return 1
|
||||||
|
if not success:
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cli_main(argv=unicode_argv()):
|
||||||
|
keypath = argv[1]
|
||||||
|
return extractKeyfile(keypath)
|
||||||
|
|
||||||
|
|
||||||
|
def gui_main(argv=unicode_argv()):
|
||||||
|
import Tkinter
|
||||||
|
import Tkconstants
|
||||||
|
import tkMessageBox
|
||||||
|
import traceback
|
||||||
|
|
||||||
class ExceptionDialog(Tkinter.Frame):
|
class ExceptionDialog(Tkinter.Frame):
|
||||||
def __init__(self, root, text):
|
def __init__(self, root, text):
|
||||||
Tkinter.Frame.__init__(self, root, border=5)
|
Tkinter.Frame.__init__(self, root, border=5)
|
||||||
label = Tkinter.Label(self, text="Unexpected error:",
|
label = Tkinter.Label(self, text=u"Unexpected error:",
|
||||||
anchor=Tkconstants.W, justify=Tkconstants.LEFT)
|
anchor=Tkconstants.W, justify=Tkconstants.LEFT)
|
||||||
label.pack(fill=Tkconstants.X, expand=0)
|
label.pack(fill=Tkconstants.X, expand=0)
|
||||||
self.text = Tkinter.Text(self)
|
self.text = Tkinter.Text(self)
|
||||||
self.text.pack(fill=Tkconstants.BOTH, expand=1)
|
self.text.pack(fill=Tkconstants.BOTH, expand=1)
|
||||||
|
|
||||||
self.text.insert(Tkconstants.END, text)
|
self.text.insert(Tkconstants.END, text)
|
||||||
|
|
||||||
def main(argv=sys.argv):
|
|
||||||
root = Tkinter.Tk()
|
root = Tkinter.Tk()
|
||||||
root.withdraw()
|
root.withdraw()
|
||||||
progname = os.path.basename(argv[0])
|
keypath, progname = os.path.split(argv[0])
|
||||||
keypath = 'adeptkey.der'
|
keypath = os.path.join(keypath, u"adeptkey.der")
|
||||||
success = False
|
success = False
|
||||||
try:
|
try:
|
||||||
success = retrieve_key(keypath)
|
success = retrieve_key(keypath)
|
||||||
except ADEPTError, e:
|
except ADEPTError, e:
|
||||||
tkMessageBox.showerror("ADEPT Key", "Error: " + str(e))
|
tkMessageBox.showerror(u"ADEPT Key", "Error: {0}".format(e.args[0]))
|
||||||
except Exception:
|
except Exception:
|
||||||
root.wm_state('normal')
|
root.wm_state('normal')
|
||||||
root.title('ADEPT Key')
|
root.title('ADEPT Key')
|
||||||
@@ -434,8 +501,12 @@ def main(argv=sys.argv):
|
|||||||
if not success:
|
if not success:
|
||||||
return 1
|
return 1
|
||||||
tkMessageBox.showinfo(
|
tkMessageBox.showinfo(
|
||||||
"ADEPT Key", "Key successfully retrieved to %s" % (keypath))
|
u"ADEPT Key", u"Key successfully retrieved to {0}".format(keypath))
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
sys.exit(main())
|
if len(sys.argv) > 1:
|
||||||
|
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||||
|
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||||
|
sys.exit(cli_main())
|
||||||
|
sys.exit(gui_main())
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,51 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# ===================================================================
|
|
||||||
# The contents of this file are dedicated to the public domain. To
|
|
||||||
# the extent that dedication to the public domain is not available,
|
|
||||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
|
||||||
# non-exclusive license to exercise all rights associated with the
|
|
||||||
# contents of this file for any purpose whatsoever.
|
|
||||||
# No rights are reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
# ===================================================================
|
|
||||||
|
|
||||||
"""Secret-key encryption algorithms.
|
|
||||||
|
|
||||||
Secret-key encryption algorithms transform plaintext in some way that
|
|
||||||
is dependent on a key, producing ciphertext. This transformation can
|
|
||||||
easily be reversed, if (and, hopefully, only if) one knows the key.
|
|
||||||
|
|
||||||
The encryption modules here all support the interface described in PEP
|
|
||||||
272, "API for Block Encryption Algorithms".
|
|
||||||
|
|
||||||
If you don't know which algorithm to choose, use AES because it's
|
|
||||||
standard and has undergone a fair bit of examination.
|
|
||||||
|
|
||||||
Crypto.Cipher.AES Advanced Encryption Standard
|
|
||||||
Crypto.Cipher.ARC2 Alleged RC2
|
|
||||||
Crypto.Cipher.ARC4 Alleged RC4
|
|
||||||
Crypto.Cipher.Blowfish
|
|
||||||
Crypto.Cipher.CAST
|
|
||||||
Crypto.Cipher.DES The Data Encryption Standard. Very commonly used
|
|
||||||
in the past, but today its 56-bit keys are too small.
|
|
||||||
Crypto.Cipher.DES3 Triple DES.
|
|
||||||
Crypto.Cipher.XOR The simple XOR cipher.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__all__ = ['AES', 'ARC2', 'ARC4',
|
|
||||||
'Blowfish', 'CAST', 'DES', 'DES3',
|
|
||||||
'XOR'
|
|
||||||
]
|
|
||||||
|
|
||||||
__revision__ = "$Id$"
|
|
||||||
|
|
||||||
|
|
||||||
Binary file not shown.
@@ -1,44 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# ===================================================================
|
|
||||||
# The contents of this file are dedicated to the public domain. To
|
|
||||||
# the extent that dedication to the public domain is not available,
|
|
||||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
|
||||||
# non-exclusive license to exercise all rights associated with the
|
|
||||||
# contents of this file for any purpose whatsoever.
|
|
||||||
# No rights are reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
# ===================================================================
|
|
||||||
|
|
||||||
"""Hashing algorithms
|
|
||||||
|
|
||||||
Hash functions take arbitrary strings as input, and produce an output
|
|
||||||
of fixed size that is dependent on the input; it should never be
|
|
||||||
possible to derive the input data given only the hash function's
|
|
||||||
output. Hash functions can be used simply as a checksum, or, in
|
|
||||||
association with a public-key algorithm, can be used to implement
|
|
||||||
digital signatures.
|
|
||||||
|
|
||||||
The hashing modules here all support the interface described in PEP
|
|
||||||
247, "API for Cryptographic Hash Functions".
|
|
||||||
|
|
||||||
Submodules:
|
|
||||||
Crypto.Hash.HMAC RFC 2104: Keyed-Hashing for Message Authentication
|
|
||||||
Crypto.Hash.MD2
|
|
||||||
Crypto.Hash.MD4
|
|
||||||
Crypto.Hash.MD5
|
|
||||||
Crypto.Hash.RIPEMD160
|
|
||||||
Crypto.Hash.SHA
|
|
||||||
"""
|
|
||||||
|
|
||||||
__all__ = ['HMAC', 'MD2', 'MD4', 'MD5', 'RIPEMD', 'RIPEMD160', 'SHA', 'SHA256']
|
|
||||||
__revision__ = "$Id$"
|
|
||||||
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# PublicKey/RSA.py : RSA public key primitive
|
|
||||||
#
|
|
||||||
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
|
|
||||||
#
|
|
||||||
# ===================================================================
|
|
||||||
# The contents of this file are dedicated to the public domain. To
|
|
||||||
# the extent that dedication to the public domain is not available,
|
|
||||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
|
||||||
# non-exclusive license to exercise all rights associated with the
|
|
||||||
# contents of this file for any purpose whatsoever.
|
|
||||||
# No rights are reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
# ===================================================================
|
|
||||||
|
|
||||||
"""RSA public-key cryptography algorithm."""
|
|
||||||
|
|
||||||
__revision__ = "$Id$"
|
|
||||||
|
|
||||||
__all__ = ['generate', 'construct', 'error']
|
|
||||||
|
|
||||||
from Crypto.Util.python_compat import *
|
|
||||||
|
|
||||||
from Crypto.PublicKey import _RSA, _slowmath, pubkey
|
|
||||||
from Crypto import Random
|
|
||||||
|
|
||||||
try:
|
|
||||||
from Crypto.PublicKey import _fastmath
|
|
||||||
except ImportError:
|
|
||||||
_fastmath = None
|
|
||||||
|
|
||||||
class _RSAobj(pubkey.pubkey):
|
|
||||||
keydata = ['n', 'e', 'd', 'p', 'q', 'u']
|
|
||||||
|
|
||||||
def __init__(self, implementation, key):
|
|
||||||
self.implementation = implementation
|
|
||||||
self.key = key
|
|
||||||
|
|
||||||
def __getattr__(self, attrname):
|
|
||||||
if attrname in self.keydata:
|
|
||||||
# For backward compatibility, allow the user to get (not set) the
|
|
||||||
# RSA key parameters directly from this object.
|
|
||||||
return getattr(self.key, attrname)
|
|
||||||
else:
|
|
||||||
raise AttributeError("%s object has no %r attribute" % (self.__class__.__name__, attrname,))
|
|
||||||
|
|
||||||
def _encrypt(self, c, K):
|
|
||||||
return (self.key._encrypt(c),)
|
|
||||||
|
|
||||||
def _decrypt(self, c):
|
|
||||||
#(ciphertext,) = c
|
|
||||||
(ciphertext,) = c[:1] # HACK - We should use the previous line
|
|
||||||
# instead, but this is more compatible and we're
|
|
||||||
# going to replace the Crypto.PublicKey API soon
|
|
||||||
# anyway.
|
|
||||||
return self.key._decrypt(ciphertext)
|
|
||||||
|
|
||||||
def _blind(self, m, r):
|
|
||||||
return self.key._blind(m, r)
|
|
||||||
|
|
||||||
def _unblind(self, m, r):
|
|
||||||
return self.key._unblind(m, r)
|
|
||||||
|
|
||||||
def _sign(self, m, K=None):
|
|
||||||
return (self.key._sign(m),)
|
|
||||||
|
|
||||||
def _verify(self, m, sig):
|
|
||||||
#(s,) = sig
|
|
||||||
(s,) = sig[:1] # HACK - We should use the previous line instead, but
|
|
||||||
# this is more compatible and we're going to replace
|
|
||||||
# the Crypto.PublicKey API soon anyway.
|
|
||||||
return self.key._verify(m, s)
|
|
||||||
|
|
||||||
def has_private(self):
|
|
||||||
return self.key.has_private()
|
|
||||||
|
|
||||||
def size(self):
|
|
||||||
return self.key.size()
|
|
||||||
|
|
||||||
def can_blind(self):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def can_encrypt(self):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def can_sign(self):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def publickey(self):
|
|
||||||
return self.implementation.construct((self.key.n, self.key.e))
|
|
||||||
|
|
||||||
def __getstate__(self):
|
|
||||||
d = {}
|
|
||||||
for k in self.keydata:
|
|
||||||
try:
|
|
||||||
d[k] = getattr(self.key, k)
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
return d
|
|
||||||
|
|
||||||
def __setstate__(self, d):
|
|
||||||
if not hasattr(self, 'implementation'):
|
|
||||||
self.implementation = RSAImplementation()
|
|
||||||
t = []
|
|
||||||
for k in self.keydata:
|
|
||||||
if not d.has_key(k):
|
|
||||||
break
|
|
||||||
t.append(d[k])
|
|
||||||
self.key = self.implementation._math.rsa_construct(*tuple(t))
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
attrs = []
|
|
||||||
for k in self.keydata:
|
|
||||||
if k == 'n':
|
|
||||||
attrs.append("n(%d)" % (self.size()+1,))
|
|
||||||
elif hasattr(self.key, k):
|
|
||||||
attrs.append(k)
|
|
||||||
if self.has_private():
|
|
||||||
attrs.append("private")
|
|
||||||
return "<%s @0x%x %s>" % (self.__class__.__name__, id(self), ",".join(attrs))
|
|
||||||
|
|
||||||
class RSAImplementation(object):
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
# 'use_fast_math' parameter:
|
|
||||||
# None (default) - Use fast math if available; Use slow math if not.
|
|
||||||
# True - Use fast math, and raise RuntimeError if it's not available.
|
|
||||||
# False - Use slow math.
|
|
||||||
use_fast_math = kwargs.get('use_fast_math', None)
|
|
||||||
if use_fast_math is None: # Automatic
|
|
||||||
if _fastmath is not None:
|
|
||||||
self._math = _fastmath
|
|
||||||
else:
|
|
||||||
self._math = _slowmath
|
|
||||||
|
|
||||||
elif use_fast_math: # Explicitly select fast math
|
|
||||||
if _fastmath is not None:
|
|
||||||
self._math = _fastmath
|
|
||||||
else:
|
|
||||||
raise RuntimeError("fast math module not available")
|
|
||||||
|
|
||||||
else: # Explicitly select slow math
|
|
||||||
self._math = _slowmath
|
|
||||||
|
|
||||||
self.error = self._math.error
|
|
||||||
|
|
||||||
# 'default_randfunc' parameter:
|
|
||||||
# None (default) - use Random.new().read
|
|
||||||
# not None - use the specified function
|
|
||||||
self._default_randfunc = kwargs.get('default_randfunc', None)
|
|
||||||
self._current_randfunc = None
|
|
||||||
|
|
||||||
def _get_randfunc(self, randfunc):
|
|
||||||
if randfunc is not None:
|
|
||||||
return randfunc
|
|
||||||
elif self._current_randfunc is None:
|
|
||||||
self._current_randfunc = Random.new().read
|
|
||||||
return self._current_randfunc
|
|
||||||
|
|
||||||
def generate(self, bits, randfunc=None, progress_func=None):
|
|
||||||
rf = self._get_randfunc(randfunc)
|
|
||||||
obj = _RSA.generate_py(bits, rf, progress_func) # TODO: Don't use legacy _RSA module
|
|
||||||
key = self._math.rsa_construct(obj.n, obj.e, obj.d, obj.p, obj.q, obj.u)
|
|
||||||
return _RSAobj(self, key)
|
|
||||||
|
|
||||||
def construct(self, tup):
|
|
||||||
key = self._math.rsa_construct(*tup)
|
|
||||||
return _RSAobj(self, key)
|
|
||||||
|
|
||||||
_impl = RSAImplementation()
|
|
||||||
generate = _impl.generate
|
|
||||||
construct = _impl.construct
|
|
||||||
error = _impl.error
|
|
||||||
|
|
||||||
# vim:set ts=4 sw=4 sts=4 expandtab:
|
|
||||||
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
#
|
|
||||||
# RSA.py : RSA encryption/decryption
|
|
||||||
#
|
|
||||||
# Part of the Python Cryptography Toolkit
|
|
||||||
#
|
|
||||||
# Written by Andrew Kuchling, Paul Swartz, and others
|
|
||||||
#
|
|
||||||
# ===================================================================
|
|
||||||
# The contents of this file are dedicated to the public domain. To
|
|
||||||
# the extent that dedication to the public domain is not available,
|
|
||||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
|
||||||
# non-exclusive license to exercise all rights associated with the
|
|
||||||
# contents of this file for any purpose whatsoever.
|
|
||||||
# No rights are reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
# ===================================================================
|
|
||||||
#
|
|
||||||
|
|
||||||
__revision__ = "$Id$"
|
|
||||||
|
|
||||||
from Crypto.PublicKey import pubkey
|
|
||||||
from Crypto.Util import number
|
|
||||||
|
|
||||||
def generate_py(bits, randfunc, progress_func=None):
|
|
||||||
"""generate(bits:int, randfunc:callable, progress_func:callable)
|
|
||||||
|
|
||||||
Generate an RSA key of length 'bits', using 'randfunc' to get
|
|
||||||
random data and 'progress_func', if present, to display
|
|
||||||
the progress of the key generation.
|
|
||||||
"""
|
|
||||||
obj=RSAobj()
|
|
||||||
obj.e = 65537L
|
|
||||||
|
|
||||||
# Generate the prime factors of n
|
|
||||||
if progress_func:
|
|
||||||
progress_func('p,q\n')
|
|
||||||
p = q = 1L
|
|
||||||
while number.size(p*q) < bits:
|
|
||||||
# Note that q might be one bit longer than p if somebody specifies an odd
|
|
||||||
# number of bits for the key. (Why would anyone do that? You don't get
|
|
||||||
# more security.)
|
|
||||||
#
|
|
||||||
# Note also that we ensure that e is coprime to (p-1) and (q-1).
|
|
||||||
# This is needed for encryption to work properly, according to the 1997
|
|
||||||
# paper by Robert D. Silverman of RSA Labs, "Fast generation of random,
|
|
||||||
# strong RSA primes", available at
|
|
||||||
# http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.17.2713&rep=rep1&type=pdf
|
|
||||||
# Since e=65537 is prime, it is sufficient to check that e divides
|
|
||||||
# neither (p-1) nor (q-1).
|
|
||||||
p = 1L
|
|
||||||
while (p - 1) % obj.e == 0:
|
|
||||||
if progress_func:
|
|
||||||
progress_func('p\n')
|
|
||||||
p = pubkey.getPrime(bits/2, randfunc)
|
|
||||||
q = 1L
|
|
||||||
while (q - 1) % obj.e == 0:
|
|
||||||
if progress_func:
|
|
||||||
progress_func('q\n')
|
|
||||||
q = pubkey.getPrime(bits - (bits/2), randfunc)
|
|
||||||
|
|
||||||
# p shall be smaller than q (for calc of u)
|
|
||||||
if p > q:
|
|
||||||
(p, q)=(q, p)
|
|
||||||
obj.p = p
|
|
||||||
obj.q = q
|
|
||||||
|
|
||||||
if progress_func:
|
|
||||||
progress_func('u\n')
|
|
||||||
obj.u = pubkey.inverse(obj.p, obj.q)
|
|
||||||
obj.n = obj.p*obj.q
|
|
||||||
|
|
||||||
if progress_func:
|
|
||||||
progress_func('d\n')
|
|
||||||
obj.d=pubkey.inverse(obj.e, (obj.p-1)*(obj.q-1))
|
|
||||||
|
|
||||||
assert bits <= 1+obj.size(), "Generated key is too small"
|
|
||||||
|
|
||||||
return obj
|
|
||||||
|
|
||||||
class RSAobj(pubkey.pubkey):
|
|
||||||
|
|
||||||
def size(self):
|
|
||||||
"""size() : int
|
|
||||||
Return the maximum number of bits that can be handled by this key.
|
|
||||||
"""
|
|
||||||
return number.size(self.n) - 1
|
|
||||||
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# ===================================================================
|
|
||||||
# The contents of this file are dedicated to the public domain. To
|
|
||||||
# the extent that dedication to the public domain is not available,
|
|
||||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
|
||||||
# non-exclusive license to exercise all rights associated with the
|
|
||||||
# contents of this file for any purpose whatsoever.
|
|
||||||
# No rights are reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
# ===================================================================
|
|
||||||
|
|
||||||
"""Public-key encryption and signature algorithms.
|
|
||||||
|
|
||||||
Public-key encryption uses two different keys, one for encryption and
|
|
||||||
one for decryption. The encryption key can be made public, and the
|
|
||||||
decryption key is kept private. Many public-key algorithms can also
|
|
||||||
be used to sign messages, and some can *only* be used for signatures.
|
|
||||||
|
|
||||||
Crypto.PublicKey.DSA Digital Signature Algorithm. (Signature only)
|
|
||||||
Crypto.PublicKey.ElGamal (Signing and encryption)
|
|
||||||
Crypto.PublicKey.RSA (Signing, encryption, and blinding)
|
|
||||||
Crypto.PublicKey.qNEW (Signature only)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
__all__ = ['RSA', 'DSA', 'ElGamal', 'qNEW']
|
|
||||||
__revision__ = "$Id$"
|
|
||||||
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# PubKey/RSA/_slowmath.py : Pure Python implementation of the RSA portions of _fastmath
|
|
||||||
#
|
|
||||||
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
|
|
||||||
#
|
|
||||||
# ===================================================================
|
|
||||||
# The contents of this file are dedicated to the public domain. To
|
|
||||||
# the extent that dedication to the public domain is not available,
|
|
||||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
|
||||||
# non-exclusive license to exercise all rights associated with the
|
|
||||||
# contents of this file for any purpose whatsoever.
|
|
||||||
# No rights are reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
# ===================================================================
|
|
||||||
|
|
||||||
"""Pure Python implementation of the RSA-related portions of Crypto.PublicKey._fastmath."""
|
|
||||||
|
|
||||||
__revision__ = "$Id$"
|
|
||||||
|
|
||||||
__all__ = ['rsa_construct']
|
|
||||||
|
|
||||||
from Crypto.Util.python_compat import *
|
|
||||||
|
|
||||||
from Crypto.Util.number import size, inverse
|
|
||||||
|
|
||||||
class error(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class _RSAKey(object):
|
|
||||||
def _blind(self, m, r):
|
|
||||||
# compute r**e * m (mod n)
|
|
||||||
return m * pow(r, self.e, self.n)
|
|
||||||
|
|
||||||
def _unblind(self, m, r):
|
|
||||||
# compute m / r (mod n)
|
|
||||||
return inverse(r, self.n) * m % self.n
|
|
||||||
|
|
||||||
def _decrypt(self, c):
|
|
||||||
# compute c**d (mod n)
|
|
||||||
if not self.has_private():
|
|
||||||
raise TypeError("No private key")
|
|
||||||
return pow(c, self.d, self.n) # TODO: CRT exponentiation
|
|
||||||
|
|
||||||
def _encrypt(self, m):
|
|
||||||
# compute m**d (mod n)
|
|
||||||
return pow(m, self.e, self.n)
|
|
||||||
|
|
||||||
def _sign(self, m): # alias for _decrypt
|
|
||||||
if not self.has_private():
|
|
||||||
raise TypeError("No private key")
|
|
||||||
return self._decrypt(m)
|
|
||||||
|
|
||||||
def _verify(self, m, sig):
|
|
||||||
return self._encrypt(sig) == m
|
|
||||||
|
|
||||||
def has_private(self):
|
|
||||||
return hasattr(self, 'd')
|
|
||||||
|
|
||||||
def size(self):
|
|
||||||
"""Return the maximum number of bits that can be encrypted"""
|
|
||||||
return size(self.n) - 1
|
|
||||||
|
|
||||||
def rsa_construct(n, e, d=None, p=None, q=None, u=None):
|
|
||||||
"""Construct an RSAKey object"""
|
|
||||||
assert isinstance(n, long)
|
|
||||||
assert isinstance(e, long)
|
|
||||||
assert isinstance(d, (long, type(None)))
|
|
||||||
assert isinstance(p, (long, type(None)))
|
|
||||||
assert isinstance(q, (long, type(None)))
|
|
||||||
assert isinstance(u, (long, type(None)))
|
|
||||||
obj = _RSAKey()
|
|
||||||
obj.n = n
|
|
||||||
obj.e = e
|
|
||||||
if d is not None: obj.d = d
|
|
||||||
if p is not None: obj.p = p
|
|
||||||
if q is not None: obj.q = q
|
|
||||||
if u is not None: obj.u = u
|
|
||||||
return obj
|
|
||||||
|
|
||||||
class _DSAKey(object):
|
|
||||||
def size(self):
|
|
||||||
"""Return the maximum number of bits that can be encrypted"""
|
|
||||||
return size(self.p) - 1
|
|
||||||
|
|
||||||
def has_private(self):
|
|
||||||
return hasattr(self, 'x')
|
|
||||||
|
|
||||||
def _sign(self, m, k): # alias for _decrypt
|
|
||||||
# SECURITY TODO - We _should_ be computing SHA1(m), but we don't because that's the API.
|
|
||||||
if not self.has_private():
|
|
||||||
raise TypeError("No private key")
|
|
||||||
if not (1L < k < self.q):
|
|
||||||
raise ValueError("k is not between 2 and q-1")
|
|
||||||
inv_k = inverse(k, self.q) # Compute k**-1 mod q
|
|
||||||
r = pow(self.g, k, self.p) % self.q # r = (g**k mod p) mod q
|
|
||||||
s = (inv_k * (m + self.x * r)) % self.q
|
|
||||||
return (r, s)
|
|
||||||
|
|
||||||
def _verify(self, m, r, s):
|
|
||||||
# SECURITY TODO - We _should_ be computing SHA1(m), but we don't because that's the API.
|
|
||||||
if not (0 < r < self.q) or not (0 < s < self.q):
|
|
||||||
return False
|
|
||||||
w = inverse(s, self.q)
|
|
||||||
u1 = (m*w) % self.q
|
|
||||||
u2 = (r*w) % self.q
|
|
||||||
v = (pow(self.g, u1, self.p) * pow(self.y, u2, self.p) % self.p) % self.q
|
|
||||||
return v == r
|
|
||||||
|
|
||||||
def dsa_construct(y, g, p, q, x=None):
|
|
||||||
assert isinstance(y, long)
|
|
||||||
assert isinstance(g, long)
|
|
||||||
assert isinstance(p, long)
|
|
||||||
assert isinstance(q, long)
|
|
||||||
assert isinstance(x, (long, type(None)))
|
|
||||||
obj = _DSAKey()
|
|
||||||
obj.y = y
|
|
||||||
obj.g = g
|
|
||||||
obj.p = p
|
|
||||||
obj.q = q
|
|
||||||
if x is not None: obj.x = x
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
# vim:set ts=4 sw=4 sts=4 expandtab:
|
|
||||||
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
#
|
|
||||||
# pubkey.py : Internal functions for public key operations
|
|
||||||
#
|
|
||||||
# Part of the Python Cryptography Toolkit
|
|
||||||
#
|
|
||||||
# Written by Andrew Kuchling, Paul Swartz, and others
|
|
||||||
#
|
|
||||||
# ===================================================================
|
|
||||||
# The contents of this file are dedicated to the public domain. To
|
|
||||||
# the extent that dedication to the public domain is not available,
|
|
||||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
|
||||||
# non-exclusive license to exercise all rights associated with the
|
|
||||||
# contents of this file for any purpose whatsoever.
|
|
||||||
# No rights are reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
# ===================================================================
|
|
||||||
#
|
|
||||||
|
|
||||||
__revision__ = "$Id$"
|
|
||||||
|
|
||||||
import types, warnings
|
|
||||||
from Crypto.Util.number import *
|
|
||||||
|
|
||||||
# Basic public key class
|
|
||||||
class pubkey:
|
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __getstate__(self):
|
|
||||||
"""To keep key objects platform-independent, the key data is
|
|
||||||
converted to standard Python long integers before being
|
|
||||||
written out. It will then be reconverted as necessary on
|
|
||||||
restoration."""
|
|
||||||
d=self.__dict__
|
|
||||||
for key in self.keydata:
|
|
||||||
if d.has_key(key): d[key]=long(d[key])
|
|
||||||
return d
|
|
||||||
|
|
||||||
def __setstate__(self, d):
|
|
||||||
"""On unpickling a key object, the key data is converted to the big
|
|
||||||
number representation being used, whether that is Python long
|
|
||||||
integers, MPZ objects, or whatever."""
|
|
||||||
for key in self.keydata:
|
|
||||||
if d.has_key(key): self.__dict__[key]=bignum(d[key])
|
|
||||||
|
|
||||||
def encrypt(self, plaintext, K):
|
|
||||||
"""encrypt(plaintext:string|long, K:string|long) : tuple
|
|
||||||
Encrypt the string or integer plaintext. K is a random
|
|
||||||
parameter required by some algorithms.
|
|
||||||
"""
|
|
||||||
wasString=0
|
|
||||||
if isinstance(plaintext, types.StringType):
|
|
||||||
plaintext=bytes_to_long(plaintext) ; wasString=1
|
|
||||||
if isinstance(K, types.StringType):
|
|
||||||
K=bytes_to_long(K)
|
|
||||||
ciphertext=self._encrypt(plaintext, K)
|
|
||||||
if wasString: return tuple(map(long_to_bytes, ciphertext))
|
|
||||||
else: return ciphertext
|
|
||||||
|
|
||||||
def decrypt(self, ciphertext):
|
|
||||||
"""decrypt(ciphertext:tuple|string|long): string
|
|
||||||
Decrypt 'ciphertext' using this key.
|
|
||||||
"""
|
|
||||||
wasString=0
|
|
||||||
if not isinstance(ciphertext, types.TupleType):
|
|
||||||
ciphertext=(ciphertext,)
|
|
||||||
if isinstance(ciphertext[0], types.StringType):
|
|
||||||
ciphertext=tuple(map(bytes_to_long, ciphertext)) ; wasString=1
|
|
||||||
plaintext=self._decrypt(ciphertext)
|
|
||||||
if wasString: return long_to_bytes(plaintext)
|
|
||||||
else: return plaintext
|
|
||||||
|
|
||||||
def sign(self, M, K):
|
|
||||||
"""sign(M : string|long, K:string|long) : tuple
|
|
||||||
Return a tuple containing the signature for the message M.
|
|
||||||
K is a random parameter required by some algorithms.
|
|
||||||
"""
|
|
||||||
if (not self.has_private()):
|
|
||||||
raise TypeError('Private key not available in this object')
|
|
||||||
if isinstance(M, types.StringType): M=bytes_to_long(M)
|
|
||||||
if isinstance(K, types.StringType): K=bytes_to_long(K)
|
|
||||||
return self._sign(M, K)
|
|
||||||
|
|
||||||
def verify (self, M, signature):
|
|
||||||
"""verify(M:string|long, signature:tuple) : bool
|
|
||||||
Verify that the signature is valid for the message M;
|
|
||||||
returns true if the signature checks out.
|
|
||||||
"""
|
|
||||||
if isinstance(M, types.StringType): M=bytes_to_long(M)
|
|
||||||
return self._verify(M, signature)
|
|
||||||
|
|
||||||
# alias to compensate for the old validate() name
|
|
||||||
def validate (self, M, signature):
|
|
||||||
warnings.warn("validate() method name is obsolete; use verify()",
|
|
||||||
DeprecationWarning)
|
|
||||||
|
|
||||||
def blind(self, M, B):
|
|
||||||
"""blind(M : string|long, B : string|long) : string|long
|
|
||||||
Blind message M using blinding factor B.
|
|
||||||
"""
|
|
||||||
wasString=0
|
|
||||||
if isinstance(M, types.StringType):
|
|
||||||
M=bytes_to_long(M) ; wasString=1
|
|
||||||
if isinstance(B, types.StringType): B=bytes_to_long(B)
|
|
||||||
blindedmessage=self._blind(M, B)
|
|
||||||
if wasString: return long_to_bytes(blindedmessage)
|
|
||||||
else: return blindedmessage
|
|
||||||
|
|
||||||
def unblind(self, M, B):
|
|
||||||
"""unblind(M : string|long, B : string|long) : string|long
|
|
||||||
Unblind message M using blinding factor B.
|
|
||||||
"""
|
|
||||||
wasString=0
|
|
||||||
if isinstance(M, types.StringType):
|
|
||||||
M=bytes_to_long(M) ; wasString=1
|
|
||||||
if isinstance(B, types.StringType): B=bytes_to_long(B)
|
|
||||||
unblindedmessage=self._unblind(M, B)
|
|
||||||
if wasString: return long_to_bytes(unblindedmessage)
|
|
||||||
else: return unblindedmessage
|
|
||||||
|
|
||||||
|
|
||||||
# The following methods will usually be left alone, except for
|
|
||||||
# signature-only algorithms. They both return Boolean values
|
|
||||||
# recording whether this key's algorithm can sign and encrypt.
|
|
||||||
def can_sign (self):
|
|
||||||
"""can_sign() : bool
|
|
||||||
Return a Boolean value recording whether this algorithm can
|
|
||||||
generate signatures. (This does not imply that this
|
|
||||||
particular key object has the private information required to
|
|
||||||
to generate a signature.)
|
|
||||||
"""
|
|
||||||
return 1
|
|
||||||
|
|
||||||
def can_encrypt (self):
|
|
||||||
"""can_encrypt() : bool
|
|
||||||
Return a Boolean value recording whether this algorithm can
|
|
||||||
encrypt data. (This does not imply that this
|
|
||||||
particular key object has the private information required to
|
|
||||||
to decrypt a message.)
|
|
||||||
"""
|
|
||||||
return 1
|
|
||||||
|
|
||||||
def can_blind (self):
|
|
||||||
"""can_blind() : bool
|
|
||||||
Return a Boolean value recording whether this algorithm can
|
|
||||||
blind data. (This does not imply that this
|
|
||||||
particular key object has the private information required to
|
|
||||||
to blind a message.)
|
|
||||||
"""
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# The following methods will certainly be overridden by
|
|
||||||
# subclasses.
|
|
||||||
|
|
||||||
def size (self):
|
|
||||||
"""size() : int
|
|
||||||
Return the maximum number of bits that can be handled by this key.
|
|
||||||
"""
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def has_private (self):
|
|
||||||
"""has_private() : bool
|
|
||||||
Return a Boolean denoting whether the object contains
|
|
||||||
private components.
|
|
||||||
"""
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def publickey (self):
|
|
||||||
"""publickey(): object
|
|
||||||
Return a new key object containing only the public information.
|
|
||||||
"""
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __eq__ (self, other):
|
|
||||||
"""__eq__(other): 0, 1
|
|
||||||
Compare us to other for equality.
|
|
||||||
"""
|
|
||||||
return self.__getstate__() == other.__getstate__()
|
|
||||||
|
|
||||||
def __ne__ (self, other):
|
|
||||||
"""__ne__(other): 0, 1
|
|
||||||
Compare us to other for inequality.
|
|
||||||
"""
|
|
||||||
return not self.__eq__(other)
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
# -*- coding: ascii -*-
|
|
||||||
#
|
|
||||||
# FortunaAccumulator.py : Fortuna's internal accumulator
|
|
||||||
#
|
|
||||||
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
|
|
||||||
#
|
|
||||||
# ===================================================================
|
|
||||||
# The contents of this file are dedicated to the public domain. To
|
|
||||||
# the extent that dedication to the public domain is not available,
|
|
||||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
|
||||||
# non-exclusive license to exercise all rights associated with the
|
|
||||||
# contents of this file for any purpose whatsoever.
|
|
||||||
# No rights are reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
# ===================================================================
|
|
||||||
|
|
||||||
__revision__ = "$Id$"
|
|
||||||
|
|
||||||
from Crypto.Util.python_compat import *
|
|
||||||
|
|
||||||
from binascii import b2a_hex
|
|
||||||
import time
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from Crypto.pct_warnings import ClockRewindWarning
|
|
||||||
import SHAd256
|
|
||||||
|
|
||||||
import FortunaGenerator
|
|
||||||
|
|
||||||
class FortunaPool(object):
|
|
||||||
"""Fortuna pool type
|
|
||||||
|
|
||||||
This object acts like a hash object, with the following differences:
|
|
||||||
|
|
||||||
- It keeps a count (the .length attribute) of the number of bytes that
|
|
||||||
have been added to the pool
|
|
||||||
- It supports a .reset() method for in-place reinitialization
|
|
||||||
- The method to add bytes to the pool is .append(), not .update().
|
|
||||||
"""
|
|
||||||
|
|
||||||
digest_size = SHAd256.digest_size
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.reset()
|
|
||||||
|
|
||||||
def append(self, data):
|
|
||||||
self._h.update(data)
|
|
||||||
self.length += len(data)
|
|
||||||
|
|
||||||
def digest(self):
|
|
||||||
return self._h.digest()
|
|
||||||
|
|
||||||
def hexdigest(self):
|
|
||||||
return b2a_hex(self.digest())
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
self._h = SHAd256.new()
|
|
||||||
self.length = 0
|
|
||||||
|
|
||||||
def which_pools(r):
|
|
||||||
"""Return a list of pools indexes (in range(32)) that are to be included during reseed number r.
|
|
||||||
|
|
||||||
According to _Practical Cryptography_, chapter 10.5.2 "Pools":
|
|
||||||
|
|
||||||
"Pool P_i is included if 2**i is a divisor of r. Thus P_0 is used
|
|
||||||
every reseed, P_1 every other reseed, P_2 every fourth reseed, etc."
|
|
||||||
"""
|
|
||||||
# This is a separate function so that it can be unit-tested.
|
|
||||||
assert r >= 1
|
|
||||||
retval = []
|
|
||||||
mask = 0
|
|
||||||
for i in range(32):
|
|
||||||
# "Pool P_i is included if 2**i is a divisor of [reseed_count]"
|
|
||||||
if (r & mask) == 0:
|
|
||||||
retval.append(i)
|
|
||||||
else:
|
|
||||||
break # optimization. once this fails, it always fails
|
|
||||||
mask = (mask << 1) | 1L
|
|
||||||
return retval
|
|
||||||
|
|
||||||
class FortunaAccumulator(object):
|
|
||||||
|
|
||||||
min_pool_size = 64 # TODO: explain why
|
|
||||||
reseed_interval = 0.100 # 100 ms TODO: explain why
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.reseed_count = 0
|
|
||||||
self.generator = FortunaGenerator.AESGenerator()
|
|
||||||
self.last_reseed = None
|
|
||||||
|
|
||||||
# Initialize 32 FortunaPool instances.
|
|
||||||
# NB: This is _not_ equivalent to [FortunaPool()]*32, which would give
|
|
||||||
# us 32 references to the _same_ FortunaPool instance (and cause the
|
|
||||||
# assertion below to fail).
|
|
||||||
self.pools = [FortunaPool() for i in range(32)] # 32 pools
|
|
||||||
assert(self.pools[0] is not self.pools[1])
|
|
||||||
|
|
||||||
def random_data(self, bytes):
|
|
||||||
current_time = time.time()
|
|
||||||
if self.last_reseed > current_time:
|
|
||||||
warnings.warn("Clock rewind detected. Resetting last_reseed.", ClockRewindWarning)
|
|
||||||
self.last_reseed = None
|
|
||||||
if (self.pools[0].length >= self.min_pool_size and
|
|
||||||
(self.last_reseed is None or
|
|
||||||
current_time > self.last_reseed + self.reseed_interval)):
|
|
||||||
self._reseed(current_time)
|
|
||||||
# The following should fail if we haven't seeded the pool yet.
|
|
||||||
return self.generator.pseudo_random_data(bytes)
|
|
||||||
|
|
||||||
def _reseed(self, current_time=None):
|
|
||||||
if current_time is None:
|
|
||||||
current_time = time.time()
|
|
||||||
seed = []
|
|
||||||
self.reseed_count += 1
|
|
||||||
self.last_reseed = current_time
|
|
||||||
for i in which_pools(self.reseed_count):
|
|
||||||
seed.append(self.pools[i].digest())
|
|
||||||
self.pools[i].reset()
|
|
||||||
|
|
||||||
seed = "".join(seed)
|
|
||||||
self.generator.reseed(seed)
|
|
||||||
|
|
||||||
def add_random_event(self, source_number, pool_number, data):
|
|
||||||
assert 1 <= len(data) <= 32
|
|
||||||
assert 0 <= source_number <= 255
|
|
||||||
assert 0 <= pool_number <= 31
|
|
||||||
self.pools[pool_number].append(chr(source_number))
|
|
||||||
self.pools[pool_number].append(chr(len(data)))
|
|
||||||
self.pools[pool_number].append(data)
|
|
||||||
|
|
||||||
# vim:set ts=4 sw=4 sts=4 expandtab:
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
# -*- coding: ascii -*-
|
|
||||||
#
|
|
||||||
# FortunaGenerator.py : Fortuna's internal PRNG
|
|
||||||
#
|
|
||||||
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
|
|
||||||
#
|
|
||||||
# ===================================================================
|
|
||||||
# The contents of this file are dedicated to the public domain. To
|
|
||||||
# the extent that dedication to the public domain is not available,
|
|
||||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
|
||||||
# non-exclusive license to exercise all rights associated with the
|
|
||||||
# contents of this file for any purpose whatsoever.
|
|
||||||
# No rights are reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
# ===================================================================
|
|
||||||
|
|
||||||
__revision__ = "$Id$"
|
|
||||||
|
|
||||||
from Crypto.Util.python_compat import *
|
|
||||||
|
|
||||||
import struct
|
|
||||||
|
|
||||||
from Crypto.Util.number import ceil_shift, exact_log2, exact_div
|
|
||||||
from Crypto.Util import Counter
|
|
||||||
from Crypto.Cipher import AES
|
|
||||||
|
|
||||||
import SHAd256
|
|
||||||
|
|
||||||
class AESGenerator(object):
|
|
||||||
"""The Fortuna "generator"
|
|
||||||
|
|
||||||
This is used internally by the Fortuna PRNG to generate arbitrary amounts
|
|
||||||
of pseudorandom data from a smaller amount of seed data.
|
|
||||||
|
|
||||||
The output is generated by running AES-256 in counter mode and re-keying
|
|
||||||
after every mebibyte (2**16 blocks) of output.
|
|
||||||
"""
|
|
||||||
|
|
||||||
block_size = AES.block_size # output block size in octets (128 bits)
|
|
||||||
key_size = 32 # key size in octets (256 bits)
|
|
||||||
|
|
||||||
# Because of the birthday paradox, we expect to find approximately one
|
|
||||||
# collision for every 2**64 blocks of output from a real random source.
|
|
||||||
# However, this code generates pseudorandom data by running AES in
|
|
||||||
# counter mode, so there will be no collisions until the counter
|
|
||||||
# (theoretically) wraps around at 2**128 blocks. Thus, in order to prevent
|
|
||||||
# Fortuna's pseudorandom output from deviating perceptibly from a true
|
|
||||||
# random source, Ferguson and Schneier specify a limit of 2**16 blocks
|
|
||||||
# without rekeying.
|
|
||||||
max_blocks_per_request = 2**16 # Allow no more than this number of blocks per _pseudo_random_data request
|
|
||||||
|
|
||||||
_four_kiblocks_of_zeros = "\0" * block_size * 4096
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.counter = Counter.new(nbits=self.block_size*8, initial_value=0, little_endian=True)
|
|
||||||
self.key = None
|
|
||||||
|
|
||||||
# Set some helper constants
|
|
||||||
self.block_size_shift = exact_log2(self.block_size)
|
|
||||||
assert (1 << self.block_size_shift) == self.block_size
|
|
||||||
|
|
||||||
self.blocks_per_key = exact_div(self.key_size, self.block_size)
|
|
||||||
assert self.key_size == self.blocks_per_key * self.block_size
|
|
||||||
|
|
||||||
self.max_bytes_per_request = self.max_blocks_per_request * self.block_size
|
|
||||||
|
|
||||||
def reseed(self, seed):
|
|
||||||
if self.key is None:
|
|
||||||
self.key = "\0" * self.key_size
|
|
||||||
self._set_key(SHAd256.new(self.key + seed).digest())
|
|
||||||
self.counter() # increment counter
|
|
||||||
assert len(self.key) == self.key_size
|
|
||||||
|
|
||||||
def pseudo_random_data(self, bytes):
|
|
||||||
assert bytes >= 0
|
|
||||||
|
|
||||||
num_full_blocks = bytes >> 20
|
|
||||||
remainder = bytes & ((1<<20)-1)
|
|
||||||
|
|
||||||
retval = []
|
|
||||||
for i in xrange(num_full_blocks):
|
|
||||||
retval.append(self._pseudo_random_data(1<<20))
|
|
||||||
retval.append(self._pseudo_random_data(remainder))
|
|
||||||
|
|
||||||
return "".join(retval)
|
|
||||||
|
|
||||||
def _set_key(self, key):
|
|
||||||
self.key = key
|
|
||||||
self._cipher = AES.new(key, AES.MODE_CTR, counter=self.counter)
|
|
||||||
|
|
||||||
def _pseudo_random_data(self, bytes):
|
|
||||||
if not (0 <= bytes <= self.max_bytes_per_request):
|
|
||||||
raise AssertionError("You cannot ask for more than 1 MiB of data per request")
|
|
||||||
|
|
||||||
num_blocks = ceil_shift(bytes, self.block_size_shift) # num_blocks = ceil(bytes / self.block_size)
|
|
||||||
|
|
||||||
# Compute the output
|
|
||||||
retval = self._generate_blocks(num_blocks)[:bytes]
|
|
||||||
|
|
||||||
# Switch to a new key to avoid later compromises of this output (i.e.
|
|
||||||
# state compromise extension attacks)
|
|
||||||
self._set_key(self._generate_blocks(self.blocks_per_key))
|
|
||||||
|
|
||||||
assert len(retval) == bytes
|
|
||||||
assert len(self.key) == self.key_size
|
|
||||||
|
|
||||||
return retval
|
|
||||||
|
|
||||||
def _generate_blocks(self, num_blocks):
|
|
||||||
if self.key is None:
|
|
||||||
raise AssertionError("generator must be seeded before use")
|
|
||||||
assert 0 <= num_blocks <= self.max_blocks_per_request
|
|
||||||
retval = []
|
|
||||||
for i in xrange(num_blocks >> 12): # xrange(num_blocks / 4096)
|
|
||||||
retval.append(self._cipher.encrypt(self._four_kiblocks_of_zeros))
|
|
||||||
remaining_bytes = (num_blocks & 4095) << self.block_size_shift # (num_blocks % 4095) * self.block_size
|
|
||||||
retval.append(self._cipher.encrypt(self._four_kiblocks_of_zeros[:remaining_bytes]))
|
|
||||||
return "".join(retval)
|
|
||||||
|
|
||||||
# vim:set ts=4 sw=4 sts=4 expandtab:
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
# -*- coding: ascii -*-
|
|
||||||
#
|
|
||||||
# Random/Fortuna/SHAd256.py : SHA_d-256 hash function implementation
|
|
||||||
#
|
|
||||||
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
|
|
||||||
#
|
|
||||||
# ===================================================================
|
|
||||||
# The contents of this file are dedicated to the public domain. To
|
|
||||||
# the extent that dedication to the public domain is not available,
|
|
||||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
|
||||||
# non-exclusive license to exercise all rights associated with the
|
|
||||||
# contents of this file for any purpose whatsoever.
|
|
||||||
# No rights are reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
# ===================================================================
|
|
||||||
|
|
||||||
"""\
|
|
||||||
SHA_d-256 hash function implementation.
|
|
||||||
|
|
||||||
This module should comply with PEP 247.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__revision__ = "$Id$"
|
|
||||||
__all__ = ['new', 'digest_size']
|
|
||||||
|
|
||||||
from Crypto.Util.python_compat import *
|
|
||||||
|
|
||||||
from binascii import b2a_hex
|
|
||||||
|
|
||||||
from Crypto.Hash import SHA256
|
|
||||||
|
|
||||||
assert SHA256.digest_size == 32
|
|
||||||
|
|
||||||
class _SHAd256(object):
|
|
||||||
"""SHA-256, doubled.
|
|
||||||
|
|
||||||
Returns SHA-256(SHA-256(data)).
|
|
||||||
"""
|
|
||||||
|
|
||||||
digest_size = SHA256.digest_size
|
|
||||||
|
|
||||||
_internal = object()
|
|
||||||
|
|
||||||
def __init__(self, internal_api_check, sha256_hash_obj):
|
|
||||||
if internal_api_check is not self._internal:
|
|
||||||
raise AssertionError("Do not instantiate this class directly. Use %s.new()" % (__name__,))
|
|
||||||
self._h = sha256_hash_obj
|
|
||||||
|
|
||||||
# PEP 247 "copy" method
|
|
||||||
def copy(self):
|
|
||||||
"""Return a copy of this hashing object"""
|
|
||||||
return _SHAd256(SHAd256._internal, self._h.copy())
|
|
||||||
|
|
||||||
# PEP 247 "digest" method
|
|
||||||
def digest(self):
|
|
||||||
"""Return the hash value of this object as a binary string"""
|
|
||||||
retval = SHA256.new(self._h.digest()).digest()
|
|
||||||
assert len(retval) == 32
|
|
||||||
return retval
|
|
||||||
|
|
||||||
# PEP 247 "hexdigest" method
|
|
||||||
def hexdigest(self):
|
|
||||||
"""Return the hash value of this object as a (lowercase) hexadecimal string"""
|
|
||||||
retval = b2a_hex(self.digest())
|
|
||||||
assert len(retval) == 64
|
|
||||||
return retval
|
|
||||||
|
|
||||||
# PEP 247 "update" method
|
|
||||||
def update(self, data):
|
|
||||||
self._h.update(data)
|
|
||||||
|
|
||||||
# PEP 247 module-level "digest_size" variable
|
|
||||||
digest_size = _SHAd256.digest_size
|
|
||||||
|
|
||||||
# PEP 247 module-level "new" function
|
|
||||||
def new(data=""):
|
|
||||||
"""Return a new SHAd256 hashing object"""
|
|
||||||
return _SHAd256(_SHAd256._internal, SHA256.new(data))
|
|
||||||
|
|
||||||
# vim:set ts=4 sw=4 sts=4 expandtab:
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
#
|
|
||||||
# Random/OSRNG/__init__.py : Platform-independent OS RNG API
|
|
||||||
#
|
|
||||||
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
|
|
||||||
#
|
|
||||||
# ===================================================================
|
|
||||||
# The contents of this file are dedicated to the public domain. To
|
|
||||||
# the extent that dedication to the public domain is not available,
|
|
||||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
|
||||||
# non-exclusive license to exercise all rights associated with the
|
|
||||||
# contents of this file for any purpose whatsoever.
|
|
||||||
# No rights are reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
# ===================================================================
|
|
||||||
|
|
||||||
"""Provides a platform-independent interface to the random number generators
|
|
||||||
supplied by various operating systems."""
|
|
||||||
|
|
||||||
__revision__ = "$Id$"
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
if os.name == 'posix':
|
|
||||||
from Crypto.Random.OSRNG.posix import new
|
|
||||||
elif os.name == 'nt':
|
|
||||||
from Crypto.Random.OSRNG.nt import new
|
|
||||||
elif hasattr(os, 'urandom'):
|
|
||||||
from Crypto.Random.OSRNG.fallback import new
|
|
||||||
else:
|
|
||||||
raise ImportError("Not implemented")
|
|
||||||
|
|
||||||
# vim:set ts=4 sw=4 sts=4 expandtab:
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
#
|
|
||||||
# Random/OSRNG/fallback.py : Fallback entropy source for systems with os.urandom
|
|
||||||
#
|
|
||||||
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
|
|
||||||
#
|
|
||||||
# ===================================================================
|
|
||||||
# The contents of this file are dedicated to the public domain. To
|
|
||||||
# the extent that dedication to the public domain is not available,
|
|
||||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
|
||||||
# non-exclusive license to exercise all rights associated with the
|
|
||||||
# contents of this file for any purpose whatsoever.
|
|
||||||
# No rights are reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
# ===================================================================
|
|
||||||
|
|
||||||
|
|
||||||
__revision__ = "$Id$"
|
|
||||||
__all__ = ['PythonOSURandomRNG']
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from rng_base import BaseRNG
|
|
||||||
|
|
||||||
class PythonOSURandomRNG(BaseRNG):
|
|
||||||
|
|
||||||
name = "<os.urandom>"
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._read = os.urandom
|
|
||||||
BaseRNG.__init__(self)
|
|
||||||
|
|
||||||
def _close(self):
|
|
||||||
self._read = None
|
|
||||||
|
|
||||||
def new(*args, **kwargs):
|
|
||||||
return PythonOSURandomRNG(*args, **kwargs)
|
|
||||||
|
|
||||||
# vim:set ts=4 sw=4 sts=4 expandtab:
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
#
|
|
||||||
# Random/OSRNG/nt.py : OS entropy source for MS Windows
|
|
||||||
#
|
|
||||||
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
|
|
||||||
#
|
|
||||||
# ===================================================================
|
|
||||||
# The contents of this file are dedicated to the public domain. To
|
|
||||||
# the extent that dedication to the public domain is not available,
|
|
||||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
|
||||||
# non-exclusive license to exercise all rights associated with the
|
|
||||||
# contents of this file for any purpose whatsoever.
|
|
||||||
# No rights are reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
# ===================================================================
|
|
||||||
|
|
||||||
|
|
||||||
__revision__ = "$Id$"
|
|
||||||
__all__ = ['WindowsRNG']
|
|
||||||
|
|
||||||
import winrandom
|
|
||||||
from rng_base import BaseRNG
|
|
||||||
|
|
||||||
class WindowsRNG(BaseRNG):
|
|
||||||
|
|
||||||
name = "<CryptGenRandom>"
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.__winrand = winrandom.new()
|
|
||||||
BaseRNG.__init__(self)
|
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
"""Work around weakness in Windows RNG.
|
|
||||||
|
|
||||||
The CryptGenRandom mechanism in some versions of Windows allows an
|
|
||||||
attacker to learn 128 KiB of past and future output. As a workaround,
|
|
||||||
this function reads 128 KiB of 'random' data from Windows and discards
|
|
||||||
it.
|
|
||||||
|
|
||||||
For more information about the weaknesses in CryptGenRandom, see
|
|
||||||
_Cryptanalysis of the Random Number Generator of the Windows Operating
|
|
||||||
System_, by Leo Dorrendorf and Zvi Gutterman and Benny Pinkas
|
|
||||||
http://eprint.iacr.org/2007/419
|
|
||||||
"""
|
|
||||||
if self.closed:
|
|
||||||
raise ValueError("I/O operation on closed file")
|
|
||||||
data = self.__winrand.get_bytes(128*1024)
|
|
||||||
assert (len(data) == 128*1024)
|
|
||||||
BaseRNG.flush(self)
|
|
||||||
|
|
||||||
def _close(self):
|
|
||||||
self.__winrand = None
|
|
||||||
|
|
||||||
def _read(self, N):
|
|
||||||
# Unfortunately, research shows that CryptGenRandom doesn't provide
|
|
||||||
# forward secrecy and fails the next-bit test unless we apply a
|
|
||||||
# workaround, which we do here. See http://eprint.iacr.org/2007/419
|
|
||||||
# for information on the vulnerability.
|
|
||||||
self.flush()
|
|
||||||
data = self.__winrand.get_bytes(N)
|
|
||||||
self.flush()
|
|
||||||
return data
|
|
||||||
|
|
||||||
def new(*args, **kwargs):
|
|
||||||
return WindowsRNG(*args, **kwargs)
|
|
||||||
|
|
||||||
# vim:set ts=4 sw=4 sts=4 expandtab:
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
#
|
|
||||||
# Random/OSRNG/rng_base.py : Base class for OSRNG
|
|
||||||
#
|
|
||||||
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
|
|
||||||
#
|
|
||||||
# ===================================================================
|
|
||||||
# The contents of this file are dedicated to the public domain. To
|
|
||||||
# the extent that dedication to the public domain is not available,
|
|
||||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
|
||||||
# non-exclusive license to exercise all rights associated with the
|
|
||||||
# contents of this file for any purpose whatsoever.
|
|
||||||
# No rights are reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
# ===================================================================
|
|
||||||
|
|
||||||
__revision__ = "$Id$"
|
|
||||||
|
|
||||||
from Crypto.Util.python_compat import *
|
|
||||||
|
|
||||||
class BaseRNG(object):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.closed = False
|
|
||||||
self._selftest()
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def _selftest(self):
|
|
||||||
# Test that urandom can return data
|
|
||||||
data = self.read(16)
|
|
||||||
if len(data) != 16:
|
|
||||||
raise AssertionError("read truncated")
|
|
||||||
|
|
||||||
# Test that we get different data every time (if we don't, the RNG is
|
|
||||||
# probably malfunctioning)
|
|
||||||
data2 = self.read(16)
|
|
||||||
if data == data2:
|
|
||||||
raise AssertionError("OS RNG returned duplicate data")
|
|
||||||
|
|
||||||
# PEP 343: Support for the "with" statement
|
|
||||||
def __enter__(self):
|
|
||||||
pass
|
|
||||||
def __exit__(self):
|
|
||||||
"""PEP 343 support"""
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
if not self.closed:
|
|
||||||
self._close()
|
|
||||||
self.closed = True
|
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def read(self, N=-1):
|
|
||||||
"""Return N bytes from the RNG."""
|
|
||||||
if self.closed:
|
|
||||||
raise ValueError("I/O operation on closed file")
|
|
||||||
if not isinstance(N, (long, int)):
|
|
||||||
raise TypeError("an integer is required")
|
|
||||||
if N < 0:
|
|
||||||
raise ValueError("cannot read to end of infinite stream")
|
|
||||||
elif N == 0:
|
|
||||||
return ""
|
|
||||||
data = self._read(N)
|
|
||||||
if len(data) != N:
|
|
||||||
raise AssertionError("%s produced truncated output (requested %d, got %d)" % (self.name, N, len(data)))
|
|
||||||
return data
|
|
||||||
|
|
||||||
def _close(self):
|
|
||||||
raise NotImplementedError("child class must implement this")
|
|
||||||
|
|
||||||
def _read(self, N):
|
|
||||||
raise NotImplementedError("child class must implement this")
|
|
||||||
|
|
||||||
|
|
||||||
# vim:set ts=4 sw=4 sts=4 expandtab:
|
|
||||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user