Compare commits

...

35 Commits
v3.4 ... v6.0.5

Author SHA1 Message Date
Apprentice Alf
a399d3b7bd tools v6.0.5 2015-03-07 19:29:43 +00:00
Apprentice Alf
cd2d74601a tools v6.0.4 2015-03-07 14:55:09 +00:00
Apprentice Alf
51919284ca tools v6.0.3 2015-03-07 14:47:45 +00:00
Apprentice Alf
d586f74faa tools v6.0.2 2015-03-07 14:45:12 +00:00
Apprentice Alf
a2f044e672 tools v6.0.1 2015-03-07 14:38:41 +00:00
Apprentice Alf
20bc936e99 tools v6.0.0
The first unified calibre plugin
2015-03-07 14:34:21 +00:00
Apprentice Alf
748bd2d471 tools v5.6.2 2015-03-07 14:29:24 +00:00
Apprentice Alf
490ee4e5d8 tools v5.6.1 2015-03-07 14:25:39 +00:00
Apprentice Alf
c23b903420 tools v5.6 2015-03-07 14:21:18 +00:00
Apprentice Alf
602ff30b3a tools v5.5.3 2015-03-07 14:02:17 +00:00
Apprentice Alf
c010e3f77a tools v5.5.2 2015-03-07 13:58:52 +00:00
Apprentice Alf
0c03820db7 tools v5.5.1 2015-03-07 13:55:45 +00:00
Apprentice Alf
9fda194391 tools v5.5
Plugins now include unaltered stand-alone scripts, so no longer need to keep separate copies.
2015-03-07 13:48:25 +00:00
Apprentice Alf
b661a6cdc5 tools v5.4.1 2015-03-07 13:28:34 +00:00
Apprentice Alf
0dcd18d524 tools v5.4 2015-03-07 13:14:37 +00:00
Apprentice Alf
0028027f71 Changed from DeDRM_WinApp to DeDRM_App 2015-03-07 13:05:48 +00:00
TetraChroma
4b3618246b ineptpdf 8.4.51 2015-03-06 18:02:17 +00:00
Apprentice Alf
07ea87edf4 tools v5.3.1 2015-03-06 18:00:34 +00:00
Apprentice Alf
899fd419ae tools v5.3 2015-03-06 17:57:20 +00:00
Apprentice Alf
f3f02adc98 tools v5.2 2015-03-06 17:43:57 +00:00
Apprentice Alf
0812438b9d nib changed from folder to file, so must delete first 2015-03-06 17:41:42 +00:00
Apprentice Alf
26d9f7bd20 ReadMe names changed 2015-03-06 17:30:07 +00:00
Apprentice Alf
2c95633fcd tools v5.1
alfcrypto added to DeDRM plugin
2015-03-06 17:15:59 +00:00
Apprentice Alf
07e532f59c tools v5.0
Introduction of alfcrypto library for speed
Reorganisation of archive plugins,apps,other
2015-03-06 07:43:33 +00:00
Apprentice Alf
882edb6c69 Re-arrange folders and files for 5.0 tools 2015-03-06 07:32:13 +00:00
Apprentice Alf
93f02c625a tools v4.8 2015-03-06 07:24:30 +00:00
Apprentice Alf
e95ed1a8ed tools v4.7 2015-03-06 07:18:01 +00:00
Apprentice Alf
ba5927a20d tools v4.6 2015-03-06 07:13:06 +00:00
Apprentice Alf
297a9ddc66 tools v4.5 2015-03-06 07:08:24 +00:00
Apprentice Alf
4f34a9a196 tools v4.0
New calibre plugin interface (0.7.55)
Dropped unswindle.pyw
Added Android patch
2015-03-06 06:59:36 +00:00
Apprentice Alf
529dd3f160 tools v3.8
version 2 - a minor change to one script.
2015-03-05 17:54:25 +00:00
Apprentice Alf
4163d5ccf4 tools v3.8 2015-03-05 17:48:25 +00:00
Apprentice Alf
867ac35b45 tools v3.7 2015-03-05 17:42:55 +00:00
Apprentice Alf
427137b0fe tools v3.6 2015-03-05 17:37:44 +00:00
Apprentice Alf
ac9cdb1e98 tools v3.5 2015-03-05 17:33:13 +00:00
291 changed files with 36436 additions and 29537 deletions

3
.gitignore vendored
View File

@@ -2,9 +2,6 @@
__pycache__/ __pycache__/
*.pyc *.pyc
# C extensions
*.so
# Distribution / packaging # Distribution / packaging
.Python .Python
env/ env/

View File

@@ -1,18 +0,0 @@
From Apprentice Alf's Blog
Adobe Adept ePub, .epub
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 Adobes 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_vX.X.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_vX.X.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.

View File

@@ -1,476 +0,0 @@
#! /usr/bin/python
# -*- coding: utf-8 -*-
from __future__ import with_statement
# ineptepub.pyw, version 5.6
# Copyright © 2009-2010 i♥cabbages
# Released under the terms of the GNU General Public Licence, version 3 or
# later. <http://www.gnu.org/licenses/>
# Windows users: Before running this program, you must first install Python 2.6
# from <http://www.python.org/download/> and PyCrypto from
# <http://www.voidspace.org.uk/python/modules.shtml#pycrypto> (make 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 - Rename to INEPT, fix exit code
# 5 - Version bump to avoid (?) confusion;
# Improve OS X support by using OpenSSL when available
# 5.1 - Improve OpenSSL error checking
# 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.4 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml
# 5.5 - On Windows try PyCrypto first, OpenSSL next
# 5.6 - Modify interface to allow use with import
"""
Decrypt Adobe ADEPT-encrypted EPUB books.
"""
__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 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
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)
return (AES, RSA)
def _load_crypto():
AES = RSA = 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, RSA = loader()
break
except (ImportError, ADEPTError):
pass
return (AES, RSA)
AES, RSA = _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
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('adeptkey.der'):
self.keypath.insert(0, 'adeptkey.der')
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 ADEPT key file',
defaultextension='.der', filetypes=[('DER-encoded files', '.der'),
('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 ADEPT-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 decryptBook(keypath, inpath, outpath):
with open(keypath, 'rb') as f:
keyder = f.read()
rsa = RSA(keyder)
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 ADEPTError('%s: not an 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 = 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))
return 0
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:]
return decryptBook(keypath, inpath, outpath)
def gui_main():
root = Tkinter.Tk()
if AES is None:
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.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())

View File

@@ -1,18 +0,0 @@
From Apprentice Alf's Blog
Adobe Adept PDF, .pdf
This directory includes modified versions of the I♥CABBAGES Adobe Adept inept scripts for pdfs. These scripts have been modified to work with OpenSSL on Windows as well as Linux and Mac OS X. 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 Adobes 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_vX.X.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 ineptpdf_vX.X.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.

View File

@@ -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, as well as to fix some minor bugs/
There are 2 scripts:
The first is ignoblekeygen_vX.X.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_vX.X.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.

View File

@@ -1,336 +0,0 @@
#! /usr/bin/python
from __future__ import with_statement
# ignobleepub.pyw, version 3.4
# 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
# 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
__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
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()
"""
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)
path = path.encode('utf-8')
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
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 decryptBook(keypath, inpath, outpath):
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
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:]
return decryptBook(keypath, inpath, outpath)
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())

View File

@@ -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())

View File

@@ -1,239 +0,0 @@
#! /usr/bin/python
from __future__ import with_statement
# ignoblekeygen.pyw, version 2.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
# 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
# 2.2 - On Windows try PyCrypto first and then OpenSSL next
# 2.3 - Modify interface to allow use of import
"""
Generate Barnes & Noble EPUB user key from name and credit card number.
"""
__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
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_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
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 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
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())

View File

@@ -1,818 +0,0 @@
#! /usr/bin/python
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
# For use with Topaz Scripts Version 2.6
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
# Get a 7 bit encoded number from string. The most
# significant byte comes first and has the high bit (8th) set
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
# returns a binary string that encodes a number into 7 bits
# most significant byte first which has the high bit set
def encodeNumber(number):
result = ""
negative = False
flag = 0
if number < 0 :
number = -number + 1
negative = True
while True:
byte = number & 0x7F
number = number >> 7
byte += flag
result += chr(byte)
flag = 0x80
if number == 0 :
if (byte == 0xFF and negative == False) :
result += chr(0x80)
break
if negative:
result += chr(0xFF)
return result[::-1]
# create / read 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 ""
sv = file.read(stringLength)
if (len(sv) != stringLength):
return ""
return unpack(str(stringLength)+"s",sv)[0]
# convert a binary string generated by encodeNumber (7 bit encoded number)
# to the value you would find inside the page*.dat files to be processed
def convert(i):
result = ''
val = encodeNumber(i)
for j in xrange(len(val)):
c = ord(val[j:j+1])
result += '%02x' % c
return result
# the complete string table used to store all book text content
# as well as the xml tokens and values that make sense out of it
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('&','&amp;')
str = str.replace('<','&lt;')
str = str.replace('>','&gt;')
str = str.replace('=','&#61;')
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
sys.exit(-1)
def getSize(self):
return self.size
def getPos(self):
return self.pos
def dumpDict(self):
for i in xrange(self.size):
print "%d %s %s" % (i, convert(i), self.stable[i])
return
# parses the xml snippets that are represented by each page*.dat file.
# also parses the other0.dat file - the main stylesheet
# and information used to inject the xml snippets into page*.dat files
class PageParser(object):
def __init__(self, filename, dict, debug, flat_xml):
self.fo = file(filename,'rb')
self.id = os.path.basename(filename).replace('.dat','')
self.dict = dict
self.debug = debug
self.flat_xml = flat_xml
self.tagpath = []
self.doc = []
self.snippetList = []
# hash table used to enable the decoding process
# This has all been developed by trial and error so it may still have omissions or
# contain errors
# Format:
# tag : (number of arguments, argument type, subtags present, special case of subtags presents when escaped)
token_tags = {
'x' : (1, 'scalar_number', 0, 0),
'y' : (1, 'scalar_number', 0, 0),
'h' : (1, 'scalar_number', 0, 0),
'w' : (1, 'scalar_number', 0, 0),
'firstWord' : (1, 'scalar_number', 0, 0),
'lastWord' : (1, 'scalar_number', 0, 0),
'rootID' : (1, 'scalar_number', 0, 0),
'stemID' : (1, 'scalar_number', 0, 0),
'type' : (1, 'scalar_text', 0, 0),
'info' : (0, 'number', 1, 0),
'info.word' : (0, 'number', 1, 1),
'info.word.ocrText' : (1, 'text', 0, 0),
'info.word.firstGlyph' : (1, 'raw', 0, 0),
'info.word.lastGlyph' : (1, 'raw', 0, 0),
'info.word.bl' : (1, 'raw', 0, 0),
'info.word.link_id' : (1, 'number', 0, 0),
'glyph' : (0, 'number', 1, 1),
'glyph.x' : (1, 'number', 0, 0),
'glyph.y' : (1, 'number', 0, 0),
'glyph.glyphID' : (1, 'number', 0, 0),
'dehyphen' : (0, 'number', 1, 1),
'dehyphen.rootID' : (1, 'number', 0, 0),
'dehyphen.stemID' : (1, 'number', 0, 0),
'dehyphen.stemPage' : (1, 'number', 0, 0),
'dehyphen.sh' : (1, 'number', 0, 0),
'links' : (0, 'number', 1, 1),
'links.page' : (1, 'number', 0, 0),
'links.rel' : (1, 'number', 0, 0),
'links.row' : (1, 'number', 0, 0),
'links.title' : (1, 'text', 0, 0),
'links.href' : (1, 'text', 0, 0),
'links.type' : (1, 'text', 0, 0),
'paraCont' : (0, 'number', 1, 1),
'paraCont.rootID' : (1, 'number', 0, 0),
'paraCont.stemID' : (1, 'number', 0, 0),
'paraCont.stemPage' : (1, 'number', 0, 0),
'paraStems' : (0, 'number', 1, 1),
'paraStems.stemID' : (1, 'number', 0, 0),
'wordStems' : (0, 'number', 1, 1),
'wordStems.stemID' : (1, 'number', 0, 0),
'empty' : (1, 'snippets', 1, 0),
'page' : (1, 'snippets', 1, 0),
'page.pageid' : (1, 'scalar_text', 0, 0),
'page.pagelabel' : (1, 'scalar_text', 0, 0),
'page.type' : (1, 'scalar_text', 0, 0),
'page.h' : (1, 'scalar_number', 0, 0),
'page.w' : (1, 'scalar_number', 0, 0),
'page.startID' : (1, 'scalar_number', 0, 0),
'group' : (1, 'snippets', 1, 0),
'group.type' : (1, 'scalar_text', 0, 0),
'group._tag' : (1, 'scalar_text', 0, 0),
'region' : (1, 'snippets', 1, 0),
'region.type' : (1, 'scalar_text', 0, 0),
'region.x' : (1, 'scalar_number', 0, 0),
'region.y' : (1, 'scalar_number', 0, 0),
'region.h' : (1, 'scalar_number', 0, 0),
'region.w' : (1, 'scalar_number', 0, 0),
'empty_text_region' : (1, 'snippets', 1, 0),
'img' : (1, 'snippets', 1, 0),
'img.x' : (1, 'scalar_number', 0, 0),
'img.y' : (1, 'scalar_number', 0, 0),
'img.h' : (1, 'scalar_number', 0, 0),
'img.w' : (1, 'scalar_number', 0, 0),
'img.src' : (1, 'scalar_number', 0, 0),
'img.color_src' : (1, 'scalar_number', 0, 0),
'paragraph' : (1, 'snippets', 1, 0),
'paragraph.class' : (1, 'scalar_text', 0, 0),
'paragraph.firstWord' : (1, 'scalar_number', 0, 0),
'paragraph.lastWord' : (1, 'scalar_number', 0, 0),
'word_semantic' : (1, 'snippets', 1, 1),
'word_semantic.type' : (1, 'scalar_text', 0, 0),
'word_semantic.firstWord' : (1, 'scalar_number', 0, 0),
'word_semantic.lastWord' : (1, 'scalar_number', 0, 0),
'word' : (1, 'snippets', 1, 0),
'word.type' : (1, 'scalar_text', 0, 0),
'word.class' : (1, 'scalar_text', 0, 0),
'word.firstGlyph' : (1, 'scalar_number', 0, 0),
'word.lastGlyph' : (1, 'scalar_number', 0, 0),
'_span' : (1, 'snippets', 1, 0),
'_span.firstWord' : (1, 'scalar_number', 0, 0),
'-span.lastWord' : (1, 'scalar_number', 0, 0),
'span' : (1, 'snippets', 1, 0),
'span.firstWord' : (1, 'scalar_number', 0, 0),
'span.lastWord' : (1, 'scalar_number', 0, 0),
'extratokens' : (1, 'snippets', 1, 0),
'extratokens.type' : (1, 'scalar_text', 0, 0),
'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0),
'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0),
'glyph.h' : (1, 'number', 0, 0),
'glyph.w' : (1, 'number', 0, 0),
'glyph.use' : (1, 'number', 0, 0),
'glyph.vtx' : (1, 'number', 0, 1),
'glyph.len' : (1, 'number', 0, 1),
'glyph.dpi' : (1, 'number', 0, 0),
'vtx' : (0, 'number', 1, 1),
'vtx.x' : (1, 'number', 0, 0),
'vtx.y' : (1, 'number', 0, 0),
'len' : (0, 'number', 1, 1),
'len.n' : (1, 'number', 0, 0),
'book' : (1, 'snippets', 1, 0),
'version' : (1, 'snippets', 1, 0),
'version.FlowEdit_1_id' : (1, 'scalar_text', 0, 0),
'version.FlowEdit_1_version' : (1, 'scalar_text', 0, 0),
'version.Schema_id' : (1, 'scalar_text', 0, 0),
'version.Schema_version' : (1, 'scalar_text', 0, 0),
'version.Topaz_version' : (1, 'scalar_text', 0, 0),
'version.WordDetailEdit_1_id' : (1, 'scalar_text', 0, 0),
'version.WordDetailEdit_1_version' : (1, 'scalar_text', 0, 0),
'version.ZoneEdit_1_id' : (1, 'scalar_text', 0, 0),
'version.ZoneEdit_1_version' : (1, 'scalar_text', 0, 0),
'version.chapterheaders' : (1, 'scalar_text', 0, 0),
'version.creation_date' : (1, 'scalar_text', 0, 0),
'version.header_footer' : (1, 'scalar_text', 0, 0),
'version.init_from_ocr' : (1, 'scalar_text', 0, 0),
'version.letter_insertion' : (1, 'scalar_text', 0, 0),
'version.xmlinj_convert' : (1, 'scalar_text', 0, 0),
'version.xmlinj_reflow' : (1, 'scalar_text', 0, 0),
'version.xmlinj_transform' : (1, 'scalar_text', 0, 0),
'version.findlists' : (1, 'scalar_text', 0, 0),
'version.page_num' : (1, 'scalar_text', 0, 0),
'version.page_type' : (1, 'scalar_text', 0, 0),
'version.bad_text' : (1, 'scalar_text', 0, 0),
'version.glyph_mismatch' : (1, 'scalar_text', 0, 0),
'version.margins' : (1, 'scalar_text', 0, 0),
'version.staggered_lines' : (1, 'scalar_text', 0, 0),
'version.paragraph_continuation' : (1, 'scalar_text', 0, 0),
'version.toc' : (1, 'scalar_text', 0, 0),
'stylesheet' : (1, 'snippets', 1, 0),
'style' : (1, 'snippets', 1, 0),
'style._tag' : (1, 'scalar_text', 0, 0),
'style.type' : (1, 'scalar_text', 0, 0),
'style._parent_type' : (1, 'scalar_text', 0, 0),
'style.class' : (1, 'scalar_text', 0, 0),
'style._after_class' : (1, 'scalar_text', 0, 0),
'rule' : (1, 'snippets', 1, 0),
'rule.attr' : (1, 'scalar_text', 0, 0),
'rule.value' : (1, 'scalar_text', 0, 0),
'original' : (0, 'number', 1, 1),
'original.pnum' : (1, 'number', 0, 0),
'original.pid' : (1, 'text', 0, 0),
'pages' : (0, 'number', 1, 1),
'pages.ref' : (1, 'number', 0, 0),
'pages.id' : (1, 'number', 0, 0),
'startID' : (0, 'number', 1, 1),
'startID.page' : (1, 'number', 0, 0),
'startID.id' : (1, 'number', 0, 0),
}
# full tag path record keeping routines
def tag_push(self, token):
self.tagpath.append(token)
def tag_pop(self):
if len(self.tagpath) > 0 :
self.tagpath.pop()
def tagpath_len(self):
return len(self.tagpath)
def get_tagpath(self, i):
cnt = len(self.tagpath)
if i < cnt : result = self.tagpath[i]
for j in xrange(i+1, cnt) :
result += '.' + self.tagpath[j]
return result
# list of absolute command byte values values that indicate
# various types of loop meachanisms typically used to generate vectors
cmd_list = (0x76, 0x76)
# peek at and return 1 byte that is ahead by i bytes
def peek(self, aheadi):
c = self.fo.read(aheadi)
if (len(c) == 0):
return None
self.fo.seek(-aheadi,1)
c = c[-1:]
return ord(c)
# get the next value from the file being processed
def getNext(self):
nbyte = self.peek(1);
if (nbyte == None):
return None
val = readEncodedNumber(self.fo)
return val
# format an arg by argtype
def formatArg(self, arg, argtype):
if (argtype == 'text') or (argtype == 'scalar_text') :
result = self.dict.lookup(arg)
elif (argtype == 'raw') or (argtype == 'number') or (argtype == 'scalar_number') :
result = arg
elif (argtype == 'snippets') :
result = arg
else :
print "Error Unknown argtype %s" % argtype
sys.exit(-2)
return result
# process the next tag token, recursively handling subtags,
# arguments, and commands
def procToken(self, token):
known_token = False
self.tag_push(token)
if self.debug : print 'Processing: ', self.get_tagpath(0)
cnt = self.tagpath_len()
for j in xrange(cnt):
tkn = self.get_tagpath(j)
if tkn in self.token_tags :
num_args = self.token_tags[tkn][0]
argtype = self.token_tags[tkn][1]
subtags = self.token_tags[tkn][2]
splcase = self.token_tags[tkn][3]
ntags = -1
known_token = True
break
if known_token :
# handle subtags if present
subtagres = []
if (splcase == 1):
# this type of tag uses of escape marker 0x74 indicate subtag count
if self.peek(1) == 0x74:
skip = readEncodedNumber(self.fo)
subtags = 1
num_args = 0
if (subtags == 1):
ntags = readEncodedNumber(self.fo)
if self.debug : print 'subtags: ' + token + ' has ' + str(ntags)
for j in xrange(ntags):
val = readEncodedNumber(self.fo)
subtagres.append(self.procToken(self.dict.lookup(val)))
# arguments can be scalars or vectors of text or numbers
argres = []
if num_args > 0 :
firstarg = self.peek(1)
if (firstarg in self.cmd_list) and (argtype != 'scalar_number') and (argtype != 'scalar_text'):
# single argument is a variable length vector of data
arg = readEncodedNumber(self.fo)
argres = self.decodeCMD(arg,argtype)
else :
# num_arg scalar arguments
for i in xrange(num_args):
argres.append(self.formatArg(readEncodedNumber(self.fo), argtype))
# build the return tag
result = []
tkn = self.get_tagpath(0)
result.append(tkn)
result.append(subtagres)
result.append(argtype)
result.append(argres)
self.tag_pop()
return result
# all tokens that need to be processed should be in the hash
# table if it may indicate a problem, either new token
# or an out of sync condition
else:
result = []
if (self.debug):
print 'Unknown Token:', token
self.tag_pop()
return result
# special loop used to process code snippets
# it is NEVER used to format arguments.
# builds the snippetList
def doLoop72(self, argtype):
cnt = readEncodedNumber(self.fo)
if self.debug :
result = 'Set of '+ str(cnt) + ' xml snippets. The overall structure \n'
result += 'of the document is indicated by snippet number sets at the\n'
result += 'end of each snippet. \n'
print result
for i in xrange(cnt):
if self.debug: print 'Snippet:',str(i)
snippet = []
snippet.append(i)
val = readEncodedNumber(self.fo)
snippet.append(self.procToken(self.dict.lookup(val)))
self.snippetList.append(snippet)
return
# general loop code gracisouly submitted by "skindle" - thank you!
def doLoop76Mode(self, argtype, cnt, mode):
result = []
adj = 0
if mode & 1:
adj = readEncodedNumber(self.fo)
mode = mode >> 1
x = []
for i in xrange(cnt):
x.append(readEncodedNumber(self.fo) - adj)
for i in xrange(mode):
for j in xrange(1, cnt):
x[j] = x[j] + x[j - 1]
for i in xrange(cnt):
result.append(self.formatArg(x[i],argtype))
return result
# dispatches loop commands bytes with various modes
# The 0x76 style loops are used to build vectors
# This was all derived by trial and error and
# new loop types may exist that are not handled here
# since they did not appear in the test cases
def decodeCMD(self, cmd, argtype):
if (cmd == 0x76):
# loop with cnt, and mode to control loop styles
cnt = readEncodedNumber(self.fo)
mode = readEncodedNumber(self.fo)
if self.debug : print 'Loop for', cnt, 'with mode', mode, ': '
return self.doLoop76Mode(argtype, cnt, mode)
if self.dbug: print "Unknown command", cmd
result = []
return result
# add full tag path to injected snippets
def updateName(self, tag, prefix):
name = tag[0]
subtagList = tag[1]
argtype = tag[2]
argList = tag[3]
nname = prefix + '.' + name
nsubtaglist = []
for j in subtagList:
nsubtaglist.append(self.updateName(j,prefix))
ntag = []
ntag.append(nname)
ntag.append(nsubtaglist)
ntag.append(argtype)
ntag.append(argList)
return ntag
# perform depth first injection of specified snippets into this one
def injectSnippets(self, snippet):
snipno, tag = snippet
name = tag[0]
subtagList = tag[1]
argtype = tag[2]
argList = tag[3]
nsubtagList = []
if len(argList) > 0 :
for j in argList:
asnip = self.snippetList[j]
aso, atag = self.injectSnippets(asnip)
atag = self.updateName(atag, name)
nsubtagList.append(atag)
argtype='number'
argList=[]
if len(nsubtagList) > 0 :
subtagList.extend(nsubtagList)
tag = []
tag.append(name)
tag.append(subtagList)
tag.append(argtype)
tag.append(argList)
snippet = []
snippet.append(snipno)
snippet.append(tag)
return snippet
# format the tag for output
def formatTag(self, node):
name = node[0]
subtagList = node[1]
argtype = node[2]
argList = node[3]
fullpathname = name.split('.')
nodename = fullpathname.pop()
ilvl = len(fullpathname)
indent = ' ' * (3 * ilvl)
result = indent + '<' + nodename + '>'
if len(argList) > 0:
argres = ''
for j in argList:
if (argtype == 'text') or (argtype == 'scalar_text') :
argres += j + '|'
else :
argres += str(j) + ','
argres = argres[0:-1]
if argtype == 'snippets' :
result += 'snippets:' + argres
else :
result += argres
if len(subtagList) > 0 :
result += '\n'
for j in subtagList:
if len(j) > 0 :
result += self.formatTag(j)
result += indent + '</' + nodename + '>\n'
else:
result += '</' + nodename + '>\n'
return result
# flatten tag
def flattenTag(self, node):
name = node[0]
subtagList = node[1]
argtype = node[2]
argList = node[3]
result = name
if (len(argList) > 0):
argres = ''
for j in argList:
if (argtype == 'text') or (argtype == 'scalar_text') :
argres += j + '|'
else :
argres += str(j) + '|'
argres = argres[0:-1]
if argtype == 'snippets' :
result += '.snippets=' + argres
else :
result += '=' + argres
result += '\n'
for j in subtagList:
if len(j) > 0 :
result += self.flattenTag(j)
return result
# reduce create xml output
def formatDoc(self, flat_xml):
result = ''
for j in self.doc :
if len(j) > 0:
if flat_xml:
result += self.flattenTag(j)
else:
result += self.formatTag(j)
if self.debug : print result
return result
# main loop - parse the page.dat files
# to create structured document and snippets
# FIXME: value at end of magic appears to be a subtags count
# but for what? For now, inject an 'info" tag as it is in
# every dictionary and seems close to what is meant
# The alternative is to special case the last _ "0x5f" to mean something
def process(self):
# peek at the first bytes to see what type of file it is
magic = self.fo.read(9)
if (magic[0:1] == 'p') and (magic[2:9] == 'marker_'):
first_token = 'info'
elif (magic[0:1] == 'p') and (magic[2:9] == '__PAGE_'):
skip = self.fo.read(2)
first_token = 'info'
elif (magic[0:1] == 'p') and (magic[2:8] == '_PAGE_'):
first_token = 'info'
elif (magic[0:1] == 'g') and (magic[2:9] == '__GLYPH'):
skip = self.fo.read(3)
first_token = 'info'
else :
# other0.dat file
first_token = None
self.fo.seek(-9,1)
# main loop to read and build the document tree
while True:
if first_token != None :
# use "inserted" first token 'info' for page and glyph files
tag = self.procToken(first_token)
if len(tag) > 0 :
self.doc.append(tag)
first_token = None
v = self.getNext()
if (v == None):
break
if (v == 0x72):
self.doLoop72('number')
elif (v > 0) and (v < self.dict.getSize()) :
tag = self.procToken(self.dict.lookup(v))
if len(tag) > 0 :
self.doc.append(tag)
else:
if self.debug:
print "Main Loop: Unknown value: %x" % v
if (v == 0):
if (self.peek(1) == 0x5f):
skip = self.fo.read(1)
first_token = 'info'
# now do snippet injection
if len(self.snippetList) > 0 :
if self.debug : print 'Injecting Snippets:'
snippet = self.injectSnippets(self.snippetList[0])
snipno = snippet[0]
tag_add = snippet[1]
if self.debug : print self.formatTag(tag_add)
if len(tag_add) > 0:
self.doc.append(tag_add)
# handle generation of xml output
xmlpage = self.formatDoc(self.flat_xml)
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():
print 'Usage: '
print ' convert2xml.py dict0000.dat infile.dat '
print ' '
print ' Options:'
print ' -h print this usage help message '
print ' -d turn on debug output to check for potential errors '
print ' --flat-xml output the flattened xml page description only '
print ' '
print ' This program will attempt to convert a page*.dat file or '
print ' glyphs*.dat file, using the dict0000.dat file, to its xml description. '
print ' '
print ' Use "cmbtc_dump.py" first to unencrypt, uncompress, and dump '
print ' the *.dat files from a Topaz format e-book.'
#
# Main
#
def main(argv):
dictFile = ""
pageFile = ""
debug = False
flat_xml = False
printOutput = False
if len(argv) == 0:
printOutput = True
argv = sys.argv
try:
opts, args = getopt.getopt(argv[1:], "hd", ["flat-xml"])
except getopt.GetoptError, err:
# print help information and exit:
print str(err) # will print something like "option -a not recognized"
usage()
sys.exit(2)
if len(opts) == 0 and len(args) == 0 :
usage()
sys.exit(2)
for o, a in opts:
if o =="-d":
debug=True
if o =="-h":
usage()
sys.exit(0)
if o =="--flat-xml":
flat_xml = True
dictFile, pageFile = args[0], args[1]
# read in the string table dictionary
dict = Dictionary(dictFile)
# dict.dumpDict()
# create a page parser
pp = PageParser(pageFile, dict, debug, flat_xml)
xmlpage = pp.process()
if printOutput:
print xmlpage
return 0
return xmlpage
if __name__ == '__main__':
sys.exit(main(''))

View File

@@ -1,151 +0,0 @@
#! /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):
self.gd = gd
self.flatdoc = flatxml.split('\n')
self.temp = []
foo = self.getData('page.h') or self.getData('book.h')
self.ph = foo[0]
foo = self.getData('page.w') or self.getData('book.w')
self.pw = foo[0]
self.gx = self.getData('info.glyph.x')
self.gy = self.getData('info.glyph.y')
self.gid = self.getData('info.glyph.glyphID')
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 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, counter, numfiles, svgDir, raw, meta_array, scaledpi):
ml = ''
pp = PParser(gdict, flat_xml)
ml += '<?xml version="1.0" standalone="no"?>\n'
if (raw):
ml += '<!DOCTYPE svg PUBLIC "-//W3C/DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n'
ml += '<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)
ml += '<title>Page %d - %s by %s</title>\n' % (counter, meta_array['Title'],meta_array['Authors'])
else:
ml += '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n'
ml += '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" ><head>\n'
ml += '<title>Page %d - %s by %s</title>\n' % (counter, meta_array['Title'],meta_array['Authors'])
ml += '<script><![CDATA[\n'
ml += 'function gd(){var p=window.location.href.replace(/^.*\?dpi=(\d+).*$/i,"$1");return p;}\n'
ml += 'var dpi=%d;\n' % scaledpi
if (counter) :
ml += 'var prevpage="page%04d.xhtml";\n' % (counter - 1)
if (counter < numfiles-1) :
ml += 'var nextpage="page%04d.xhtml";\n' % (counter + 1)
ml += 'var pw=%d;var ph=%d;' % (pp.pw, pp.ph)
ml += 'function zoomin(){dpi=dpi*(0.8);setsize();}\n'
ml += 'function zoomout(){dpi=dpi*1.25;setsize();}\n'
ml += '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'
ml += 'function ppage(){window.location.href=prevpage+"?dpi="+Math.round(dpi);}\n'
ml += 'function npage(){window.location.href=nextpage+"?dpi="+Math.round(dpi);}\n'
ml += 'var gt=gd();if(gt>0){dpi=gt;}\n'
ml += 'window.onload=setsize;\n'
ml += ']]></script>\n'
ml += '</head>\n'
ml += '<body onLoad="setsize();" style="background-color:#777;text-align:center;">\n'
ml += '<div style="white-space:nowrap;">\n'
if (counter == 0) :
ml += '<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:
ml += '<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'
ml += '<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):
ml += '<defs>\n'
gdefs = pp.getGlyphs()
for j in xrange(0,len(gdefs)):
ml += gdefs[j]
ml += '</defs>\n'
img = pp.getImages()
if (img != None):
for j in xrange(0,len(img)):
ml += img[j]
if (pp.gid != None):
for j in xrange(0,len(pp.gid)):
ml += '<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):
ml += '<text x="10" y="10" font-family="Helvetica" font-size="100" stroke="black">This page intentionally left blank.</text>\n<text x="10" y="110" font-family="Helvetica" font-size="50" stroke="black">Until this notice unintentionally gave it content. (gensvg.py)</text>\n'
if (raw) :
ml += '</svg>'
else :
ml += '</svg></a>\n'
if (counter == numfiles - 1) :
ml += '<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 :
ml += '<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'
ml += '</div>\n'
ml += '<div><a href="javascript:zoomin();">zoom in</a> - <a href="javascript:zoomout();">zoom out</a></div>\n'
ml += '</body>\n'
ml += '</html>\n'
return ml

View File

@@ -1,316 +0,0 @@
#!/usr/bin/env python
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 kindleDatabase
global charMap1
global charMap2
global charMap3
global charMap4
if sys.platform.startswith('win'):
from k4pcutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap2
if sys.platform.startswith('darwin'):
from k4mutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap2
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
# Parse the Kindle.info file and return the records as a list of key-values
def parseKindleInfo(kInfoFile):
DB = {}
infoReader = openKindleInfo(kInfoFile)
infoReader.read(1)
data = infoReader.read()
if sys.platform.startswith('win'):
items = data.split('{')
else :
items = data.split('[')
for item in items:
splito = item.split(':')
DB[splito[0]] =splito[1]
return DB
# Get a record from the Kindle.info file for the key "hashedKey" (already hashed and encoded).
# Return the decoded and decrypted record
def getKindleInfoValueForHash(hashedKey):
global kindleDatabase
global charMap1
global charMap2
encryptedValue = decode(kindleDatabase[hashedKey],charMap2)
if sys.platform.startswith('win'):
return CryptUnprotectData(encryptedValue,"")
else:
cleartext = CryptUnprotectData(encryptedValue)
return decode(cleartext, charMap1)
# Get a record from the Kindle.info file for the string in "key" (plaintext).
# Return the decoded and decrypted record
def getKindleInfoValueForKey(key):
global charMap2
return getKindleInfoValueForHash(encodeHash(key,charMap2))
# Find if the original string for a hashed/encoded string is known.
# If so return the original string othwise return an empty string.
def findNameForHash(hash):
global charMap2
names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
result = ""
for name in names:
if hash == encodeHash(name, charMap2):
result = name
break
return result
# Print all the records from the kindle.info file (option -i)
def printKindleInfo():
for record in kindleDatabase:
name = findNameForHash(record)
if name != "" :
print (name)
print ("--------------------------")
else :
print ("Unknown Record")
print getKindleInfoValueForHash(record)
print "\n"
#
# 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 getKindlePid(pidlst, rec209, token, serialnum):
if rec209 != None and token != None:
# Compute book PID
pidHash = SHA1(serialnum+rec209+token)
bookPID = encodePID(pidHash)
bookPID = checksumPid(bookPID)
pidlst.append(bookPID)
# compute fixed pid for old pre 2.5 firmware update pid as well
bookPID = pidFromSerial(serialnum, 7) + "*"
bookPID = checksumPid(bookPID)
pidlst.append(bookPID)
return pidlst
# Parse the EXTH header records and parse the Kindleinfo
# file to calculate the book pid.
def getK4Pids(pidlst, rec209, token, kInfoFile=None):
global kindleDatabase
global charMap1
kindleDatabase = None
try:
kindleDatabase = parseKindleInfo(kInfoFile)
except Exception, message:
print(message)
kindleDatabase = None
pass
if kindleDatabase == None :
return pidlst
# Get the Mazama Random number
MazamaRandomNumber = getKindleInfoValueForKey("MazamaRandomNumber")
# Get the HDD serial
encodedSystemVolumeSerialNumber = encodeHash(GetVolumeSerialNumber(),charMap1)
# Get the current user name
encodedUsername = encodeHash(GetUserName(),charMap1)
# concat, hash and encode to calculate the DSN
DSN = encode(SHA1(MazamaRandomNumber+encodedSystemVolumeSerialNumber+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)
pidlst.append(devicePID)
# Compute book PID
if rec209 == None or token == None:
print "\nNo EXTH record type 209 or token - Perhaps not a K4 file?"
return pidlst
# Get the kindle account token
kindleAccountToken = getKindleInfoValueForKey("kindle.account.tokens")
# book pid
pidHash = SHA1(DSN+kindleAccountToken+rec209+token)
bookPID = encodePID(pidHash)
bookPID = checksumPid(bookPID)
pidlst.append(bookPID)
# variant 1
pidHash = SHA1(kindleAccountToken+rec209+token)
bookPID = encodePID(pidHash)
bookPID = checksumPid(bookPID)
pidlst.append(bookPID)
# variant 2
pidHash = SHA1(DSN+rec209+token)
bookPID = encodePID(pidHash)
bookPID = checksumPid(bookPID)
pidlst.append(bookPID)
return pidlst
def getPidList(md1, md2, k4, pids, serials, kInfoFiles):
pidlst = []
if k4:
pidlst = getK4Pids(pidlst, md1, md2)
for infoFile in kInfoFiles:
pidlst = getK4Pids(pidlst, md1, md2, infoFile)
for serialnum in serials:
pidlst = getKindlePid(pidlst, md1, md2, serialnum)
for pid in pids:
pidlst.append(pid)
return pidlst

View File

@@ -1,436 +0,0 @@
#!/usr/bin/env python
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 os, csv, getopt
import zlib, zipfile, tempfile, shutil
from struct import pack
from struct import unpack
class TpzDRMError(Exception):
pass
# local support routines
import kgenpids
import genbook
#
# 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):
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):
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 TpzDRMError("Didn't find PID magic numbers in record")
elif fields[1] != 8 or fields[3] != 8 :
raise TpzDRMError("Record didn't contain correct length fields")
elif fields[2] != PID :
raise TpzDRMError("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 TpzDRMError:
pass
data = data[1+length:]
if len(records) == 0:
raise TpzDRMError("BookKey Not Found")
return records
class TopazBook:
def __init__(self, filename, outdir):
self.fo = file(filename, 'rb')
self.outdir = outdir
self.bookPayloadOffset = 0
self.bookHeaderRecords = {}
self.bookMetadata = {}
self.bookKey = None
magic = unpack("4s",self.fo.read(4))[0]
if magic != 'TPZ0':
raise TpzDRMError("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)
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 TpzDRMError("Parse Error : Invalid Header")
tag = bookReadString(self.fo)
record = bookReadHeaderRecordData()
return [tag,record]
nbRecords = bookReadEncodedNumber(self.fo)
for i in range (0,nbRecords):
result = parseTopazHeaderRecord()
# print result[0], result[1]
self.bookHeaderRecords[result[0]] = result[1]
if ord(self.fo.read(1)) != 0x64 :
raise TpzDRMError("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 TpzDRMError("Parse Error : Record Names Don't Match")
flags = ord(self.fo.read(1))
nbRecords = ord(self.fo.read(1))
for i in range (0,nbRecords) :
record = [bookReadString(self.fo), bookReadString(self.fo)]
self.bookMetadata[record[0]] = record[1]
return self.bookMetadata
def getPIDMetaInfo(self):
keysRecord = None
keysRecordRecord = None
if 'keys' in self.bookMetadata:
keysRecord = self.bookMetadata['keys']
if keysRecord in self.bookMetadata:
keysRecordRecord = self.bookMetadata[keysRecord]
return keysRecord, keysRecordRecord
def getBookTitle(self):
title = ''
if 'Title' in self.bookMetadata:
title = self.bookMetadata['Title']
return title
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 TpzDRMError("Parse Error : Invalid Record, record not found")
self.fo.seek(self.bookPayloadOffset + recordOffset)
tag = bookReadString(self.fo)
if tag != name :
raise TpzDRMError("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 TpzDRMError("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 TpzDRMError("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 TpzDRMError, e:
print "no dkey record found, book may not be encrypted"
print "attempting to extrct files without a book key"
self.createBookDirectory()
self.extractFiles()
print "Successfully Extracted Topaz contents"
rv = genbook.generateBook(self.outdir, raw, fixedimage)
if rv == 0:
print "\nBook 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 "\nTrying: ", pid
bookKeys = []
data = keydata
try:
bookKeys+=decryptDkeyRecords(data,pid)
except TpzDRMError, e:
pass
else:
bookKey = bookKeys[0]
print "Book Key Found!"
break
if not bookKey:
raise TpzDRMError('Decryption Unsucessful; No valid pid found')
self.setBookKey(bookKey)
self.createBookDirectory()
self.extractFiles()
print "Successfully Extracted Topaz contents"
rv = genbook.generateBook(self.outdir, raw, fixedimage)
if rv == 0:
print "\nBook 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,'img')
if not os.path.exists(destdir):
os.makedirs(destdir)
destdir = os.path.join(outdir,'color_img')
if not os.path.exists(destdir):
os.makedirs(destdir)
destdir = os.path.join(outdir,'page')
if not os.path.exists(destdir):
os.makedirs(destdir)
destdir = os.path.join(outdir,'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 = '.dat'
if name == 'img' : ext = '.jpg'
if name == 'color' : ext = '.jpg'
print "\nProcessing Section: %s " % name
for index in range (0,len(self.bookHeaderRecords[name])) :
fnum = "%04d" % index
fname = name + fnum + ext
destdir = outdir
if name == 'img':
destdir = os.path.join(outdir,'img')
if name == 'color':
destdir = os.path.join(outdir,'color_img')
if name == 'page':
destdir = os.path.join(outdir,'page')
if name == 'glyphs':
destdir = os.path.join(outdir,'glyphs')
outputFile = os.path.join(destdir,fname)
print ".",
record = self.getBookPayloadRecord(name,index)
if record != '':
file(outputFile, 'wb').write(record)
print " "
def zipUpDir(myzip, tempdir,localname):
currentdir = tempdir
if localname != "":
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, tempdir, localfilePath)
def usage(progname):
print "Removes DRM protection from Topaz ebooks and extract the contents"
print "Usage:"
print " %s [-k <kindle.info>] [-p <pidnums>] [-s <kindleSerialNumbers>] <infile> <outdir> " % progname
# Main
def main(argv=sys.argv):
progname = os.path.basename(argv[0])
k4 = False
pids = []
serials = []
kInfoFiles = []
try:
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:")
except getopt.GetoptError, err:
print str(err)
usage(progname)
return 1
if len(args)<2:
usage(progname)
return 1
for o, a in opts:
if o == "-k":
if a == None :
print "Invalid parameter for -k"
return 1
kInfoFiles.append(a)
if o == "-p":
if a == None :
print "Invalid parameter for -p"
return 1
pids = a.split(',')
if o == "-s":
if a == None :
print "Invalid parameter for -s"
return 1
serials = a.split(',')
k4 = True
infile = args[0]
outdir = args[1]
if not os.path.isfile(infile):
print "Input File Does Not Exist"
return 1
bookname = os.path.splitext(os.path.basename(infile))[0]
tempdir = tempfile.mkdtemp()
tb = TopazBook(infile, tempdir)
title = tb.getBookTitle()
print "Processing Book: ", title
keysRecord, keysRecordRecord = tb.getPIDMetaInfo()
pidlst = kgenpids.getPidList(keysRecord, keysRecordRecord, k4, pids, serials, kInfoFiles)
try:
tb.processBook(pidlst)
except TpzDRMError, e:
print str(e)
print " Creating DeBug Full Zip Archive of Book"
zipname = os.path.join(outdir, bookname + '_debug' + '.zip')
myzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
zipUpDir(myzip, tempdir, '')
myzip.close()
shutil.rmtree(tempdir, True)
return 1
print " Creating HTML ZIP Archive"
zipname = os.path.join(outdir, bookname + '_nodrm' + '.zip')
myzip1 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
myzip1.write(os.path.join(tempdir,'book.html'),'book.html')
myzip1.write(os.path.join(tempdir,'book.opf'),'book.opf')
if os.path.isfile(os.path.join(tempdir,'cover.jpg')):
myzip1.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg')
myzip1.write(os.path.join(tempdir,'style.css'),'style.css')
zipUpDir(myzip1, tempdir, 'img')
myzip1.close()
print " Creating SVG ZIP Archive"
zipname = os.path.join(outdir, bookname + '_SVG' + '.zip')
myzip2 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
myzip2.write(os.path.join(tempdir,'index_svg.xhtml'),'index_svg.xhtml')
zipUpDir(myzip2, tempdir, 'svg')
zipUpDir(myzip2, tempdir, 'img')
myzip2.close()
print " Creating XML ZIP Archive"
zipname = os.path.join(outdir, bookname + '_XML' + '.zip')
myzip3 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
targetdir = os.path.join(tempdir,'xml')
zipUpDir(myzip3, targetdir, '')
zipUpDir(myzip3, tempdir, 'img')
myzip3.close()
shutil.rmtree(tempdir, True)
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@@ -1,39 +0,0 @@
Inept PDF Plugin - ineptpdf_vXX_plugin.zip
Requires Calibre version 0.6.44 or higher.
All credit given to IHeartCabbages 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:
Go to Calibre's Preferences page... click on the Plugins button. Use the file dialog button to select the plugin's zip file (ineptpdf_vXX_plugin.zip) and click the 'Add' button. you're done.
Please note: Calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added.
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.

View File

@@ -1,23 +0,0 @@
Plugin for K4PC, K4Mac, standalone Kindles, Mobi Books, and for Devices with Fixed PIDs.
This plugin supersedes MobiDeDRM, K4DeDRM, and K4PCDeDRM and K4X plugins. If you install this plugin, those plugins can be safely removed.
This plugin is meant to remove the DRM from .prc, .azw, .azw1, 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 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.
Please note: Calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added.
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. Include in this list (again separated by commas) any 16 digit serial numbers the standalone Kindles you may have (these typically begin "B0...") This is not needed if you only want to decode "Kindle for PC" or "Kindle for Mac" books.

View File

@@ -1,23 +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.
Please note: Calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added.
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

View File

@@ -1,40 +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.
Please note: Calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added.
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

View File

@@ -1,39 +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.
Please note: Calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added.
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.

View File

@@ -1,137 +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.
#
# 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
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, 4) # 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
global bookname, erdr2pml
import erdr2pml
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:
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
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)'

View File

@@ -1,90 +0,0 @@
#!/usr/bin/env python
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
# implement just enough of des from openssl to make erdr2pml.py happy
def load_libcrypto():
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_char, c_int, c_long, \
Structure, c_ulong, create_string_buffer, cast
from ctypes.util import find_library
import sys
if sys.platform.startswith('win'):
libcrypto = find_library('libeay32')
else:
libcrypto = find_library('crypto')
if libcrypto is None:
return None
libcrypto = CDLL(libcrypto)
# typedef struct DES_ks
# {
# union
# {
# DES_cblock cblock;
# /* make sure things are correct size on machines with
# * 8 byte longs */
# DES_LONG deslong[2];
# } ks[16];
# } DES_key_schedule;
# just create a big enough place to hold everything
# it will have alignment of structure so we should be okay (16 byte aligned?)
class DES_KEY_SCHEDULE(Structure):
_fields_ = [('DES_cblock1', c_char * 16),
('DES_cblock2', c_char * 16),
('DES_cblock3', c_char * 16),
('DES_cblock4', c_char * 16),
('DES_cblock5', c_char * 16),
('DES_cblock6', c_char * 16),
('DES_cblock7', c_char * 16),
('DES_cblock8', c_char * 16),
('DES_cblock9', c_char * 16),
('DES_cblock10', c_char * 16),
('DES_cblock11', c_char * 16),
('DES_cblock12', c_char * 16),
('DES_cblock13', c_char * 16),
('DES_cblock14', c_char * 16),
('DES_cblock15', c_char * 16),
('DES_cblock16', c_char * 16)]
DES_KEY_SCHEDULE_p = POINTER(DES_KEY_SCHEDULE)
def F(restype, name, argtypes):
func = getattr(libcrypto, name)
func.restype = restype
func.argtypes = argtypes
return func
DES_set_key = F(None, 'DES_set_key',[c_char_p, DES_KEY_SCHEDULE_p])
DES_ecb_encrypt = F(None, 'DES_ecb_encrypt',[c_char_p, c_char_p, DES_KEY_SCHEDULE_p, c_int])
class DES(object):
def __init__(self, key):
if len(key) != 8 :
raise Error('DES improper key used')
return
self.key = key
self.keyschedule = DES_KEY_SCHEDULE()
DES_set_key(self.key, self.keyschedule)
def desdecrypt(self, data):
ob = create_string_buffer(len(data))
DES_ecb_encrypt(data, ob, self.keyschedule, 0)
return ob.raw
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

View File

@@ -1,384 +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.
# 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
"""
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
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, _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)
path = path.encode('utf-8')
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, 5)
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
AES, AES2 = _load_crypto()
if AES == None or AES2 == None:
# Failed to load libcrypto or PyCrypto... Adobe Epubs can't be decrypted.'
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.
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:
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()
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()
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
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)'

View File

@@ -1,156 +0,0 @@
#!/usr/bin/env python
import sys
import zlib
import zipfile
import os
import os.path
import getopt
from struct import unpack
_FILENAME_LEN_OFFSET = 26
_EXTRA_LEN_OFFSET = 28
_FILENAME_OFFSET = 30
_MAX_SIZE = 64 * 1024
_MIMETYPE = 'application/epub+zip'
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 fixZip:
def __init__(self, zinput, zoutput):
self.ztype = 'zip'
if zinput.lower().find('.epub') >= 0 :
self.ztype = 'epub'
self.inzip = zipfile.ZipFile(zinput,'r')
self.outzip = zipfile.ZipFile(zoutput,'w')
# open the input zip for reading only as a raw file
self.bzf = file(zinput,'rb')
def getlocalname(self, zi):
local_header_offset = zi.header_offset
self.bzf.seek(local_header_offset + _FILENAME_LEN_OFFSET)
leninfo = self.bzf.read(2)
local_name_length, = unpack('<H', leninfo)
self.bzf.seek(local_header_offset + _FILENAME_OFFSET)
local_name = self.bzf.read(local_name_length)
return local_name
def uncompress(self, cmpdata):
dc = zlib.decompressobj(-15)
data = ''
while len(cmpdata) > 0:
if len(cmpdata) > _MAX_SIZE :
newdata = cmpdata[0:_MAX_SIZE]
cmpdata = cmpdata[_MAX_SIZE:]
else:
newdata = cmpdata
cmpdata = ''
newdata = dc.decompress(newdata)
unprocessed = dc.unconsumed_tail
if len(unprocessed) == 0:
newdata += dc.flush()
data += newdata
cmpdata += unprocessed
unprocessed = ''
return data
def getfiledata(self, zi):
# get file name length and exta data length to find start of file data
local_header_offset = zi.header_offset
self.bzf.seek(local_header_offset + _FILENAME_LEN_OFFSET)
leninfo = self.bzf.read(2)
local_name_length, = unpack('<H', leninfo)
self.bzf.seek(local_header_offset + _EXTRA_LEN_OFFSET)
exinfo = self.bzf.read(2)
extra_field_length, = unpack('<H', exinfo)
self.bzf.seek(local_header_offset + _FILENAME_OFFSET + local_name_length + extra_field_length)
data = None
# if not compressed we are good to go
if zi.compress_type == zipfile.ZIP_STORED:
data = self.bzf.read(zi.file_size)
# if compressed we must decompress it using zlib
if zi.compress_type == zipfile.ZIP_DEFLATED:
cmpdata = self.bzf.read(zi.compress_size)
data = self.uncompress(cmpdata)
return data
def fix(self):
# get the zipinfo for each member of the input archive
# and copy member over to output archive
# if problems exist with local vs central filename, fix them
# if epub write mimetype file first, with no compression
if self.ztype == 'epub':
nzinfo = ZipInfo('mimetype', compress_type=zipfile.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
nzinfo = zinfo
try:
data = self.inzip.read(zinfo.filename)
except zipfile.BadZipfile or zipfile.error:
local_name = self.getlocalname(zinfo)
data = self.getfiledata(zinfo)
nzinfo.filename = local_name
nzinfo.date_time = zinfo.date_time
nzinfo.compress_type = zinfo.compress_type
nzinfo.flag_bits = 0
nzinfo.internal_attr = 0
self.outzip.writestr(nzinfo,data)
self.bzf.close()
self.inzip.close()
self.outzip.close()
def usage():
print """usage: zipfix.py inputzip outputzip
inputzip is the source zipfile to fix
outputzip is the fixed zip archive
"""
def repairBook(infile, outfile):
if not os.path.exists(infile):
print "Error: Input Zip File does not exist"
return 1
try:
fr = fixZip(infile, outfile)
fr.fix()
return 0
except Exception, e:
print "Error Occurred ", e
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__' :
sys.exit(main())

Binary file not shown.

View File

@@ -1,346 +0,0 @@
#!/usr/bin/env python
"""
Retrieve Adobe ADEPT user key.
"""
from __future__ import with_statement
__license__ = 'GPL v3'
import sys
import os
import struct
from calibre.constants import iswindows, isosx
class ADEPTError(Exception):
pass
if iswindows:
from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
string_at, Structure, c_void_p, cast, c_size_t, memmove, CDLL, c_int, \
c_long, c_ulong
from ctypes.wintypes import LPVOID, DWORD, BOOL
import _winreg as winreg
def _load_crypto_libcrypto():
from ctypes.util import find_library
libcrypto = find_library('libeay32')
if libcrypto is None:
raise ADEPTError('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 ADEPTError('AES improper key used')
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
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_pycrypto, _load_crypto_libcrypto):
try:
AES = loader()
break
except (ImportError, ADEPTError):
pass
return AES
AES = _load_crypto()
DEVICE_KEY_PATH = r'Software\Adobe\Adept\Device'
PRIVATE_LICENCE_KEY_PATH = r'Software\Adobe\Adept\Activation'
MAX_PATH = 255
kernel32 = windll.kernel32
advapi32 = windll.advapi32
crypt32 = windll.crypt32
def GetSystemDirectory():
GetSystemDirectoryW = kernel32.GetSystemDirectoryW
GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint]
GetSystemDirectoryW.restype = c_uint
def GetSystemDirectory():
buffer = create_unicode_buffer(MAX_PATH + 1)
GetSystemDirectoryW(buffer, len(buffer))
return buffer.value
return GetSystemDirectory
GetSystemDirectory = GetSystemDirectory()
def GetVolumeSerialNumber():
GetVolumeInformationW = kernel32.GetVolumeInformationW
GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint,
POINTER(c_uint), POINTER(c_uint),
POINTER(c_uint), c_wchar_p, c_uint]
GetVolumeInformationW.restype = c_uint
def GetVolumeSerialNumber(path):
vsn = c_uint(0)
GetVolumeInformationW(
path, None, 0, byref(vsn), None, None, None, 0)
return vsn.value
return GetVolumeSerialNumber
GetVolumeSerialNumber = GetVolumeSerialNumber()
def GetUserName():
GetUserNameW = advapi32.GetUserNameW
GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
GetUserNameW.restype = c_uint
def GetUserName():
buffer = create_unicode_buffer(32)
size = c_uint(len(buffer))
while not GetUserNameW(buffer, byref(size)):
buffer = create_unicode_buffer(len(buffer) * 2)
size.value = len(buffer)
return buffer.value.encode('utf-16-le')[::2]
return GetUserName
GetUserName = GetUserName()
PAGE_EXECUTE_READWRITE = 0x40
MEM_COMMIT = 0x1000
MEM_RESERVE = 0x2000
def VirtualAlloc():
_VirtualAlloc = kernel32.VirtualAlloc
_VirtualAlloc.argtypes = [LPVOID, c_size_t, DWORD, DWORD]
_VirtualAlloc.restype = LPVOID
def VirtualAlloc(addr, size, alloctype=(MEM_COMMIT | MEM_RESERVE),
protect=PAGE_EXECUTE_READWRITE):
return _VirtualAlloc(addr, size, alloctype, protect)
return VirtualAlloc
VirtualAlloc = VirtualAlloc()
MEM_RELEASE = 0x8000
def VirtualFree():
_VirtualFree = kernel32.VirtualFree
_VirtualFree.argtypes = [LPVOID, c_size_t, DWORD]
_VirtualFree.restype = BOOL
def VirtualFree(addr, size=0, freetype=MEM_RELEASE):
return _VirtualFree(addr, size, freetype)
return VirtualFree
VirtualFree = VirtualFree()
class NativeFunction(object):
def __init__(self, restype, argtypes, insns):
self._buf = buf = VirtualAlloc(None, len(insns))
memmove(buf, insns, len(insns))
ftype = CFUNCTYPE(restype, *argtypes)
self._native = ftype(buf)
def __call__(self, *args):
return self._native(*args)
def __del__(self):
if self._buf is not None:
VirtualFree(self._buf)
self._buf = None
if struct.calcsize("P") == 4:
CPUID0_INSNS = (
"\x53" # push %ebx
"\x31\xc0" # xor %eax,%eax
"\x0f\xa2" # cpuid
"\x8b\x44\x24\x08" # mov 0x8(%esp),%eax
"\x89\x18" # mov %ebx,0x0(%eax)
"\x89\x50\x04" # mov %edx,0x4(%eax)
"\x89\x48\x08" # mov %ecx,0x8(%eax)
"\x5b" # pop %ebx
"\xc3" # ret
)
CPUID1_INSNS = (
"\x53" # push %ebx
"\x31\xc0" # xor %eax,%eax
"\x40" # inc %eax
"\x0f\xa2" # cpuid
"\x5b" # pop %ebx
"\xc3" # ret
)
else:
CPUID0_INSNS = (
"\x49\x89\xd8" # mov %rbx,%r8
"\x49\x89\xc9" # mov %rcx,%r9
"\x48\x31\xc0" # xor %rax,%rax
"\x0f\xa2" # cpuid
"\x4c\x89\xc8" # mov %r9,%rax
"\x89\x18" # mov %ebx,0x0(%rax)
"\x89\x50\x04" # mov %edx,0x4(%rax)
"\x89\x48\x08" # mov %ecx,0x8(%rax)
"\x4c\x89\xc3" # mov %r8,%rbx
"\xc3" # retq
)
CPUID1_INSNS = (
"\x53" # push %rbx
"\x48\x31\xc0" # xor %rax,%rax
"\x48\xff\xc0" # inc %rax
"\x0f\xa2" # cpuid
"\x5b" # pop %rbx
"\xc3" # retq
)
def cpuid0():
_cpuid0 = NativeFunction(None, [c_char_p], CPUID0_INSNS)
buf = create_string_buffer(12)
def cpuid0():
_cpuid0(buf)
return buf.raw
return cpuid0
cpuid0 = cpuid0()
cpuid1 = NativeFunction(c_uint, [], CPUID1_INSNS)
class DataBlob(Structure):
_fields_ = [('cbData', c_uint),
('pbData', c_void_p)]
DataBlob_p = POINTER(DataBlob)
def CryptUnprotectData():
_CryptUnprotectData = crypt32.CryptUnprotectData
_CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
c_void_p, c_void_p, c_uint, DataBlob_p]
_CryptUnprotectData.restype = c_uint
def CryptUnprotectData(indata, entropy):
indatab = create_string_buffer(indata)
indata = DataBlob(len(indata), cast(indatab, c_void_p))
entropyb = create_string_buffer(entropy)
entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
outdata = DataBlob()
if not _CryptUnprotectData(byref(indata), None, byref(entropy),
None, None, 0, byref(outdata)):
raise ADEPTError("Failed to decrypt user key key (sic)")
return string_at(outdata.pbData, outdata.cbData)
return CryptUnprotectData
CryptUnprotectData = CryptUnprotectData()
def retrieve_key():
if AES is None:
tkMessageBox.showerror(
"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] + '\\'
serial = GetVolumeSerialNumber(root)
vendor = cpuid0()
signature = struct.pack('>I', cpuid1())[1:]
user = GetUserName()
entropy = struct.pack('>I12s3s13s', serial, vendor, signature, user)
cuser = winreg.HKEY_CURRENT_USER
try:
regkey = winreg.OpenKey(cuser, DEVICE_KEY_PATH)
except WindowsError:
raise ADEPTError("Adobe Digital Editions not activated")
device = winreg.QueryValueEx(regkey, 'key')[0]
keykey = CryptUnprotectData(device, entropy)
userkey = None
try:
plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH)
except WindowsError:
raise ADEPTError("Could not locate ADE activation")
for i in xrange(0, 16):
try:
plkparent = winreg.OpenKey(plkroot, "%04d" % (i,))
except WindowsError:
break
ktype = winreg.QueryValueEx(plkparent, None)[0]
if ktype != 'credentials':
continue
for j in xrange(0, 16):
try:
plkkey = winreg.OpenKey(plkparent, "%04d" % (j,))
except WindowsError:
break
ktype = winreg.QueryValueEx(plkkey, None)[0]
if ktype != 'privateLicenseKey':
continue
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')
aes = AES(keykey)
userkey = aes.decrypt(userkey)
userkey = userkey[26:-ord(userkey[-1])]
return userkey
else:
import xml.etree.ElementTree as etree
import subprocess
NSMAP = {'adept': 'http://ns.adobe.com/adept',
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
def findActivationDat():
home = os.getenv('HOME')
cmdline = 'find "' + home + '/Library/Application Support/Adobe/Digital Editions" -name "activation.dat"'
cmdline = cmdline.encode(sys.getfilesystemencoding())
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
def retrieve_key():
actpath = findActivationDat()
if actpath is None:
raise ADEPTError("Could not locate ADE activation")
tree = etree.parse(actpath)
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
expr = '//%s/%s' % (adept('credentials'), adept('privateLicenseKey'))
userkey = tree.findtext(expr)
userkey = userkey.decode('base64')
userkey = userkey[26:]
return userkey

View File

@@ -1,479 +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.
# 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
"""
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
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, _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)
path = path.encode('utf-8')
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, 6)
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
AES, RSA = _load_crypto()
if AES == None or RSA == None:
# Failed to load libcrypto or PyCrypto... Adobe Epubs can\'t be decrypted.'
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, 'calibre-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.
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()
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
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
raise ADEPTError('IneptEpub - Ultimately failed to decrypt')
return

View File

@@ -1,156 +0,0 @@
#!/usr/bin/env python
import sys
import zlib
import zipfile
import os
import os.path
import getopt
from struct import unpack
_FILENAME_LEN_OFFSET = 26
_EXTRA_LEN_OFFSET = 28
_FILENAME_OFFSET = 30
_MAX_SIZE = 64 * 1024
_MIMETYPE = 'application/epub+zip'
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 fixZip:
def __init__(self, zinput, zoutput):
self.ztype = 'zip'
if zinput.lower().find('.epub') >= 0 :
self.ztype = 'epub'
self.inzip = zipfile.ZipFile(zinput,'r')
self.outzip = zipfile.ZipFile(zoutput,'w')
# open the input zip for reading only as a raw file
self.bzf = file(zinput,'rb')
def getlocalname(self, zi):
local_header_offset = zi.header_offset
self.bzf.seek(local_header_offset + _FILENAME_LEN_OFFSET)
leninfo = self.bzf.read(2)
local_name_length, = unpack('<H', leninfo)
self.bzf.seek(local_header_offset + _FILENAME_OFFSET)
local_name = self.bzf.read(local_name_length)
return local_name
def uncompress(self, cmpdata):
dc = zlib.decompressobj(-15)
data = ''
while len(cmpdata) > 0:
if len(cmpdata) > _MAX_SIZE :
newdata = cmpdata[0:_MAX_SIZE]
cmpdata = cmpdata[_MAX_SIZE:]
else:
newdata = cmpdata
cmpdata = ''
newdata = dc.decompress(newdata)
unprocessed = dc.unconsumed_tail
if len(unprocessed) == 0:
newdata += dc.flush()
data += newdata
cmpdata += unprocessed
unprocessed = ''
return data
def getfiledata(self, zi):
# get file name length and exta data length to find start of file data
local_header_offset = zi.header_offset
self.bzf.seek(local_header_offset + _FILENAME_LEN_OFFSET)
leninfo = self.bzf.read(2)
local_name_length, = unpack('<H', leninfo)
self.bzf.seek(local_header_offset + _EXTRA_LEN_OFFSET)
exinfo = self.bzf.read(2)
extra_field_length, = unpack('<H', exinfo)
self.bzf.seek(local_header_offset + _FILENAME_OFFSET + local_name_length + extra_field_length)
data = None
# if not compressed we are good to go
if zi.compress_type == zipfile.ZIP_STORED:
data = self.bzf.read(zi.file_size)
# if compressed we must decompress it using zlib
if zi.compress_type == zipfile.ZIP_DEFLATED:
cmpdata = self.bzf.read(zi.compress_size)
data = self.uncompress(cmpdata)
return data
def fix(self):
# get the zipinfo for each member of the input archive
# and copy member over to output archive
# if problems exist with local vs central filename, fix them
# if epub write mimetype file first, with no compression
if self.ztype == 'epub':
nzinfo = ZipInfo('mimetype', compress_type=zipfile.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
nzinfo = zinfo
try:
data = self.inzip.read(zinfo.filename)
except zipfile.BadZipfile or zipfile.error:
local_name = self.getlocalname(zinfo)
data = self.getfiledata(zinfo)
nzinfo.filename = local_name
nzinfo.date_time = zinfo.date_time
nzinfo.compress_type = zinfo.compress_type
nzinfo.flag_bits = 0
nzinfo.internal_attr = 0
self.outzip.writestr(nzinfo,data)
self.bzf.close()
self.inzip.close()
self.outzip.close()
def usage():
print """usage: zipfix.py inputzip outputzip
inputzip is the source zipfile to fix
outputzip is the fixed zip archive
"""
def repairBook(infile, outfile):
if not os.path.exists(infile):
print "Error: Input Zip File does not exist"
return 1
try:
fr = fixZip(infile, outfile)
fr.fix()
return 0
except Exception, e:
print "Error Occurred ", e
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__' :
sys.exit(main())

Binary file not shown.

View File

@@ -1,346 +0,0 @@
#!/usr/bin/env python
"""
Retrieve Adobe ADEPT user key.
"""
from __future__ import with_statement
__license__ = 'GPL v3'
import sys
import os
import struct
from calibre.constants import iswindows, isosx
class ADEPTError(Exception):
pass
if iswindows:
from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
string_at, Structure, c_void_p, cast, c_size_t, memmove, CDLL, c_int, \
c_long, c_ulong
from ctypes.wintypes import LPVOID, DWORD, BOOL
import _winreg as winreg
def _load_crypto_libcrypto():
from ctypes.util import find_library
libcrypto = find_library('libeay32')
if libcrypto is None:
raise ADEPTError('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 ADEPTError('AES improper key used')
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
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_pycrypto, _load_crypto_libcrypto):
try:
AES = loader()
break
except (ImportError, ADEPTError):
pass
return AES
AES = _load_crypto()
DEVICE_KEY_PATH = r'Software\Adobe\Adept\Device'
PRIVATE_LICENCE_KEY_PATH = r'Software\Adobe\Adept\Activation'
MAX_PATH = 255
kernel32 = windll.kernel32
advapi32 = windll.advapi32
crypt32 = windll.crypt32
def GetSystemDirectory():
GetSystemDirectoryW = kernel32.GetSystemDirectoryW
GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint]
GetSystemDirectoryW.restype = c_uint
def GetSystemDirectory():
buffer = create_unicode_buffer(MAX_PATH + 1)
GetSystemDirectoryW(buffer, len(buffer))
return buffer.value
return GetSystemDirectory
GetSystemDirectory = GetSystemDirectory()
def GetVolumeSerialNumber():
GetVolumeInformationW = kernel32.GetVolumeInformationW
GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint,
POINTER(c_uint), POINTER(c_uint),
POINTER(c_uint), c_wchar_p, c_uint]
GetVolumeInformationW.restype = c_uint
def GetVolumeSerialNumber(path):
vsn = c_uint(0)
GetVolumeInformationW(
path, None, 0, byref(vsn), None, None, None, 0)
return vsn.value
return GetVolumeSerialNumber
GetVolumeSerialNumber = GetVolumeSerialNumber()
def GetUserName():
GetUserNameW = advapi32.GetUserNameW
GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
GetUserNameW.restype = c_uint
def GetUserName():
buffer = create_unicode_buffer(32)
size = c_uint(len(buffer))
while not GetUserNameW(buffer, byref(size)):
buffer = create_unicode_buffer(len(buffer) * 2)
size.value = len(buffer)
return buffer.value.encode('utf-16-le')[::2]
return GetUserName
GetUserName = GetUserName()
PAGE_EXECUTE_READWRITE = 0x40
MEM_COMMIT = 0x1000
MEM_RESERVE = 0x2000
def VirtualAlloc():
_VirtualAlloc = kernel32.VirtualAlloc
_VirtualAlloc.argtypes = [LPVOID, c_size_t, DWORD, DWORD]
_VirtualAlloc.restype = LPVOID
def VirtualAlloc(addr, size, alloctype=(MEM_COMMIT | MEM_RESERVE),
protect=PAGE_EXECUTE_READWRITE):
return _VirtualAlloc(addr, size, alloctype, protect)
return VirtualAlloc
VirtualAlloc = VirtualAlloc()
MEM_RELEASE = 0x8000
def VirtualFree():
_VirtualFree = kernel32.VirtualFree
_VirtualFree.argtypes = [LPVOID, c_size_t, DWORD]
_VirtualFree.restype = BOOL
def VirtualFree(addr, size=0, freetype=MEM_RELEASE):
return _VirtualFree(addr, size, freetype)
return VirtualFree
VirtualFree = VirtualFree()
class NativeFunction(object):
def __init__(self, restype, argtypes, insns):
self._buf = buf = VirtualAlloc(None, len(insns))
memmove(buf, insns, len(insns))
ftype = CFUNCTYPE(restype, *argtypes)
self._native = ftype(buf)
def __call__(self, *args):
return self._native(*args)
def __del__(self):
if self._buf is not None:
VirtualFree(self._buf)
self._buf = None
if struct.calcsize("P") == 4:
CPUID0_INSNS = (
"\x53" # push %ebx
"\x31\xc0" # xor %eax,%eax
"\x0f\xa2" # cpuid
"\x8b\x44\x24\x08" # mov 0x8(%esp),%eax
"\x89\x18" # mov %ebx,0x0(%eax)
"\x89\x50\x04" # mov %edx,0x4(%eax)
"\x89\x48\x08" # mov %ecx,0x8(%eax)
"\x5b" # pop %ebx
"\xc3" # ret
)
CPUID1_INSNS = (
"\x53" # push %ebx
"\x31\xc0" # xor %eax,%eax
"\x40" # inc %eax
"\x0f\xa2" # cpuid
"\x5b" # pop %ebx
"\xc3" # ret
)
else:
CPUID0_INSNS = (
"\x49\x89\xd8" # mov %rbx,%r8
"\x49\x89\xc9" # mov %rcx,%r9
"\x48\x31\xc0" # xor %rax,%rax
"\x0f\xa2" # cpuid
"\x4c\x89\xc8" # mov %r9,%rax
"\x89\x18" # mov %ebx,0x0(%rax)
"\x89\x50\x04" # mov %edx,0x4(%rax)
"\x89\x48\x08" # mov %ecx,0x8(%rax)
"\x4c\x89\xc3" # mov %r8,%rbx
"\xc3" # retq
)
CPUID1_INSNS = (
"\x53" # push %rbx
"\x48\x31\xc0" # xor %rax,%rax
"\x48\xff\xc0" # inc %rax
"\x0f\xa2" # cpuid
"\x5b" # pop %rbx
"\xc3" # retq
)
def cpuid0():
_cpuid0 = NativeFunction(None, [c_char_p], CPUID0_INSNS)
buf = create_string_buffer(12)
def cpuid0():
_cpuid0(buf)
return buf.raw
return cpuid0
cpuid0 = cpuid0()
cpuid1 = NativeFunction(c_uint, [], CPUID1_INSNS)
class DataBlob(Structure):
_fields_ = [('cbData', c_uint),
('pbData', c_void_p)]
DataBlob_p = POINTER(DataBlob)
def CryptUnprotectData():
_CryptUnprotectData = crypt32.CryptUnprotectData
_CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
c_void_p, c_void_p, c_uint, DataBlob_p]
_CryptUnprotectData.restype = c_uint
def CryptUnprotectData(indata, entropy):
indatab = create_string_buffer(indata)
indata = DataBlob(len(indata), cast(indatab, c_void_p))
entropyb = create_string_buffer(entropy)
entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
outdata = DataBlob()
if not _CryptUnprotectData(byref(indata), None, byref(entropy),
None, None, 0, byref(outdata)):
raise ADEPTError("Failed to decrypt user key key (sic)")
return string_at(outdata.pbData, outdata.cbData)
return CryptUnprotectData
CryptUnprotectData = CryptUnprotectData()
def retrieve_key():
if AES is None:
tkMessageBox.showerror(
"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] + '\\'
serial = GetVolumeSerialNumber(root)
vendor = cpuid0()
signature = struct.pack('>I', cpuid1())[1:]
user = GetUserName()
entropy = struct.pack('>I12s3s13s', serial, vendor, signature, user)
cuser = winreg.HKEY_CURRENT_USER
try:
regkey = winreg.OpenKey(cuser, DEVICE_KEY_PATH)
except WindowsError:
raise ADEPTError("Adobe Digital Editions not activated")
device = winreg.QueryValueEx(regkey, 'key')[0]
keykey = CryptUnprotectData(device, entropy)
userkey = None
try:
plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH)
except WindowsError:
raise ADEPTError("Could not locate ADE activation")
for i in xrange(0, 16):
try:
plkparent = winreg.OpenKey(plkroot, "%04d" % (i,))
except WindowsError:
break
ktype = winreg.QueryValueEx(plkparent, None)[0]
if ktype != 'credentials':
continue
for j in xrange(0, 16):
try:
plkkey = winreg.OpenKey(plkparent, "%04d" % (j,))
except WindowsError:
break
ktype = winreg.QueryValueEx(plkkey, None)[0]
if ktype != 'privateLicenseKey':
continue
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')
aes = AES(keykey)
userkey = aes.decrypt(userkey)
userkey = userkey[26:-ord(userkey[-1])]
return userkey
else:
import xml.etree.ElementTree as etree
import subprocess
NSMAP = {'adept': 'http://ns.adobe.com/adept',
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
def findActivationDat():
home = os.getenv('HOME')
cmdline = 'find "' + home + '/Library/Application Support/Adobe/Digital Editions" -name "activation.dat"'
cmdline = cmdline.encode(sys.getfilesystemencoding())
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
def retrieve_key():
actpath = findActivationDat()
if actpath is None:
raise ADEPTError("Could not locate ADE activation")
tree = etree.parse(actpath)
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
expr = '//%s/%s' % (adept('credentials'), adept('privateLicenseKey'))
userkey = tree.findtext(expr)
userkey = userkey.decode('base64')
userkey = userkey[26:]
return userkey

File diff suppressed because it is too large Load Diff

View File

@@ -1,369 +0,0 @@
#!/usr/bin/env python
from __future__ import with_statement
# 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
# It can run standalone to convert K4M/K4PC/Mobi files, or it can be installed as a
# plugin for Calibre (http://calibre-ebook.com/about) so that importing
# K4 or Mobi with DRM is no londer a multi-step process.
#
# ***NOTE*** If you are using this script as a calibre plugin for a K4M or K4PC ebook
# then calibre must be installed on the same machine and in the same account as K4PC or K4M
# for the plugin version to function properly.
#
# To create a Calibre plugin, rename this file so that the filename
# ends in '_plugin.py', put it into a ZIP file with all its supporting python routines
# and import that ZIP into Calibre using its plugin configuration GUI.
__version__ = '2.3'
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
import os, csv, getopt
import string
import binascii
import zlib
import re
import zlib, zipfile, tempfile, shutil
from struct import pack, unpack, unpack_from
class DrmException(Exception):
pass
if 'calibre' in sys.modules:
inCalibre = True
else:
inCalibre = False
def zipUpDir(myzip, tempdir,localname):
currentdir = tempdir
if localname != "":
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, tempdir, localfilePath)
# cleanup bytestring filenames
# borrowed from calibre from calibre/src/calibre/__init__.py
# added in removal of non-printing chars
# and removal of . at start
# convert spaces to underscores
def cleanup_name(name):
_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+/]')
substitute='_'
one = ''.join(char for char in name if char in string.printable)
one = _filename_sanitize.sub(substitute, one)
one = re.sub(r'\s', ' ', one).strip()
one = re.sub(r'^\.+$', '_', one)
one = one.replace('..', substitute)
# Windows doesn't like path components that end with a period
if one.endswith('.'):
one = one[:-1]+substitute
# Mac and Unix don't like file names that begin with a full stop
if len(one) > 0 and one[0] == '.':
one = substitute+one[1:]
one = one.replace(' ','_')
return one
def decryptBook(infile, outdir, k4, kInfoFiles, serials, pids):
import mobidedrm
import topazextract
import kgenpids
# handle the obvious cases at the beginning
if not os.path.isfile(infile):
print "Error: Input file does not exist"
return 1
mobi = True
magic3 = file(infile,'rb').read(3)
if magic3 == 'TPZ':
mobi = False
bookname = os.path.splitext(os.path.basename(infile))[0]
if mobi:
mb = mobidedrm.MobiBook(infile)
else:
tempdir = tempfile.mkdtemp()
mb = topazextract.TopazBook(infile, tempdir)
title = mb.getBookTitle()
print "Processing Book: ", title
filenametitle = cleanup_name(title)
outfilename = bookname
if len(bookname)>4 and len(filenametitle)>4 and bookname[:4] != filenametitle[:4]:
outfilename = outfilename + "_" + filenametitle
# build pid list
md1, md2 = mb.getPIDMetaInfo()
pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles)
try:
if mobi:
unlocked_file = mb.processBook(pidlst)
else:
mb.processBook(pidlst)
except mobidedrm.DrmException, e:
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
return 1
except Exception, e:
if not mobi:
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
print " Creating DeBug Full Zip Archive of Book"
zipname = os.path.join(outdir, bookname + '_debug' + '.zip')
myzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
zipUpDir(myzip, tempdir, '')
myzip.close()
shutil.rmtree(tempdir, True)
return 1
pass
if mobi:
outfile = os.path.join(outdir,outfilename + '_nodrm' + '.mobi')
file(outfile, 'wb').write(unlocked_file)
return 0
# topaz: build up zip archives of results
print " Creating HTML ZIP Archive"
zipname = os.path.join(outdir, outfilename + '_nodrm' + '.zip')
myzip1 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
myzip1.write(os.path.join(tempdir,'book.html'),'book.html')
myzip1.write(os.path.join(tempdir,'book.opf'),'book.opf')
if os.path.isfile(os.path.join(tempdir,'cover.jpg')):
myzip1.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg')
myzip1.write(os.path.join(tempdir,'style.css'),'style.css')
zipUpDir(myzip1, tempdir, 'img')
myzip1.close()
print " Creating SVG ZIP Archive"
zipname = os.path.join(outdir, outfilename + '_SVG' + '.zip')
myzip2 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
myzip2.write(os.path.join(tempdir,'index_svg.xhtml'),'index_svg.xhtml')
zipUpDir(myzip2, tempdir, 'svg')
zipUpDir(myzip2, tempdir, 'img')
myzip2.close()
print " Creating XML ZIP Archive"
zipname = os.path.join(outdir, outfilename + '_XML' + '.zip')
myzip3 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
targetdir = os.path.join(tempdir,'xml')
zipUpDir(myzip3, targetdir, '')
zipUpDir(myzip3, tempdir, 'img')
myzip3.close()
shutil.rmtree(tempdir, True)
return 0
def usage(progname):
print "Removes DRM protection from K4PC/M, Kindle, Mobi and Topaz ebooks"
print "Usage:"
print " %s [-k <kindle.info>] [-p <pidnums>] [-s <kindleSerialNumbers>] <infile> <outdir> " % progname
#
# Main
#
def main(argv=sys.argv):
progname = os.path.basename(argv[0])
k4 = False
kInfoFiles = []
serials = []
pids = []
print ('K4MobiDeDrm v%(__version__)s '
'provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc .' % globals())
print ' '
try:
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:")
except getopt.GetoptError, err:
print str(err)
usage(progname)
sys.exit(2)
if len(args)<2:
usage(progname)
sys.exit(2)
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
k4 = True
infile = args[0]
outdir = args[1]
return decryptBook(infile, outdir, k4, kInfoFiles, serials, pids)
if __name__ == '__main__':
sys.stdout=Unbuffered(sys.stdout)
sys.exit(main())
if not __name__ == "__main__" and inCalibre:
from calibre.customize import FileTypePlugin
class K4DeDRM(FileTypePlugin):
name = 'K4PC, K4Mac, Kindle Mobi and Topaz DeDRM' # Name of the plugin
description = 'Removes DRM from K4PC and Mac, Kindle Mobi and Topaz files. \
Provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc.'
supported_platforms = ['osx', 'windows', 'linux'] # Platforms this plugin will run on
author = 'DiapDealer, SomeUpdates' # The author of this plugin
version = (0, 2, 3) # The version number of this plugin
file_types = set(['prc','mobi','azw','azw1','tpz']) # The file types that this plugin will be applied to
on_import = True # Run this plugin during the import
priority = 210 # run this plugin before mobidedrm, k4pcdedrm, k4dedrm
def run(self, path_to_ebook):
from calibre.gui2 import is_ok_to_use_qt
from PyQt4.Qt import QMessageBox
from calibre.ptempfile import PersistentTemporaryDirectory
import kgenpids
import zlib
import zipfile
import topazextract
import mobidedrm
k4 = True
pids = []
serials = []
kInfoFiles = []
# Get supplied list of PIDs to try from plugin customization.
customvalues = self.site_customization.split(',')
for customvalue in customvalues:
customvalue = str(customvalue)
customvalue = customvalue.strip()
if len(customvalue) == 10 or len(customvalue) == 8:
pids.append(customvalue)
else :
if len(customvalue) == 16 and customvalue[0] == 'B':
serials.append(customvalue)
else:
print "%s is not a valid Kindle serial number or PID." % str(customvalue)
# Load any kindle info files (*.info) included Calibre's config directory.
try:
# Find Calibre's configuration directory.
confpath = os.path.split(os.path.split(self.plugin_path)[0])[0]
print 'K4MobiDeDRM: Calibre configuration directory = %s' % confpath
files = os.listdir(confpath)
filefilter = re.compile("\.info$", re.IGNORECASE)
files = filter(filefilter.search, files)
if files:
for filename in files:
fpath = os.path.join(confpath, filename)
kInfoFiles.append(fpath)
print 'K4MobiDeDRM: Kindle info file %s found in config folder.' % filename
except IOError:
print 'K4MobiDeDRM: Error reading kindle info files from config directory.'
pass
mobi = True
magic3 = file(path_to_ebook,'rb').read(3)
if magic3 == 'TPZ':
mobi = False
bookname = os.path.splitext(os.path.basename(path_to_ebook))[0]
if mobi:
mb = mobidedrm.MobiBook(path_to_ebook)
else:
tempdir = PersistentTemporaryDirectory()
mb = topazextract.TopazBook(path_to_ebook, tempdir)
title = mb.getBookTitle()
md1, md2 = mb.getPIDMetaInfo()
pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles)
try:
if mobi:
unlocked_file = mb.processBook(pidlst)
else:
mb.processBook(pidlst)
except mobidedrm.DrmException:
#if you reached here then no luck raise and exception
if is_ok_to_use_qt():
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "Error decoding: %s\n" % path_to_ebook)
d.show()
d.raise_()
d.exec_()
raise Exception("K4MobiDeDRM plugin could not decode the file")
return ""
except topazextract.TpzDRMError:
#if you reached here then no luck raise and exception
if is_ok_to_use_qt():
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "Error decoding: %s\n" % path_to_ebook)
d.show()
d.raise_()
d.exec_()
raise Exception("K4MobiDeDRM plugin could not decode the file")
return ""
print "Success!"
if mobi:
of = self.temporary_file(bookname+'.mobi')
of.write(unlocked_file)
of.close()
return of.name
# topaz: build up zip archives of results
print " Creating HTML ZIP Archive"
of = self.temporary_file(bookname + '.zip')
myzip = zipfile.ZipFile(of.name,'w',zipfile.ZIP_DEFLATED, False)
myzip.write(os.path.join(tempdir,'book.html'),'book.html')
myzip.write(os.path.join(tempdir,'book.opf'),'book.opf')
if os.path.isfile(os.path.join(tempdir,'cover.jpg')):
myzip.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg')
myzip.write(os.path.join(tempdir,'style.css'),'style.css')
zipUpDir(myzip, tempdir, 'img')
myzip.close()
return of.name
def customization_help(self, gui=False):
return 'Enter 10 character PIDs and/or Kindle serial numbers, separated by commas.'

View File

@@ -1,194 +0,0 @@
# standlone set of Mac OSX specific routines needed for K4DeDRM
from __future__ import with_statement
import sys
import os
import subprocess
class DrmException(Exception):
pass
# interface to needed routines in openssl's libcrypto
def _load_crypto_libcrypto():
from ctypes import CDLL, byref, POINTER, c_void_p, c_char_p, c_int, c_long, \
Structure, c_ulong, create_string_buffer, addressof, string_at, cast
from ctypes.util import find_library
libcrypto = find_library('crypto')
if libcrypto is None:
raise DrmException('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])
PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1',
[c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p])
class LibCrypto(object):
def __init__(self):
self._blocksize = 0
self._keyctx = None
self.iv = 0
def set_decrypt_key(self, userkey, iv):
self._blocksize = len(userkey)
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
raise DrmException('AES 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 DrmException('Failed to initialize AES key')
def decrypt(self, data):
out = create_string_buffer(len(data))
rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self.iv, 0)
if rv == 0:
raise DrmException('AES decryption failed')
return out.raw
def keyivgen(self, passwd):
salt = '16743'
saltlen = 5
passlen = len(passwd)
iter = 0x3e8
keylen = 80
out = create_string_buffer(keylen)
rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out)
return out.raw
return LibCrypto
def _load_crypto():
LibCrypto = None
try:
LibCrypto = _load_crypto_libcrypto()
except (ImportError, DrmException):
pass
return LibCrypto
LibCrypto = _load_crypto()
#
# Utility Routines
#
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM"
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
# uses a sub process to get the Hard Drive Serial Number using ioreg
# returns with the serial number of drive whose BSD Name is "disk0"
def GetVolumeSerialNumber():
sernum = os.getenv('MYSERIALNUMBER')
if sernum != None:
return sernum
cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver'
cmdline = cmdline.encode(sys.getfilesystemencoding())
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p.communicate()
reslst = out1.split('\n')
cnt = len(reslst)
bsdname = None
sernum = None
foundIt = False
for j in xrange(cnt):
resline = reslst[j]
pp = resline.find('"Serial Number" = "')
if pp >= 0:
sernum = resline[pp+19:-1]
sernum = sernum.strip()
bb = resline.find('"BSD Name" = "')
if bb >= 0:
bsdname = resline[bb+14:-1]
bsdname = bsdname.strip()
if (bsdname == 'disk0') and (sernum != None):
foundIt = True
break
if not foundIt:
sernum = '9999999999'
return sernum
# uses unix env to get username instead of using sysctlbyname
def GetUserName():
username = os.getenv('USER')
return username
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
import hashlib
def SHA256(message):
ctx = hashlib.sha256()
ctx.update(message)
return ctx.digest()
# implements an Pseudo Mac Version of Windows built-in Crypto routine
def CryptUnprotectData(encryptedData):
sp = GetVolumeSerialNumber() + '!@#' + GetUserName()
passwdData = encode(SHA256(sp),charMap1)
crp = LibCrypto()
key_iv = crp.keyivgen(passwdData)
key = key_iv[0:32]
iv = key_iv[32:48]
crp.set_decrypt_key(key,iv)
cleartext = crp.decrypt(encryptedData)
return cleartext
# Locate and open the .kindle-info file
def openKindleInfo(kInfoFile=None):
if kInfoFile == None:
home = os.getenv('HOME')
cmdline = 'find "' + home + '/Library/Application Support" -name ".kindle-info"'
cmdline = cmdline.encode(sys.getfilesystemencoding())
p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p1.communicate()
reslst = out1.split('\n')
kinfopath = 'NONE'
cnt = len(reslst)
for j in xrange(cnt):
resline = reslst[j]
pp = resline.find('.kindle-info')
if pp >= 0:
kinfopath = resline
break
if not os.path.isfile(kinfopath):
raise DrmException('Error: .kindle-info file can not be found')
return open(kinfopath,'r')
else:
if not os.path.isfile(kInfoFile):
raise DrmException('Error: kindle-info file can not be found')
return open(kInfoFile, 'r')

View File

@@ -1,110 +0,0 @@
# K4PC Windows specific routines
from __future__ import with_statement
import sys, os
from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
string_at, Structure, c_void_p, cast
import _winreg as winreg
import traceback
MAX_PATH = 255
kernel32 = windll.kernel32
advapi32 = windll.advapi32
crypt32 = windll.crypt32
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_"
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
class DrmException(Exception):
pass
class DataBlob(Structure):
_fields_ = [('cbData', c_uint),
('pbData', c_void_p)]
DataBlob_p = POINTER(DataBlob)
def GetSystemDirectory():
GetSystemDirectoryW = kernel32.GetSystemDirectoryW
GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint]
GetSystemDirectoryW.restype = c_uint
def GetSystemDirectory():
buffer = create_unicode_buffer(MAX_PATH + 1)
GetSystemDirectoryW(buffer, len(buffer))
return buffer.value
return GetSystemDirectory
GetSystemDirectory = GetSystemDirectory()
def GetVolumeSerialNumber():
GetVolumeInformationW = kernel32.GetVolumeInformationW
GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint,
POINTER(c_uint), POINTER(c_uint),
POINTER(c_uint), c_wchar_p, c_uint]
GetVolumeInformationW.restype = c_uint
def GetVolumeSerialNumber(path = GetSystemDirectory().split('\\')[0] + '\\'):
vsn = c_uint(0)
GetVolumeInformationW(path, None, 0, byref(vsn), None, None, None, 0)
return str(vsn.value)
return GetVolumeSerialNumber
GetVolumeSerialNumber = GetVolumeSerialNumber()
def GetUserName():
GetUserNameW = advapi32.GetUserNameW
GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
GetUserNameW.restype = c_uint
def GetUserName():
buffer = create_unicode_buffer(32)
size = c_uint(len(buffer))
while not GetUserNameW(buffer, byref(size)):
buffer = create_unicode_buffer(len(buffer) * 2)
size.value = len(buffer)
return buffer.value.encode('utf-16-le')[::2]
return GetUserName
GetUserName = GetUserName()
def CryptUnprotectData():
_CryptUnprotectData = crypt32.CryptUnprotectData
_CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
c_void_p, c_void_p, c_uint, DataBlob_p]
_CryptUnprotectData.restype = c_uint
def CryptUnprotectData(indata, entropy):
indatab = create_string_buffer(indata)
indata = DataBlob(len(indata), cast(indatab, c_void_p))
entropyb = create_string_buffer(entropy)
entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
outdata = DataBlob()
if not _CryptUnprotectData(byref(indata), None, byref(entropy),
None, None, 0, byref(outdata)):
raise DrmException("Failed to Unprotect Data")
return string_at(outdata.pbData, outdata.cbData)
return CryptUnprotectData
CryptUnprotectData = CryptUnprotectData()
#
# Locate and open the Kindle.info file.
#
def openKindleInfo(kInfoFile=None):
if kInfoFile == None:
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info'
if not os.path.isfile(kinfopath):
raise DrmException('Error: kindle.info file can not be found')
return open(kinfopath,'r')
else:
if not os.path.isfile(kInfoFile):
raise DrmException('Error: kindle.info file can not be found')
return open(kInfoFile, 'r')

View File

@@ -0,0 +1,76 @@
{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fnil\fcharset0 Verdana;\f2\fnil\fcharset128 HiraKakuProN-W3;
}
{\colortbl;\red255\green255\blue255;}
\paperw11900\paperh16840\margl1440\margr1440\vieww18160\viewh16520\viewkind0
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\qc
\f0\b\fs24 \cf0 DeDRM ReadMe
\b0 \
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural
\cf0 \
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\qj
\cf0 DeDRM is an application that packs all of the python dm-removal software into one easy to use program that remembers preferences and settings.\
It works without manual configuration with Kindle for Mac ebooks and Adobe Adept (including nook) ePub and PDF ebooks.\
\
To remove the DRM of Kindle ebooks from eInk Kindles, eReader pdb ebooks, Barnes & Noble ePubs, or Mobipocket ebooks, you must first run DeDRM application (by double-clicking it) and set some additional Preferences including:\
\
eInk Kindle (not Kindle Fire): 16 digit Serial Number\
Barnes & Noble ePub: Name and CC number or key file (bnepubkey.b64)\
eReader: Name and last 8 digits of CC number\
Mobipocket: 10 digit PID\
\
A final preference is the destination folder for the DRM-free copies of your ebooks that the application produces. This can be either the same folder as the original ebook, or a folder of your choice.\
\
Once these preferences have been set, you can drag and drop ebooks (or folders of ebooks) onto the DeDRM droplet to remove the DRM.\
\
This program requires Mac OS X 10.4 or above. It will not work on Mac OS X 10.3 or earlier.\
\
\
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural
\b \cf0 Installation
\b0 \
Mac OS X 10.4
\i only
\i0 : You
\i must
\i0 first install Python 2.7.3 from http://python.org/. At the time of writing, the direct download link is http://www.python.org/ftp/python/2.7.3/python-2.7.3-macosx10.3.dmg.\
\
Mac OS X 10.5 and above: You do
\i not
\i0 need to install Python.\
\
Drag the DeDRM application from from the DeDRM_Application_Macintosh folder (the location of this ReadMe) to your Applications folder, or anywhere else you find convenient.\
\
\
\b Use
\b0 \
1. To set the preferences, double-click the application and follow the instructions in the dialogs.\
2. Drag & Drop DRMed ebooks or folders of DRMed ebooks onto the application icon when it is not running.\
\
\
\b Troubleshooting\
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural
\b0 \cf0 A log is created on your desktop (DeDRM.log) containing detailed information from all the scripts. If you have any problems decrypting your ebooks, copy the contents of this log in a comment at Apprentice Alf's blog.\
http://apprenticealf.wordpress.com/\
\
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural
\b \cf0 Credits\
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural
\b0 \cf0 The mobidedrm and erdr2pml scripts were created by The Dark Reverser\
The i
\f1 \CocoaLigature0 gnobleepub, ignoblekeygen, ineptepub and adobe key scripts were created by
\f0 \CocoaLigature1 i
\f2 \CocoaLigature0 \uc0\u9829
\f1 cabbages\
The k4mobidedrm script and supporting scripts were written by some_updates with help from DiapDealer and Apprentice Alf, based on code by Bart Simpson (aka Skindle), CMBDTC and clarknova \
The alfcrypto library was created by some_updates\
The ePub encryption detection script was adapted by Apprentice Alf from a script by Paul Durrant\
The DeDRM all-in-one AppleScript was created by Apprentice Alf\
}

View File

@@ -24,9 +24,11 @@
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>droplet</string> <string>droplet</string>
<key>CFBundleGetInfoString</key> <key>CFBundleGetInfoString</key>
<string>DeDRM 2.2, Copyright © 20102011 by Apprentice Alf and others.</string> <string>DeDRM AppleScript 6.0.5. Written 20102013 by Apprentice Alf and others.</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string>droplet</string> <string>DeDRM</string>
<key>CFBundleIdentifier</key>
<string>com.apple.ScriptEditor.id.707CCCD5-0C6C-4BEB-B67C-B6E866ADE85A</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
@@ -34,11 +36,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2.2</string> <string>6.0.5</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>dplt</string> <string>dplt</string>
<key>LSMinimumSystemVersion</key>
<string>10.5.0</string>
<key>LSRequiresCarbon</key> <key>LSRequiresCarbon</key>
<true/> <true/>
<key>WindowState</key> <key>WindowState</key>
@@ -52,7 +52,7 @@
<key>positionOfDivider</key> <key>positionOfDivider</key>
<real>0.0</real> <real>0.0</real>
<key>savedFrame</key> <key>savedFrame</key>
<string>1578 27 862 788 1440 -150 1680 1050 </string> <string>85 100 1002 753 0 0 1440 878 </string>
<key>selectedTabView</key> <key>selectedTabView</key>
<string>event log</string> <string>event log</string>
</dict> </dict>

View File

@@ -2,10 +2,16 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>BuildMachineOSBuild</key>
<string>10K549</string>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>English</string> <string>English</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>DeDRM Progress</string> <string>DeDRM Progress</string>
<key>CFBundleGetInfoString</key>
<string>DeDRM Progress 1.1, Written 2010, 2012 by Apprentice Alf and others.</string>
<key>CFBundleIconFile</key>
<string>DeDRM Progress</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>com.apprenticealf.DeDRMProgress</string> <string>com.apprenticealf.DeDRMProgress</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
@@ -15,11 +21,25 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.0</string> <string>1.1</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0</string> <string>1.0</string>
<key>DTCompiler</key>
<string>4.0</string>
<key>DTPlatformBuild</key>
<string>10M2518</string>
<key>DTPlatformVersion</key>
<string>PG</string>
<key>DTSDKBuild</key>
<string>8S2167</string>
<key>DTSDKName</key>
<string>macosx10.4</string>
<key>DTXcode</key>
<string>0400</string>
<key>DTXcodeBuild</key>
<string>10M2518</string>
<key>NSAppleScriptEnabled</key> <key>NSAppleScriptEnabled</key>
<string>YES</string> <string>YES</string>
<key>NSMainNibFile</key> <key>NSMainNibFile</key>

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IBClasses</key>
<array>
<dict>
<key>CLASS</key>
<string>FirstResponder</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>SUPERCLASS</key>
<string>NSObject</string>
</dict>
</array>
<key>IBVersion</key>
<string>1</string>
</dict>
</plist>

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IBFramework Version</key>
<string>680</string>
<key>IBLastKnownRelativeProjectPath</key>
<string>../Display Panel.xcodeproj</string>
<key>IBOldestOS</key>
<integer>5</integer>
<key>IBOpenObjects</key>
<array/>
<key>IBSystem Version</key>
<string>9L31a</string>
<key>targetFramework</key>
<string>IBCocoaFramework</string>
</dict>
</plist>

View File

@@ -0,0 +1,60 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Managing Adobe Digital Editions Keys</title>
<style type="text/css">
span.version {font-size: 50%}
span.bold {font-weight: bold}
h3 {margin-bottom: 0}
p {margin-top: 0}
li {margin-top: 0.5em}
</style>
</head>
<body>
<h1>Managing Adobe Digital Editions Keys</h1>
<p>If you have upgraded from an earlier version of the plugin, any existing Adobe Digital Editions keys will have been automatically imported, so you might not need to do any more configuration. In addition, on Windows and Mac, the default Adobe Digital Editions key is added the first time the plugin is run. Continue reading for key generation and management instructions.</p>
<h3>Creating New Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog prompting you to enter a key name for the default Adobe Digital Editions key. </p>
<ul>
<li><span class="bold">Unique Key Name:</span> this is a unique name you choose to help you identify the key. This name will show in the list of configured keys.</li>
</ul>
<p>Click the OK button to create and store the Adobe Digital Editions key for the current installation of Adobe Digital Editions. Or Cancel if you dont want to create the key.</p>
<p>New keys are checked against the current list of keys before being added, and duplicates are discarded.</p>
<h3>Deleting Keys:</h3>
<p>On the right-hand side of the plugins 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 thats what you truly mean to do. Once gone, its permanently gone.</p>
<h3>Renaming Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will promt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</p>
<h3>Exporting Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a computers hard-drive. Use this button to export the highlighted key to a file (with a .der file name extension). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.</p>
<h3>Linux Users: WINEPREFIX</h3>
<p>Under the list of keys, Linux users will see a text field labeled "WINEPREFIX". If you are use Adobe Digital Editions under Wine, and your wine installation containing Adobe Digital Editions isn't the default Wine installation, you may enter the full path to the correct Wine installation here. Leave blank if you are unsure.</p>
<h3>Importing Existing Keyfiles:</h3>
<p>At the bottom-left of the plugins customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing .der key files. Key files might come from being exported from this or older plugins, or may have been generated using the adobekey.pyw script running under Wine on Linux systems.</p>
<p>Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes will only be saved permanently when you click OK in the main configuration dialog.</p>
</body>
</html>

View File

@@ -0,0 +1,57 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Managing Barnes and Noble Keys</title>
<style type="text/css">
span.version {font-size: 50%}
span.bold {font-weight: bold}
h3 {margin-bottom: 0}
p {margin-top: 0}
li {margin-top: 0.5em}
</style>
</head>
<body>
<h1>Managing Barnes and Noble Keys</h1>
<p>If you have upgraded from an earlier version of the plugin, any existing Barnes and Noble keys will have been automatically imported, so you might not need to do any more configuration. Continue reading for key generation and management instructions.</p>
<h3>Creating New Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering the necessary data to generate a new key.</p>
<ul>
<li><span class="bold">Unique Key Name:</span> this is a unique name you choose to help you identify the key. This name will show in the list of configured keys. Choose something that will help you remember the data (name, cc#) it was created with.</li>
<li><span class="bold">Your Name:</span> This is the name used by Barnes and Noble to generate your encryption key. Seemingly at random, Barnes and Noble choose one of three places from which to take this name. Most commonly, its your name as set in your Barnes &amp; Noble account, My Account page, directly under PERSONAL INFORMATION. Sometimes it is the the name used in the default shipping address, and sometimes its the name listed for the active credit card. If these names are different in your Barnes and Noble account preferences, I suggest creating one key for each version of your name. This name will not be stored anywhere on your computer or in calibre. It will only be used in the creation of the one-way hash/key thats stored in the preferences.</li>
<li><span class="bold">Credit Card#:</span> this is the default credit card number that was on file with Barnes and Noble at the time of download of the ebook to be de-DRMed. Just enter the 16 (15 for American Express) digits. As with the name, this number will not be stored anywhere on your computer or in calibre. It will only be used in the creation of the one-way hash/key thats stored in the preferences.</li>
</ul>
<p>Click the OK button to create and store the generated key. Or Cancel if you dont want to create a key.</p>
<p>New keys are checked against the current list of keys before being added, and duplicates are discarded.</p>
<h3>Deleting Keys:</h3>
<p>On the right-hand side of the plugins 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 thats what you truly mean to do. Once gone, its permanently gone.</p>
<h3>Renaming Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will promt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</p>
<h3>Exporting Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a computers hard-drive. Use this button to export the highlighted key to a file (with a .b64 file name extension). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.</p>
<h3>Importing Existing Keyfiles:</h3>
<p>At the bottom-left of the plugins customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing .b64 key files. Key files might come from being exported from this or older plugins, or may have been generated using the original i♥cabbages script.</p>
<p>Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.</p>
</body>
</html>

View File

@@ -0,0 +1,43 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Managing eInk Kindle serial numbers</title>
<style type="text/css">
span.version {font-size: 50%}
span.bold {font-weight: bold}
h3 {margin-bottom: 0}
p {margin-top: 0}
li {margin-top: 0.5em}
</style>
</head>
<body>
<h1>Managing eInk Kindle serial numbers</h1>
<p>If you have upgraded from an earlier version of the plugin, any existing eInk Kindle serial numbers will have been automatically imported, so you might not need to do any more configuration.</p>
<p>Please note that Kindle serial numbers are only valid keys for eInk Kindles like the Kindle Touch and PaperWhite. The Kindle Fire and Fire HD do not use their serial number for DRM and it is useless to enter those serial numbers.</p>
<h3>Creating New Kindle serial numbers:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering a new Kindle serial number.</p>
<ul>
<li><span class="bold">Eink Kindle Serial Number:</span> this is the unique serial number of your device. It usually starts with a B or a 9 and is sixteen characters long. For a reference of where to find serial numbers and their ranges, please refere to this <a href="http://wiki.mobileread.com/wiki/Kindle_serial_numbers">mobileread wiki page.</a></li>
</ul>
<p>Click the OK button to save the serial number. Or Cancel if you didnt want to enter a serial number.</p>
<h3>Deleting Kindle serial numbers:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted Kindle serial number from the list. You will be prompted once to be sure thats what you truly mean to do. Once gone, its permanently gone.</p>
<p>Once done creating/deleting serial numbers, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.</p>
</body>
</html>

View File

@@ -0,0 +1,73 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>DeDRM Plugin Configuration</title>
<style type="text/css">
span.version {font-size: 50%}
span.bold {font-weight: bold}
h3 {margin-bottom: 0}
p {margin-top: 0}
</style>
</head>
<body>
<h1>DeDRM Plugin <span class="version">(v6.0.0)</span></h1>
<p>This plugin removes DRM from ebooks when they are imported into calibre. If you already have DRMed ebooks in your calibre library, you will need to remove them and import them again.</p>
<h3>Installation</h3>
<p>You have obviously managed to install the plugin, as otherwise you wouldnt be reading this help file. However, you should also delete any older DeDRM plugins, as this DeDRM plugin replaces the five older plugins: Kindle and Mobipocket DeDRM (K4MobiDeDRM), Ignoble Epub DeDRM (ignobleepub), Inept Epub DeDRM (ineptepub), Inept PDF DeDRM (ineptepub) and eReader PDB 2 PML (eReaderPDB2PML).</p>
<h3>Configuration</h3>
<p>On Windows and Mac, the keys for ebooks downloaded for Kindle for Mac/PC and Adobe Digital Editions are automatically generated. If all your DRMed ebooks can be opened and read in Kindle for Mac/PC and/or Adobe Digital Editions on the same computer on which you are running calibre, you do not need to do any configuration of this plugin. On Linux, keys for Kindle for PC and Adobe Digital Editions need to be generated separately (see the Linux section below)</p>
<p>If you have other DRMed ebooks, you will need to enter extra configuration information. The buttons in this dialog will open individual configuration dialogs that will allow you to enter the needed information, depending on the type and source of your DRMed eBooks. Additional help on the information required is available in each of the the dialogs.</p>
<p>If you have used previous versions of the various DeDRM plugins on this machine, you may find that some of the configuration dialogs already contain the information you entered through those previous plugins.</p>
<p>When you have finished entering your configuration information, you <em>must</em> click the OK button to save it. If you click the Cancel button, all your changes in all the configuration dialogs will be lost.</p>
<h3>Troubleshooting:</h3>
<p >If you find that its not working for you , you can save a lot of time by trying to add the ebook to Calibre in debug mode. This will print out a lot of helpful info that can be copied into any online help requests.</p>
<p>Open a command prompt (terminal window) and type "calibre-debug -g" (without the quotes). Calibre will launch, and you can can add the problem ebook the usual way. The debug info will be output to the original command prompt (terminal window). Copy the resulting output and paste it into the comment you make at my blog.</p>
<p><span class="bold">Note:</span> The Mac version of Calibre doesnt install the command line tools by default. If you go to the Preferences page and click on the miscellaneous button, youll find the option to install the command line tools.</p>
<h3>Credits:</h3>
<ul>
<li>The Dark Reverser for the Mobipocket and eReader scripts</li>
<li>i♥cabbages for the Adobe Digital Editions scripts</li>
<li>Skindle aka Bart Simpson for the Amazon Kindle for PC script</li>
<li>CMBDTC for Amazon Topaz DRM removal script</li>
<li>some_updates, clarknova and Bart Simpson for Amazon Topaz conversion scripts</li>
<li>DiapDealer for the first calibre plugin versions of the tools</li>
<li>some_updates, DiapDealer, Apprentice Alf and mdlnx for Amazon Kindle/Mobipocket tools</li>
<li>some_updates for the DeDRM all-in-one Python tool</li>
<li>Apprentice Alf for the DeDRM all-in-one AppleScript tool</li>
<li>Apprentice Alf for the DeDRM all-in-one calibre plugin</li>
<li>And probably many more.</li>
</ul>
<h3> For additional help read the <a href="http://apprenticealf.wordpress.com/2011/01/17/frequently-asked-questions-about-the-drm-removal-tools/">FAQs</a> at <a href="http://apprenticealf.wordpress.com">Apprentice Alfs Blog</a> and ask questions in the comments section of the <a href="http://apprenticealf.wordpress.com/2012/09/10/drm-removal-tools-for-ebooks/">first post</a>.</h3>
<h2>Linux Systems Only</h2>
<h3>Generating decryption keys for Adobe Digital Editions and Kindle for PC</h3>
<p>If you install Kindle for PC and/or Adobe Digital Editions in Wine, you will be able to download DRMed ebooks to them under Wine. To be able to remove the DRM, you will need to generate key files and add them in the plugin's customisation dialogs.</p>
<p>To generate the key files you will need to install Python and PyCrypto under the same Wine setup as your Kindle for PC and/or Adobe Digital Editions installations. (Kindle for PC, Python and Pycrypto installation instructions in the ReadMe.)</p>
<p>Once everything's installed under Wine, you'll need to run the adobekey.pyw script (for Adobe Digital Editions) and kindlekey.pyw (For Kindle for PC) using the python installation in your Wine system. The scripts can be found in Other_Tools/Key_Retrieval_Scripts.</p>
<p>Each script will create a key file in the same folder as the script. Copy the key files to your Linux system and then load the key files using the Adobe Digital Editions ebooks dialog and the Kindle for Mac/PC ebooks dialog.</p>
</body>
</html>

View File

@@ -0,0 +1,59 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Managing Kindle for Mac/PC Keys</title>
<style type="text/css">
span.version {font-size: 50%}
span.bold {font-weight: bold}
h3 {margin-bottom: 0}
p {margin-top: 0}
li {margin-top: 0.5em}
</style>
</head>
<body>
<h1>Managing Kindle for Mac/PC Keys</h1>
<p>If you have upgraded from an earlier version of the plugin, any existing Kindle for Mac/PC keys will have been automatically imported, so you might not need to do any more configuration. In addition, on Windows and Mac, the default Kindle for Mac/PC key is added the first time the plugin is run. Continue reading for key generation and management instructions.</p>
<h3>Creating New Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog prompting you to enter a key name for the default Kindle for Mac/PC key. </p>
<ul>
<li><span class="bold">Unique Key Name:</span> this is a unique name you choose to help you identify the key. This name will show in the list of configured keys.</li>
</ul>
<p>Click the OK button to create and store the Kindle for Mac/PC key for the current installation of Kindle for Mac/PC. Or Cancel if you dont want to create the key.</p>
<p>New keys are checked against the current list of keys before being added, and duplicates are discarded.</p>
<h3>Deleting Keys:</h3>
<p>On the right-hand side of the plugins 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 thats what you truly mean to do. Once gone, its permanently gone.</p>
<h3>Renaming Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will promt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</p>
<h3>Exporting Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a computers hard-drive. Use this button to export the highlighted key to a file (with a .der file name extension). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.</p>
<h3>Linux Users: WINEPREFIX</h3>
<p>Under the list of keys, Linux users will see a text field labeled "WINEPREFIX". If you are use Kindle for PC under Wine, and your wine installation containing Kindle for PC isn't the default Wine installation, you may enter the full path to the correct Wine installation here. Leave blank if you are unsure.</p>
<h3>Importing Existing Keyfiles:</h3>
<p>At the bottom-left of the plugins customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing .k4i key files. Key files might come from being exported from this plugin, or may have been generated using the kindlekey.pyw script running under Wine on Linux systems.</p>
<p>Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.</p>
</body>
</html>

View File

@@ -0,0 +1,42 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Managing Mobipocket PIDs</title>
<style type="text/css">
span.version {font-size: 50%}
span.bold {font-weight: bold}
h3 {margin-bottom: 0}
p {margin-top: 0}
li {margin-top: 0.5em}
</style>
</head>
<body>
<h1>Managing Mobipocket PIDs</h1>
<p>If you have upgraded from an earlier version of the plugin, any existing Mobipocket PIDs will have been automatically imported, so you might not need to do any more configuration.</p>
<h3>Creating New Mobipocket PIDs:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering a new Mobipocket PID.</p>
<ul>
<li><span class="bold">PID:</span> this is a PID used to decrypt your Mobipocket ebooks. It is eight or ten characters long. Mobipocket PIDs are usualy displayed in the About screen of your Mobipocket device.</li>
</ul>
<p>Click the OK button to save the PID. Or Cancel if you didnt want to enter a PID.</p>
<h3>Deleting Mobipocket PIDs:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted Mobipocket PID from the list. You will be prompted once to be sure thats what you truly mean to do. Once gone, its permanently gone.</p>
<p>Once done creating/deleting PIDs, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.</p>
</body>
</html>

View File

@@ -0,0 +1,56 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Managing eReader Keys</title>
<style type="text/css">
span.version {font-size: 50%}
span.bold {font-weight: bold}
h3 {margin-bottom: 0}
p {margin-top: 0}
li {margin-top: 0.5em}
</style>
</head>
<body>
<h1>Managing eReader Keys</h1>
<p>If you have upgraded from an earlier version of the plugin, any existing eReader (Fictionwise .pdb) keys will have been automatically imported, so you might not need to do any more configuration. Continue reading for key generation and management instructions.</p>
<h3>Creating New Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering the necessary data to generate a new key.</p>
<ul>
<li><span class="bold">Unique Key Name:</span> this is a unique name you choose to help you identify the key. This name will show in the list of configured keys. Choose something that will help you remember the data (name, cc#) it was created with.</li>
<li><span class="bold">Your Name:</span> This is the name used by Fictionwise to generate your encryption key. Since Fictionwise has now closed down, you might not have easy access to this. It was often the name on the Credit Card used at Fictionwise.</li>
<li><span class="bold">Credit Card#:</span> this is the default credit card number that was on file with Fictionwise at the time of download of the ebook to be de-DRMed. Just enter the last 8 digits of the number. As with the name, this number will not be stored anywhere on your computer or in calibre. It will only be used in the creation of the one-way hash/key thats stored in the preferences.</li>
</ul>
<p>Click the OK button to create and store the generated key. Or Cancel if you dont want to create a key.</p>
<p>New keys are checked against the current list of keys before being added, and duplicates are discarded.</p>
<h3>Deleting Keys:</h3>
<p>On the right-hand side of the plugins 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 thats what you truly mean to do. Once gone, its permanently gone.</p>
<h3>Renaming Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will promt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</p>
<h3>Exporting Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a computers hard-drive. Use this button to export the highlighted key to a file (with a .b63 file name extension). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.</p>
<h3>Importing Existing Keyfiles:</h3>
<p>At the bottom-left of the plugins customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing .b63 key files that have previously been exported.</p>
<p>Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.</p>
</body>
</html>

View File

@@ -0,0 +1,491 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import with_statement
__license__ = 'GPL v3'
__docformat__ = 'restructuredtext en'
# Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/>
#
# Requires Calibre version 0.7.55 or higher.
#
# All credit given to i♥cabbages and The Dark Reverser for the original standalone scripts.
# We had the much easier job of converting them to a calibre plugin.
#
# This plugin is meant to decrypt eReader PDBs, Adobe Adept ePubs, Barnes & Noble ePubs,
# Adobe Adept PDFs, Amazon Kindle and Mobipocket files without having to
# install any dependencies... other than having calibre installed, of course.
#
# Configuration:
# Check out the plugin's configuration settings by clicking the "Customize plugin"
# button when you have the "DeDRM" plugin highlighted (under Preferences->
# Plugins->File type plugins). Once you have the configuration dialog open, you'll
# see a Help link on the top right-hand side.
#
# Revision history:
# 6.0.0 - Initial release
# 6.0.1 - Bug Fixes for Windows App, Kindle for Mac and Windows Adobe Digital Editions
# 6.0.2 - Restored call to Wine to get Kindle for PC keys
"""
Decrypt DRMed ebooks.
"""
PLUGIN_NAME = u"DeDRM"
PLUGIN_VERSION_TUPLE = (6, 0, 5)
PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE])
# Include an html helpfile in the plugin's zipfile with the following name.
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
import sys, os, re
import time
import zipfile
import traceback
from zipfile import ZipFile
class DeDRMError(Exception):
pass
from calibre.customize import FileTypePlugin
from calibre.constants import iswindows, isosx
from calibre.gui2 import is_ok_to_use_qt
from calibre.utils.config import config_dir
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get safely
# encoded using "replace" before writing them.
class SafeUnbuffered:
def __init__(self, stream):
self.stream = stream
self.encoding = stream.encoding
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
class DeDRM(FileTypePlugin):
name = PLUGIN_NAME
description = u"Removes DRM from Amazon Kindle, Adobe Adept (including Kobo), Barnes & Noble, Mobipocket and eReader ebooks. Credit given to i♥cabbages and The Dark Reverser for the original stand-alone scripts."
supported_platforms = ['linux', 'osx', 'windows']
author = u"DiapDealer, Apprentice Alf, The Dark Reverser and i♥cabbages"
version = PLUGIN_VERSION_TUPLE
minimum_calibre_version = (0, 7, 55) # Compiled python libraries cannot be imported in earlier versions.
file_types = set(['epub','pdf','pdb','prc','mobi','azw','azw1','azw3','azw4','tpz'])
on_import = True
priority = 600
def initialize(self):
# convert old preferences, if necessary.
try:
from calibre_plugins.dedrm.prefs import convertprefs
convertprefs()
except:
traceback.print_exc()
"""
Dynamic modules can't be imported/loaded from a zipfile... so this routine
runs whenever the plugin gets initialized. This will extract the appropriate
library for the target OS and copy it to the 'alfcrypto' subdirectory of
calibre's configuration directory. That 'alfcrypto' directory is then
inserted into the syspath (as the very first entry) in the run function
so the CDLL stuff will work in the alfcrypto.py script.
"""
try:
if iswindows:
names = [u"alfcrypto.dll",u"alfcrypto64.dll"]
elif isosx:
names = [u"libalfcrypto.dylib"]
else:
names = [u"libalfcrypto32.so",u"libalfcrypto64.so",u"kindlekey.py",u"adobekey.py",u"subasyncio.py"]
lib_dict = self.load_resources(names)
self.pluginsdir = os.path.join(config_dir,u"plugins")
if not os.path.exists(self.pluginsdir):
os.mkdir(self.pluginsdir)
self.maindir = os.path.join(self.pluginsdir,u"DeDRM")
if not os.path.exists(self.maindir):
os.mkdir(self.maindir)
self.helpdir = os.path.join(self.maindir,u"help")
if not os.path.exists(self.helpdir):
os.mkdir(self.helpdir)
self.alfdir = os.path.join(self.maindir,u"libraryfiles")
if not os.path.exists(self.alfdir):
os.mkdir(self.alfdir)
for entry, data in lib_dict.items():
file_path = os.path.join(self.alfdir, entry)
open(file_path,'wb').write(data)
except Exception, e:
traceback.print_exc()
raise
def ePubDecrypt(self,path_to_ebook):
# Create a TemporaryPersistent file to work with.
# Check original epub archive for zip errors.
import calibre_plugins.dedrm.zipfix
inf = self.temporary_file(u".epub")
try:
print u"{0} v{1}: Verifying zip archive integrity".format(PLUGIN_NAME, PLUGIN_VERSION)
fr = zipfix.fixZip(path_to_ebook, inf.name)
fr.fix()
except Exception, e:
print u"{0} v{1}: Error \'{2}\' when checking zip archive".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0])
raise Exception(e)
# import the decryption keys
import calibre_plugins.dedrm.prefs as prefs
dedrmprefs = prefs.DeDRM_Prefs()
# import the Barnes & Noble ePub handler
import calibre_plugins.dedrm.ignobleepub as ignobleepub
#check the book
if ignobleepub.ignobleBook(inf.name):
print u"{0} v{1}: “{2}” is a secure Barnes & Noble ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))
# Attempt to decrypt epub with each encryption key (generated or provided).
for keyname, userkey in dedrmprefs['bandnkeys'].items():
keyname_masked = u"".join((u'X' if (x.isdigit()) else x) for x in keyname)
print u"{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname_masked)
of = self.temporary_file(u".epub")
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
result = ignobleepub.decryptBook(userkey, inf.name, of.name)
of.close()
if result == 0:
# Decryption was successful.
# Return the modified PersistentTemporary file to calibre.
return of.name
print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime)
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt “{2}” after {3:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook),time.time()-self.starttime))
# import the Adobe Adept ePub handler
import calibre_plugins.dedrm.ineptepub as ineptepub
if ineptepub.adeptBook(inf.name):
print u"{0} v{1}: {2} is a secure Adobe Adept ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))
# Attempt to decrypt epub with each encryption key (generated or provided).
for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
userkey = userkeyhex.decode('hex')
print u"{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname)
of = self.temporary_file(u".epub")
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
try:
result = ineptepub.decryptBook(userkey, inf.name, of.name)
except:
result = 1
of.close()
if result == 0:
# Decryption was successful.
# Return the modified PersistentTemporary file to calibre.
return of.name
print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime)
# perhaps we need to get a new default ADE key
print u"{0} v{1}: Looking for new default Adobe Digital Editions Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
# get the default Adobe keys
defaultkeys = []
try:
if iswindows or isosx:
from calibre_plugins.dedrm.adobekey import adeptkeys
defaultkeys = adeptkeys()
else: # linux
from wineutils import WineGetKeys
scriptpath = os.join(self.alfdir,u"adobekey.py")
defaultkeys = WineGetKeys(scriptpath, u".der",dedrmprefs['adobewineprefix'])
self.default_key = default_keys[0]
except:
traceback.print_exc()
self.default_key = u""
newkeys = []
for keyvalue in defaultkeys:
if keyvalue.encode('hex') not in dedrmprefs['adeptkeys'].values():
newkeys.append(keyvalue)
if len(newkeys) > 0:
try:
for i,userkey in enumerate(newkeys):
print u"{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)
of = self.temporary_file(u".epub")
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
try:
result = ineptepub.decryptBook(userkey, inf.name, of.name)
except:
result = 1
of.close()
if result == 0:
# Decryption was a success
# Store the new successful key in the defaults
print u"{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)
try:
dedrmprefs.addnamedvaluetoprefs('adeptkeys','default_key',keyvalue.encode('hex'))
dedrmprefs.writeprefs()
except:
traceback.print_exc()
# Return the modified PersistentTemporary file to calibre.
return of.name
print u"{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
except Exception, e:
pass
# Something went wrong with decryption.
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt “{2}” after {3:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook),time.time()-self.starttime))
# Not a Barnes & Noble nor an Adobe Adept
# Import the fixed epub.
print u"{0} v{1}: “{2}” is neither an Adobe Adept nor a Barnes & Noble encrypted ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))
return inf.name
def PDFDecrypt(self,path_to_ebook):
import calibre_plugins.dedrm.prefs as prefs
import calibre_plugins.dedrm.ineptpdf
dedrmprefs = prefs.DeDRM_Prefs()
# Attempt to decrypt epub with each encryption key (generated or provided).
print u"{0} v{1}: {2} is a PDF ebook".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))
for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
userkey = userkeyhex.decode('hex')
print u"{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname)
of = self.temporary_file(u".pdf")
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
try:
result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name)
except:
result = 1
of.close()
if result == 0:
# Decryption was successful.
# Return the modified PersistentTemporary file to calibre.
return of.name
# perhaps we need to get a new default ADE key
print u"{0} v{1}: Looking for new default Adobe Digital Editions Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
# get the default Adobe keys
defaultkeys = []
if iswindows or isosx:
import calibre_plugins.dedrm.adobekey as adobe
try:
defaultkeys = adobe.adeptkeys()
except:
pass
else:
# linux
try:
from wineutils import WineGetKeys
scriptpath = os.join(self.alfdir,u"adobekey.py")
defaultkeys = self.WineGetKeys(scriptpath, u".der",dedrmprefs['adobewineprefix'])
except:
pass
newkeys = []
for keyvalue in defaultkeys:
if keyvalue.encode('hex') not in dedrmprefs['adeptkeys'].values():
newkeys.append(keyvalue)
if len(newkeys) > 0:
try:
for i,userkey in enumerate(newkeys):
print u"{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)
of = self.temporary_file(u".pdf")
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
try:
result = ineptepdf.decryptBook(userkey, inf.name, of.name)
except:
result = 1
of.close()
if result == 0:
# Decryption was a success
# Store the new successful key in the defaults
print u"{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)
try:
dedrmprefs.addnamedvaluetoprefs('adeptkeys','default_key',keyvalue.encode('hex'))
dedrmprefs.writeprefs()
except:
traceback.print_exc()
# Return the modified PersistentTemporary file to calibre.
return of.name
print u"{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
except Exception, e:
pass
# Something went wrong with decryption.
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt “{2}” after {3:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook),time.time()-self.starttime))
def KindleMobiDecrypt(self,path_to_ebook):
# add the alfcrypto directory to sys.path so alfcrypto.py
# will be able to locate the custom lib(s) for CDLL import.
sys.path.insert(0, self.alfdir)
# Had to move this import here so the custom libs can be
# extracted to the appropriate places beforehand these routines
# look for them.
import calibre_plugins.dedrm.prefs as prefs
import calibre_plugins.dedrm.k4mobidedrm
dedrmprefs = prefs.DeDRM_Prefs()
pids = dedrmprefs['pids']
serials = dedrmprefs['serials']
kindleDatabases = dedrmprefs['kindlekeys'].items()
try:
book = k4mobidedrm.GetDecryptedBook(path_to_ebook,kindleDatabases,serials,pids,self.starttime)
except Exception, e:
decoded = False
# perhaps we need to get a new default Kindle for Mac/PC key
defaultkeys = []
print u"{0} v{1}: Failed to decrypt with error: {2}".format(PLUGIN_NAME, PLUGIN_VERSION,e.args[0])
print u"{0} v{1}: Looking for new default Kindle Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)
try:
if iswindows or isosx:
from calibre_plugins.dedrm.kindlekey import kindlekeys
defaultkeys = kindlekeys()
else: # linux
from wineutils import WineGetKeys
scriptpath = os.join(self.alfdir,u"kindlekey.py")
defaultkeys = self.WineGetKeys(scriptpath, u".k4i",dedrmprefs['kindlewineprefix'])
except:
pass
newkeys = {}
for i,keyvalue in enumerate(defaultkeys):
keyname = u"default_key_{0:d}".format(i+1)
if keyvalue not in dedrmprefs['kindlekeys'].values():
newkeys[keyname] = keyvalue
if len(newkeys) > 0:
print u"{0} v{1}: Found {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), u"key" if len(newkeys)==1 else u"keys")
try:
book = k4mobidedrm.GetDecryptedBook(path_to_ebook,newkeys.items(),[],[],self.starttime)
decoded = True
# store the new successful keys in the defaults
print u"{0} v{1}: Saving {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), u"key" if len(newkeys)==1 else u"keys")
for keyvalue in newkeys.values():
dedrmprefs.addnamedvaluetoprefs('kindlekeys','default_key',keyvalue)
dedrmprefs.writeprefs()
except Exception, e:
pass
if not decoded:
#if you reached here then no luck raise and exception
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
traceback.print_exc()
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt “{4}” after {3:.1f} seconds with error: {2}\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0],time.time()-self.starttime,os.path.basename(path_to_ebook)))
of = self.temporary_file(book.getBookExtension())
book.getFile(of.name)
of.close()
book.cleanup()
return of.name
def eReaderDecrypt(self,path_to_ebook):
import calibre_plugins.dedrm.prefs as prefs
import calibre_plugins.dedrm.erdr2pml
dedrmrefs = prefs.DeDRM_Prefs()
# Attempt to decrypt epub with each encryption key (generated or provided).
for keyname, userkey in dedrmprefs['ereaderkeys'].items():
keyname_masked = u"".join((u'X' if (x.isdigit()) else x) for x in keyname)
print u"{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname_masked)
of = self.temporary_file(u".pmlz")
# Give the userkey, ebook and TemporaryPersistent file to the decryption function.
result = erdr2pml.decryptBook(path_to_ebook, of.name, True, userkey.decode('hex'))
of.close()
# Decryption was successful return the modified PersistentTemporary
# file to Calibre's import process.
if result == 0:
return of.name
print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime)
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt “{2}” after {3:.1f} seconds.\nRead the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook),time.time()-self.starttime))
def run(self, path_to_ebook):
# make sure any unicode output gets converted safely with 'replace'
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
print u"{0} v{1}: Trying to decrypt {2}".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))
self.starttime = time.time()
booktype = os.path.splitext(path_to_ebook)[1].lower()[1:]
if booktype in ['prc','mobi','azw','azw1','azw3','azw4','tpz']:
# Kindle/Mobipocket
decrypted_ebook = self.KindleMobiDecrypt(path_to_ebook)
elif booktype == 'pdb':
# eReader
decrypted_ebook = self.eReaderDecrypt(path_to_ebook)
pass
elif booktype == 'pdf':
# Adobe Adept PDF (hopefully)
decrypted_ebook = self.PDFDecrypt(path_to_ebook)
pass
elif booktype == 'epub':
# Adobe Adept or B&N ePub
decrypted_ebook = self.ePubDecrypt(path_to_ebook)
else:
print u"Unknown booktype {0}. Passing back to calibre unchanged".format(booktype)
return path_to_ebook
print u"{0} v{1}: Successfully decrypted book after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)
return decrypted_ebook
def is_customizable(self):
# return true to allow customization via the Plugin->Preferences.
return True
def config_widget(self):
import calibre_plugins.dedrm.config as config
return config.ConfigWidget(self.plugin_path, self.alfdir)
def save_settings(self, config_widget):
config_widget.save_settings()

View File

@@ -1,25 +1,31 @@
#! /usr/bin/python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import with_statement from __future__ import with_statement
# ineptkey.pyw, version 5.4 # adobekey.pyw, version 5.7
# 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 # Modified 20102013 by some_updates, DiapDealer and Apprentice Alf
# from <http://www.python.org/download/> and PyCrypto from
# <http://www.voidspace.org.uk/python/modules.shtml#pycrypto> (make certain # Windows users: Before running this program, you must first install Python.
# to install the version for Python 2.6). Then save this script file as # We recommend ActiveState Python 2.7.X for Windows (x86) from
# ineptkey.pyw and double-click on it to run it. It will create a file named # http://www.activestate.com/activepython/downloads.
# adeptkey.der in the same directory. This is your ADEPT user key. # You must also install PyCrypto from
# http://www.voidspace.org.uk/python/modules.shtml#pycrypto
# (make certain to install the version for Python 2.7).
# Then save this script file as adobekey.pyw and double-click on it to run it.
# It will create a file named adobekey_1.der in in the same directory as the script.
# This is your Adobe Digital Editions user key.
# #
# Mac OS X users: Save this script file as ineptkey.pyw. You can run this # Mac OS X users: Save this script file as adobekey.pyw. You can run this
# program from the command line (pythonw ineptkey.pyw) or by double-clicking # program from the command line (python adobekey.pyw) or by double-clicking
# it when it has been associated with PythonLauncher. It will create a file # it when it has been associated with PythonLauncher. It will create a file
# named adeptkey.der in the same directory. This is your ADEPT user key. # named adobekey_1.der in the same directory as the script.
# This is your Adobe Digital Editions user key.
# Revision history: # Revision history:
# 1 - Initial release, for Adobe Digital Editions 1.7 # 1 - Initial release, for Adobe Digital Editions 1.7
@@ -36,25 +42,87 @@ from __future__ import with_statement
# 5.2 - added support for output of key to a particular file # 5.2 - added support for output of key to a particular file
# 5.3 - On Windows try PyCrypto first, OpenSSL next # 5.3 - On Windows try PyCrypto first, OpenSSL next
# 5.4 - Modify interface to allow use of import # 5.4 - Modify interface to allow use of import
# 5.5 - Fix for potential problem with PyCrypto
# 5.6 - Revised to allow use in Plugins to eliminate need for duplicate code
# 5.7 - Unicode support added, renamed adobekey from ineptkey
# 5.8 - Added getkey interface for Windows DeDRM application
# 5.9 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 6.0 - Work if TkInter is missing
""" """
Retrieve Adobe ADEPT user key. Retrieve Adobe ADEPT user key.
""" """
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = '6.0'
import sys import sys, os, struct, getopt
import os
import struct # Wrap a stream so that output gets flushed immediately
import Tkinter # and also make sure that any unicode strings get
import Tkconstants # encoded using "replace" before writing them.
import tkMessageBox class SafeUnbuffered:
import traceback def __init__(self, stream):
self.stream = stream
self.encoding = stream.encoding
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
try:
from calibre.constants import iswindows, isosx
except:
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
def unicode_argv():
if iswindows:
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
# as a list of Unicode strings and encode them as utf-8
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"adobekey.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, \
@@ -110,7 +178,7 @@ 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
@@ -292,13 +360,9 @@ if sys.platform.startswith('win'):
return CryptUnprotectData return CryptUnprotectData
CryptUnprotectData = CryptUnprotectData() CryptUnprotectData = CryptUnprotectData()
def retrieve_key(keypath): def adeptkeys():
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()
@@ -308,11 +372,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:
@@ -334,84 +399,165 @@ 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: #print "found key:",userkey.encode('hex')
f.write(userkey) keys.append(userkey)
return True if len(keys) == 0:
raise ADEPTError('Could not locate privateLicenseKey')
print u"Found {0:d} keys".format(len(keys))
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: import warnings
fsref = Carbon.Folder.FSFindFolder(domain, dtype, False) warnings.filterwarnings('ignore', category=FutureWarning)
return Carbon.File.pathname(fsref)
except MacOS.Error: home = os.getenv('HOME')
cmdline = 'find "' + home + '/Library/Application Support/Adobe/Digital Editions" -name "activation.dat"'
cmdline = cmdline.encode(sys.getfilesystemencoding())
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)
ActDatPath = "activation.dat"
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 adeptkeys():
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 find ADE activation.dat file.")
tree = etree.parse(actpath) tree = etree.parse(actpath)
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
expr = '//%s/%s' % (adept('credentials'), adept('privateLicenseKey')) expr = '//%s/%s' % (adept('credentials'), adept('privateLicenseKey'))
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_key(keypath): def adeptkeys():
tkMessageBox.showerror( raise ADEPTError("This script only supports Windows and Mac OS X.")
"ADEPT Key", return []
"This script only supports Windows and Mac OS X. For Linux "
"you should be able to run ADE and this script under Wine (with " # interface for Python DeDRM
"an appropriate version of Windows Python installed).") def getkey(outpath):
keys = adeptkeys()
if len(keys) > 0:
if not os.path.isdir(outpath):
outfile = outpath
with file(outfile, 'wb') as keyfileout:
keyfileout.write(keys[0])
print u"Saved a key to {0}".format(outfile)
else:
keycount = 0
for key in keys:
while True:
keycount += 1
outfile = os.path.join(outpath,u"adobekey_{0:d}.der".format(keycount))
if not os.path.exists(outfile):
break
with file(outfile, 'wb') as keyfileout:
keyfileout.write(key)
print u"Saved a key to {0}".format(outfile)
return True
return False return False
class ExceptionDialog(Tkinter.Frame): def usage(progname):
print u"Finds, decrypts and saves the default Adobe Adept encryption key(s)."
print u"Keys are saved to the current directory, or a specified output directory."
print u"If a file name is passed instead of a directory, only the first key is saved, in that file."
print u"Usage:"
print u" {0:s} [-h] [<outpath>]".format(progname)
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv()
progname = os.path.basename(argv[0])
print u"{0} v{1}\nCopyright © 2009-2013 i♥cabbages and Apprentice Alf".format(progname,__version__)
try:
opts, args = getopt.getopt(argv[1:], "h")
except getopt.GetoptError, err:
print u"Error in options or arguments: {0}".format(err.args[0])
usage(progname)
sys.exit(2)
for o, a in opts:
if o == "-h":
usage(progname)
sys.exit(0)
if len(args) > 1:
usage(progname)
sys.exit(2)
if len(args) == 1:
# save to the specified file or directory
outpath = args[0]
if not os.path.isabs(outpath):
outpath = os.path.abspath(outpath)
else:
# save to the same directory as the script
outpath = os.path.dirname(argv[0])
# make sure the outpath is the
outpath = os.path.realpath(os.path.normpath(outpath))
keys = adeptkeys()
if len(keys) > 0:
if not os.path.isdir(outpath):
outfile = outpath
with file(outfile, 'wb') as keyfileout:
keyfileout.write(keys[0])
print u"Saved a key to {0}".format(outfile)
else:
keycount = 0
for key in keys:
while True:
keycount += 1
outfile = os.path.join(outpath,u"adobekey_{0:d}.der".format(keycount))
if not os.path.exists(outfile):
break
with file(outfile, 'wb') as keyfileout:
keyfileout.write(key)
print u"Saved a key to {0}".format(outfile)
else:
print u"Could not retrieve Adobe Adept key."
return 0
def gui_main():
try:
import Tkinter
import Tkconstants
import tkMessageBox
import traceback
except:
return cli_main()
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)
@@ -420,48 +566,38 @@ class ExceptionDialog(Tkinter.Frame):
self.text.insert(Tkconstants.END, text) self.text.insert(Tkconstants.END, text)
def extractKeyfile(keypath): argv=unicode_argv()
try:
success = retrieve_key(keypath)
except ADEPTError, e:
print "Key generation Error: " + str(e)
return 1
except Exception, e:
print "General Error: " + str(e)
return 1
if not success:
return 1
return 0
def cli_main(argv=sys.argv):
keypath = argv[1]
return extractKeyfile(keypath)
def main(argv=sys.argv):
root = Tkinter.Tk() root = Tkinter.Tk()
root.withdraw() root.withdraw()
progname = os.path.basename(argv[0]) progpath, progname = os.path.split(argv[0])
keypath = 'adeptkey.der'
success = False success = False
try: try:
success = retrieve_key(keypath) keys = adeptkeys()
keycount = 0
for key in keys:
while True:
keycount += 1
outfile = os.path.join(progpath,u"adobekey_{0:d}.der".format(keycount))
if not os.path.exists(outfile):
break
with file(outfile, 'wb') as keyfileout:
keyfileout.write(key)
success = True
tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile))
except ADEPTError, e: except ADEPTError, e:
tkMessageBox.showerror("ADEPT Key", "Error: " + str(e)) tkMessageBox.showerror(progname, u"Error: {0}".format(str(e)))
except Exception: except Exception:
root.wm_state('normal') root.wm_state('normal')
root.title('ADEPT Key') root.title(progname)
text = traceback.format_exc() text = traceback.format_exc()
ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1) ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1)
root.mainloop() root.mainloop()
if not success: if not success:
return 1 return 1
tkMessageBox.showinfo(
"ADEPT Key", "Key successfully retrieved to %s" % (keypath))
return 0 return 0
if __name__ == '__main__': if __name__ == '__main__':
if len(sys.argv) > 1: if len(sys.argv) > 1:
sys.exit(cli_main()) sys.exit(cli_main())
sys.exit(main()) sys.exit(gui_main())

View 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'

View File

@@ -0,0 +1,301 @@
#!/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'
# hard code to local location for libalfcrypto
libalfcrypto = os.path.join(sys.path[0],name_of_lib)
if not os.path.isfile(libalfcrypto):
libalfcrypto = os.path.join(sys.path[0], 'lib', name_of_lib)
if not os.path.isfile(libalfcrypto):
libalfcrypto = os.path.join('.',name_of_lib)
if not os.path.isfile(libalfcrypto):
raise Exception('libalfcrypto not found at %s' % libalfcrypto)
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]

View File

@@ -0,0 +1,907 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import with_statement
__license__ = 'GPL v3'
# Standard Python modules.
import os, traceback, json
# PyQT4 modules (part of calibre).
from PyQt4.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit,
QGroupBox, QPushButton, QListWidget, QListWidgetItem,
QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl, QString)
from PyQt4 import QtGui
from zipfile import ZipFile
# calibre modules and constants.
from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url,
choose_dir, choose_files)
from calibre.utils.config import dynamic, config_dir, JSONConfig
from calibre.constants import iswindows, isosx
# modules from this plugin's zipfile.
from calibre_plugins.dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION
from calibre_plugins.dedrm.__init__ import RESOURCE_NAME as help_file_name
from calibre_plugins.dedrm.utilities import uStrCmp
import calibre_plugins.dedrm.prefs as prefs
class ConfigWidget(QWidget):
def __init__(self, plugin_path, alfdir):
QWidget.__init__(self)
self.plugin_path = plugin_path
self.alfdir = alfdir
# get the prefs
self.dedrmprefs = prefs.DeDRM_Prefs()
# make a local copy
self.tempdedrmprefs = {}
self.tempdedrmprefs['bandnkeys'] = self.dedrmprefs['bandnkeys'].copy()
self.tempdedrmprefs['adeptkeys'] = self.dedrmprefs['adeptkeys'].copy()
self.tempdedrmprefs['ereaderkeys'] = self.dedrmprefs['ereaderkeys'].copy()
self.tempdedrmprefs['kindlekeys'] = self.dedrmprefs['kindlekeys'].copy()
self.tempdedrmprefs['pids'] = list(self.dedrmprefs['pids'])
self.tempdedrmprefs['serials'] = list(self.dedrmprefs['serials'])
self.tempdedrmprefs['adobewineprefix'] = self.dedrmprefs['adobewineprefix']
self.tempdedrmprefs['kindlewineprefix'] = self.dedrmprefs['kindlewineprefix']
# 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(_('Configuration:'), self)
layout.addWidget(keys_group_box)
keys_group_box_layout = QHBoxLayout()
keys_group_box.setLayout(keys_group_box_layout)
button_layout = QVBoxLayout()
keys_group_box_layout.addLayout(button_layout)
self.bandn_button = QtGui.QPushButton(self)
self.bandn_button.setToolTip(_(u"Click to manage keys for Barnes and Noble ebooks"))
self.bandn_button.setText(u"Barnes and Noble ebooks")
self.bandn_button.clicked.connect(self.bandn_keys)
self.kindle_serial_button = QtGui.QPushButton(self)
self.kindle_serial_button.setToolTip(_(u"Click to manage eInk Kindle serial numbers for Kindle ebooks"))
self.kindle_serial_button.setText(u"eInk Kindle ebooks")
self.kindle_serial_button.clicked.connect(self.kindle_serials)
self.kindle_key_button = QtGui.QPushButton(self)
self.kindle_key_button.setToolTip(_(u"Click to manage keys for Kindle for Mac/PC ebooks"))
self.kindle_key_button.setText(u"Kindle for Mac/PC ebooks")
self.kindle_key_button.clicked.connect(self.kindle_keys)
self.adept_button = QtGui.QPushButton(self)
self.adept_button.setToolTip(_(u"Click to manage keys for Adobe Digital Editions ebooks"))
self.adept_button.setText(u"Adobe Digital Editions ebooks")
self.adept_button.clicked.connect(self.adept_keys)
self.mobi_button = QtGui.QPushButton(self)
self.mobi_button.setToolTip(_(u"Click to manage PIDs for Mobipocket ebooks"))
self.mobi_button.setText(u"Mobipocket ebooks")
self.mobi_button.clicked.connect(self.mobi_keys)
self.ereader_button = QtGui.QPushButton(self)
self.ereader_button.setToolTip(_(u"Click to manage keys for eReader ebooks"))
self.ereader_button.setText(u"eReader ebooks")
self.ereader_button.clicked.connect(self.ereader_keys)
button_layout.addWidget(self.kindle_serial_button)
button_layout.addWidget(self.bandn_button)
button_layout.addWidget(self.mobi_button)
button_layout.addWidget(self.ereader_button)
button_layout.addWidget(self.adept_button)
button_layout.addWidget(self.kindle_key_button)
self.resize(self.sizeHint())
def kindle_serials(self):
d = ManageKeysDialog(self,u"EInk Kindle Serial Number",self.tempdedrmprefs['serials'], AddSerialDialog)
d.exec_()
def kindle_keys(self):
if isosx or iswindows:
d = ManageKeysDialog(self,u"Kindle for Mac and PC Key",self.tempdedrmprefs['kindlekeys'], AddKindleDialog, 'k4i')
else:
# linux
d = ManageKeysDialog(self,u"Kindle for Mac and PC Key",self.tempdedrmprefs['kindlekeys'], AddKindleDialog, 'k4i', self.tempdedrmprefs['kindlewineprefix'])
d.exec_()
self.tempdedrmprefs['kindlewineprefix'] = d.getwineprefix()
def adept_keys(self):
if isosx or iswindows:
d = ManageKeysDialog(self,u"Adobe Digital Editions Key",self.tempdedrmprefs['adeptkeys'], AddAdeptDialog, 'der')
else:
# linux
d = ManageKeysDialog(self,u"Adobe Digital Editions Key",self.tempdedrmprefs['adeptkeys'], AddAdeptDialog, 'der', self.tempdedrmprefs['adobewineprefix'])
d.exec_()
self.tempdedrmprefs['adobewineprefix'] = d.getwineprefix()
def mobi_keys(self):
d = ManageKeysDialog(self,u"Mobipocket PID",self.tempdedrmprefs['pids'], AddPIDDialog)
d.exec_()
def bandn_keys(self):
d = ManageKeysDialog(self,u"Barnes and Noble Key",self.tempdedrmprefs['bandnkeys'], AddBandNKeyDialog, 'b64')
d.exec_()
def ereader_keys(self):
d = ManageKeysDialog(self,u"eReader Key",self.tempdedrmprefs['ereaderkeys'], AddEReaderDialog, 'b63')
d.exec_()
def help_link_activated(self, url):
def get_help_file_resource():
# Copy the HTML helpfile to the plugin directory each time the
# link is clicked in case the helpfile is updated in newer plugins.
file_path = os.path.join(config_dir, u"plugins", u"DeDRM", u"help", help_file_name)
with open(file_path,'w') as f:
f.write(self.load_resource(help_file_name))
return file_path
url = 'file:///' + get_help_file_resource()
open_url(QUrl(url))
def save_settings(self):
self.dedrmprefs.set('bandnkeys', self.tempdedrmprefs['bandnkeys'])
self.dedrmprefs.set('adeptkeys', self.tempdedrmprefs['adeptkeys'])
self.dedrmprefs.set('ereaderkeys', self.tempdedrmprefs['ereaderkeys'])
self.dedrmprefs.set('kindlekeys', self.tempdedrmprefs['kindlekeys'])
self.dedrmprefs.set('pids', self.tempdedrmprefs['pids'])
self.dedrmprefs.set('serials', self.tempdedrmprefs['serials'])
self.dedrmprefs.set('adobewineprefix', self.tempdedrmprefs['adobewineprefix'])
self.dedrmprefs.set('kindlewineprefix', self.tempdedrmprefs['kindlewineprefix'])
self.dedrmprefs.set('configured', True)
self.dedrmprefs.writeprefs()
def load_resource(self, name):
with ZipFile(self.plugin_path, 'r') as zf:
if name in zf.namelist():
return zf.read(name)
return ""
class ManageKeysDialog(QDialog):
def __init__(self, parent, key_type_name, plugin_keys, create_key, keyfile_ext = u"", wineprefix = None):
QDialog.__init__(self,parent)
self.parent = parent
self.key_type_name = key_type_name
self.plugin_keys = plugin_keys
self.create_key = create_key
self.keyfile_ext = keyfile_ext
self.import_key = (keyfile_ext != u"")
self.binary_file = (keyfile_ext == u"der")
self.json_file = (keyfile_ext == u"k4i")
self.wineprefix = wineprefix
self.setWindowTitle("{0} {1}: Manage {2}s".format(PLUGIN_NAME, PLUGIN_VERSION, self.key_type_name))
# Start Qt Gui dialog layout
layout = QVBoxLayout(self)
self.setLayout(layout)
help_layout = QHBoxLayout()
layout.addLayout(help_layout)
# Add hyperlink to a help file at the right. We will replace the correct name when it is clicked.
help_label = QLabel('<a href="http://www.foo.com/">Help</a>', self)
help_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
help_label.setAlignment(Qt.AlignRight)
help_label.linkActivated.connect(self.help_link_activated)
help_layout.addWidget(help_label)
keys_group_box = QGroupBox(_(u"{0}s".format(self.key_type_name)), self)
layout.addWidget(keys_group_box)
keys_group_box_layout = QHBoxLayout()
keys_group_box.setLayout(keys_group_box_layout)
self.listy = QListWidget(self)
self.listy.setToolTip(u"{0}s that will be used to decrypt ebooks".format(self.key_type_name))
self.listy.setSelectionMode(QAbstractItemView.SingleSelection)
self.populate_list()
keys_group_box_layout.addWidget(self.listy)
button_layout = QVBoxLayout()
keys_group_box_layout.addLayout(button_layout)
self._add_key_button = QtGui.QToolButton(self)
self._add_key_button.setToolTip(u"Create new {0}".format(self.key_type_name))
self._add_key_button.setIcon(QIcon(I('plus.png')))
self._add_key_button.clicked.connect(self.add_key)
button_layout.addWidget(self._add_key_button)
self._delete_key_button = QtGui.QToolButton(self)
self._delete_key_button.setToolTip(_(u"Delete highlighted key"))
self._delete_key_button.setIcon(QIcon(I('list_remove.png')))
self._delete_key_button.clicked.connect(self.delete_key)
button_layout.addWidget(self._delete_key_button)
if type(self.plugin_keys) == dict and self.import_key:
self._rename_key_button = QtGui.QToolButton(self)
self._rename_key_button.setToolTip(_(u"Rename highlighted key"))
self._rename_key_button.setIcon(QIcon(I('edit-select-all.png')))
self._rename_key_button.clicked.connect(self.rename_key)
button_layout.addWidget(self._rename_key_button)
self.export_key_button = QtGui.QToolButton(self)
self.export_key_button.setToolTip(u"Save highlighted key to a .{0} file".format(self.keyfile_ext))
self.export_key_button.setIcon(QIcon(I('save.png')))
self.export_key_button.clicked.connect(self.export_key)
button_layout.addWidget(self.export_key_button)
spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
button_layout.addItem(spacerItem)
if self.wineprefix is not None:
layout.addSpacing(5)
wineprefix_layout = QHBoxLayout()
layout.addLayout(wineprefix_layout)
wineprefix_layout.setAlignment(Qt.AlignCenter)
self.wp_label = QLabel(u"WINEPREFIX:")
wineprefix_layout.addWidget(self.wp_label)
self.wp_lineedit = QLineEdit(self)
wineprefix_layout.addWidget(self.wp_lineedit)
self.wp_label.setBuddy(self.wp_lineedit)
self.wp_lineedit.setText(self.wineprefix)
layout.addSpacing(5)
migrate_layout = QHBoxLayout()
layout.addLayout(migrate_layout)
if self.import_key:
migrate_layout.setAlignment(Qt.AlignJustify)
self.migrate_btn = QPushButton(u"Import Existing Keyfiles", self)
self.migrate_btn.setToolTip(u"Import *.{0} files (created using other tools).".format(self.keyfile_ext))
self.migrate_btn.clicked.connect(self.migrate_wrapper)
migrate_layout.addWidget(self.migrate_btn)
migrate_layout.addStretch()
self.button_box = QDialogButtonBox(QDialogButtonBox.Close)
self.button_box.rejected.connect(self.close)
migrate_layout.addWidget(self.button_box)
self.resize(self.sizeHint())
def getwineprefix(self):
if self.wineprefix is not None:
return unicode(self.wp_lineedit.text().toUtf8(), 'utf8').strip()
return u""
def populate_list(self):
if type(self.plugin_keys) == dict:
for key in self.plugin_keys.keys():
self.listy.addItem(QListWidgetItem(key))
else:
for key in self.plugin_keys:
self.listy.addItem(QListWidgetItem(key))
def add_key(self):
d = self.create_key(self)
d.exec_()
if d.result() != d.Accepted:
# New key generation cancelled.
return
new_key_value = d.key_value
if type(self.plugin_keys) == dict:
if new_key_value in self.plugin_keys.values():
old_key_name = [name for name, value in self.plugin_keys.iteritems() if value == new_key_value][0]
info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
u"The new {1} is the same as the existing {1} named <strong>{0}</strong> and has not been added.".format(old_key_name,self.key_type_name), show=True)
return
self.plugin_keys[d.key_name] = new_key_value
else:
if new_key_value in self.plugin_keys:
info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
u"This {0} is already in the list of {0}s has not been added.".format(self.key_type_name), show=True)
return
self.plugin_keys.append(d.key_value)
self.listy.clear()
self.populate_list()
def rename_key(self):
if not self.listy.currentItem():
errmsg = u"No {0} selected to rename. Highlight a keyfile first.".format(self.key_type_name)
r = error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
_(errmsg), show=True, show_copy_button=False)
return
d = RenameKeyDialog(self)
d.exec_()
if d.result() != d.Accepted:
# rename cancelled or moot.
return
keyname = unicode(self.listy.currentItem().text().toUtf8(),'utf8')
if not question_dialog(self, "{0} {1}: Confirm Rename".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to rename the {2} named <strong>{0}</strong> to <strong>{1}</strong>?".format(keyname,d.key_name,self.key_type_name), show_copy_button=False, default_yes=False):
return
self.plugin_keys[d.key_name] = self.plugin_keys[keyname]
del self.plugin_keys[keyname]
self.listy.clear()
self.populate_list()
def delete_key(self):
if not self.listy.currentItem():
return
keyname = unicode(self.listy.currentItem().text().toUtf8(), 'utf8')
if not question_dialog(self, "{0} {1}: Confirm Delete".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to delete the {1} <strong>{0}</strong>?".format(keyname, self.key_type_name), show_copy_button=False, default_yes=False):
return
if type(self.plugin_keys) == dict:
del self.plugin_keys[keyname]
else:
self.plugin_keys.remove(keyname)
self.listy.clear()
self.populate_list()
def help_link_activated(self, url):
def get_help_file_resource():
# Copy the HTML helpfile to the plugin directory each time the
# link is clicked in case the helpfile is updated in newer plugins.
help_file_name = u"{0}_{1}_Help.htm".format(PLUGIN_NAME, self.key_type_name)
file_path = os.path.join(config_dir, u"plugins", u"DeDRM", u"help", help_file_name)
with open(file_path,'w') as f:
f.write(self.parent.load_resource(help_file_name))
return file_path
url = 'file:///' + get_help_file_resource()
open_url(QUrl(url))
def migrate_files(self):
dynamic[PLUGIN_NAME + u"config_dir"] = config_dir
files = choose_files(self, PLUGIN_NAME + u"config_dir",
u"Select {0} files to import".format(self.key_type_name), [(u"{0} files".format(self.key_type_name), [self.keyfile_ext])], False)
counter = 0
skipped = 0
if files:
for filename in files:
fpath = os.path.join(config_dir, filename)
filename = os.path.basename(filename)
new_key_name = os.path.splitext(os.path.basename(filename))[0]
with open(fpath,'rb') as keyfile:
new_key_value = keyfile.read()
if self.binary_file:
new_key_value = new_key_value.encode('hex')
elif self.json_file:
new_key_value = json.loads(new_key_value)
match = False
for key in self.plugin_keys.keys():
if uStrCmp(new_key_name, key, True):
skipped += 1
msg = u"A key with the name <strong>{0}</strong> already exists!\nSkipping key file <strong>{1}</strong>.\nRename the existing key and import again".format(new_key_name,filename)
inf = info_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
_(msg), show_copy_button=False, show=True)
match = True
break
if not match:
if new_key_value in self.plugin_keys.values():
old_key_name = [name for name, value in self.plugin_keys.iteritems() if value == new_key_value][0]
skipped += 1
info_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
u"The key in file {0} is the same as the existing key <strong>{1}</strong> and has been skipped.".format(filename,old_key_name), show_copy_button=False, show=True)
else:
counter += 1
self.plugin_keys[new_key_name] = new_key_value
msg = u""
if counter+skipped > 1:
if counter > 0:
msg += u"Imported <strong>{0:d}</strong> key {1}. ".format(counter, u"file" if counter == 1 else u"files")
if skipped > 0:
msg += u"Skipped <strong>{0:d}</strong> key {1}.".format(skipped, u"file" if counter == 1 else u"files")
inf = info_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
_(msg), show_copy_button=False, show=True)
return counter > 0
def migrate_wrapper(self):
if self.migrate_files():
self.listy.clear()
self.populate_list()
def export_key(self):
if not self.listy.currentItem():
errmsg = u"No keyfile selected to export. Highlight a keyfile first."
r = error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
_(errmsg), show=True, show_copy_button=False)
return
filter = QString(u"{0} Files (*.{1})".format(self.key_type_name, self.keyfile_ext))
keyname = unicode(self.listy.currentItem().text().toUtf8(), 'utf8')
if dynamic.get(PLUGIN_NAME + 'save_dir'):
defaultname = os.path.join(dynamic.get(PLUGIN_NAME + 'save_dir'), u"{0}.{1}".format(keyname , self.keyfile_ext))
else:
defaultname = os.path.join(os.path.expanduser('~'), u"{0}.{1}".format(keyname , self.keyfile_ext))
filename = unicode(QtGui.QFileDialog.getSaveFileName(self, u"Save {0} File as...".format(self.key_type_name), defaultname,
u"{0} Files (*.{1})".format(self.key_type_name,self.keyfile_ext), filter))
if filename:
dynamic[PLUGIN_NAME + 'save_dir'] = os.path.split(filename)[0]
with file(filename, 'w') as fname:
if self.binary_file:
fname.write(self.plugin_keys[keyname].decode('hex'))
elif self.json_file:
fname.write(json.dumps(self.plugin_keys[keyname]))
else:
fname.write(self.plugin_keys[keyname])
class RenameKeyDialog(QDialog):
def __init__(self, parent=None,):
print repr(self), repr(parent)
QDialog.__init__(self, parent)
self.parent = parent
self.setWindowTitle("{0} {1}: Rename {0}".format(PLUGIN_NAME, PLUGIN_VERSION, parent.key_type_name))
layout = QVBoxLayout(self)
self.setLayout(layout)
data_group_box = QGroupBox('', self)
layout.addWidget(data_group_box)
data_group_box_layout = QVBoxLayout()
data_group_box.setLayout(data_group_box_layout)
data_group_box_layout.addWidget(QLabel('New Key Name:', self))
self.key_ledit = QLineEdit(self.parent.listy.currentItem().text(), self)
self.key_ledit.setToolTip(u"Enter a new name for this existing {0}.".format(parent.key_type_name))
data_group_box_layout.addWidget(self.key_ledit)
layout.addSpacing(20)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
layout.addWidget(self.button_box)
self.resize(self.sizeHint())
def accept(self):
if self.key_ledit.text().isEmpty() or unicode(self.key_ledit.text()).isspace():
errmsg = u"Key name field cannot be empty!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
_(errmsg), show=True, show_copy_button=False)
if len(self.key_ledit.text()) < 4:
errmsg = u"Key name must be at <i>least</i> 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
_(errmsg), show=True, show_copy_button=False)
if uStrCmp(self.key_ledit.text(), self.parent.listy.currentItem().text()):
# Same exact name ... do nothing.
return QDialog.reject(self)
for k in self.parent.plugin_keys.keys():
if (uStrCmp(self.key_ledit.text(), k, True) and
not uStrCmp(k, self.parent.listy.currentItem().text(), True)):
errmsg = u"The key name <strong>{0}</strong> is already being used.".format(self.key_ledit.text())
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
_(errmsg), show=True, show_copy_button=False)
QDialog.accept(self)
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
class AddBandNKeyDialog(QDialog):
def __init__(self, parent=None,):
QDialog.__init__(self, parent)
self.parent = parent
self.setWindowTitle(u"{0} {1}: Create New Barnes & Noble Key".format(PLUGIN_NAME, PLUGIN_VERSION))
layout = QVBoxLayout(self)
self.setLayout(layout)
data_group_box = QGroupBox(u"", self)
layout.addWidget(data_group_box)
data_group_box_layout = QVBoxLayout()
data_group_box.setLayout(data_group_box_layout)
key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel(u"Unique Key Name:", self))
self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(_(u"<p>Enter an identifying name for this new key.</p>" +
u"<p>It should be something that will help you remember " +
u"what personal information was used to create it."))
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
name_group = QHBoxLayout()
data_group_box_layout.addLayout(name_group)
name_group.addWidget(QLabel(u"Your Name:", self))
self.name_ledit = QLineEdit(u"", self)
self.name_ledit.setToolTip(_(u"<p>Enter your name as it appears in your B&N " +
u"account or on your credit card.</p>" +
u"<p>It will only be used to generate this " +
u"one-time key and won\'t be stored anywhere " +
u"in calibre or on your computer.</p>" +
u"<p>(ex: Jonathan Smith)"))
name_group.addWidget(self.name_ledit)
name_disclaimer_label = QLabel(_(u"(Will not be saved in configuration data)"), self)
name_disclaimer_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(name_disclaimer_label)
ccn_group = QHBoxLayout()
data_group_box_layout.addLayout(ccn_group)
ccn_group.addWidget(QLabel(u"Credit Card#:", self))
self.cc_ledit = QLineEdit(u"", self)
self.cc_ledit.setToolTip(_(u"<p>Enter the full credit card number on record " +
u"in your B&N account.</p>" +
u"<p>No spaces or dashes... just the numbers. " +
u"This number will only be used to generate this " +
u"one-time key and won\'t be stored anywhere in " +
u"calibre or on your computer."))
ccn_group.addWidget(self.cc_ledit)
ccn_disclaimer_label = QLabel(_('(Will not be saved in configuration data)'), self)
ccn_disclaimer_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(ccn_disclaimer_label)
layout.addSpacing(10)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
layout.addWidget(self.button_box)
self.resize(self.sizeHint())
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
@property
def key_value(self):
from calibre_plugins.dedrm.ignoblekeygen import generate_key as generate_bandn_key
return generate_bandn_key(self.user_name,self.cc_number)
@property
def user_name(self):
return unicode(self.name_ledit.text().toUtf8(), 'utf8').strip().lower().replace(' ','')
@property
def cc_number(self):
return unicode(self.cc_ledit.text().toUtf8(), 'utf8').strip().replace(' ', '').replace('-','')
def accept(self):
if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace():
errmsg = u"All fields are required!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if not self.cc_number.isdigit():
errmsg = u"Numbers only in the credit card number field!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) < 4:
errmsg = u"Key name must be at <i>least</i> 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
QDialog.accept(self)
class AddEReaderDialog(QDialog):
def __init__(self, parent=None,):
QDialog.__init__(self, parent)
self.parent = parent
self.setWindowTitle(u"{0} {1}: Create New eReader Key".format(PLUGIN_NAME, PLUGIN_VERSION))
layout = QVBoxLayout(self)
self.setLayout(layout)
data_group_box = QGroupBox(u"", self)
layout.addWidget(data_group_box)
data_group_box_layout = QVBoxLayout()
data_group_box.setLayout(data_group_box_layout)
key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel(u"Unique Key Name:", self))
self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(u"<p>Enter an identifying name for this new key.\nIt should be something that will help you remember what personal information was used to create it.")
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
name_group = QHBoxLayout()
data_group_box_layout.addLayout(name_group)
name_group.addWidget(QLabel(u"Your Name:", self))
self.name_ledit = QLineEdit(u"", self)
self.name_ledit.setToolTip(u"Enter the name for this eReader key, usually the name on your credit card.\nIt will only be used to generate this one-time key and won\'t be stored anywhere in calibre or on your computer.\n(ex: Mr Jonathan Q Smith)")
name_group.addWidget(self.name_ledit)
name_disclaimer_label = QLabel(_(u"(Will not be saved in configuration data)"), self)
name_disclaimer_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(name_disclaimer_label)
ccn_group = QHBoxLayout()
data_group_box_layout.addLayout(ccn_group)
ccn_group.addWidget(QLabel(u"Credit Card#:", self))
self.cc_ledit = QLineEdit(u"", self)
self.cc_ledit.setToolTip(u"<p>Enter the last 8 digits of credit card number for this eReader key.\nThey will only be used to generate this one-time key and won\'t be stored anywhere in calibre or on your computer.")
ccn_group.addWidget(self.cc_ledit)
ccn_disclaimer_label = QLabel(_('(Will not be saved in configuration data)'), self)
ccn_disclaimer_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(ccn_disclaimer_label)
layout.addSpacing(10)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
layout.addWidget(self.button_box)
self.resize(self.sizeHint())
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
@property
def key_value(self):
from calibre_plugins.dedrm.erdr2pml import getuser_key as generate_ereader_key
return generate_ereader_key(self.user_name,self.cc_number).encode('hex')
@property
def user_name(self):
return unicode(self.name_ledit.text().toUtf8(), 'utf8').strip().lower().replace(' ','')
@property
def cc_number(self):
return unicode(self.cc_ledit.text().toUtf8(), 'utf8').strip().replace(' ', '').replace('-','')
def accept(self):
if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace():
errmsg = u"All fields are required!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if not self.cc_number.isdigit():
errmsg = u"Numbers only in the credit card number field!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) < 4:
errmsg = u"Key name must be at <i>least</i> 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
QDialog.accept(self)
class AddAdeptDialog(QDialog):
def __init__(self, parent=None,):
QDialog.__init__(self, parent)
self.parent = parent
self.setWindowTitle(u"{0} {1}: Getting Default Adobe Digital Editions Key".format(PLUGIN_NAME, PLUGIN_VERSION))
layout = QVBoxLayout(self)
self.setLayout(layout)
try:
if iswindows or isosx:
from calibre_plugins.dedrm.adobekey import adeptkeys
defaultkeys = adeptkeys()
else: # linux
from wineutils import WineGetKeys
scriptpath = os.path.join(parent.parent.alfdir,u"adobekey.py")
defaultkeys = WineGetKeys(scriptpath, u".der",parent.getwineprefix())
self.default_key = defaultkeys[0]
except:
traceback.print_exc()
self.default_key = u""
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
if len(self.default_key)>0:
data_group_box = QGroupBox(u"", self)
layout.addWidget(data_group_box)
data_group_box_layout = QVBoxLayout()
data_group_box.setLayout(data_group_box_layout)
key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel(u"Unique Key Name:", self))
self.key_ledit = QLineEdit(u"default_key", self)
self.key_ledit.setToolTip(u"<p>Enter an identifying name for the current default Adobe Digital Editions key.")
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
self.button_box.accepted.connect(self.accept)
else:
default_key_error = QLabel(u"The default encryption key for Adobe Digital Editions could not be found.", self)
default_key_error.setAlignment(Qt.AlignHCenter)
layout.addWidget(default_key_error)
# if no default, bot buttons do the same
self.button_box.accepted.connect(self.reject)
self.button_box.rejected.connect(self.reject)
layout.addWidget(self.button_box)
self.resize(self.sizeHint())
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
@property
def key_value(self):
return self.default_key.encode('hex')
def accept(self):
if len(self.key_name) == 0 or self.key_name.isspace():
errmsg = u"All fields are required!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) < 4:
errmsg = u"Key name must be at <i>least</i> 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
QDialog.accept(self)
class AddKindleDialog(QDialog):
def __init__(self, parent=None,):
QDialog.__init__(self, parent)
self.parent = parent
self.setWindowTitle(u"{0} {1}: Getting Default Kindle for Mac/PC Key".format(PLUGIN_NAME, PLUGIN_VERSION))
layout = QVBoxLayout(self)
self.setLayout(layout)
try:
if iswindows or isosx:
from calibre_plugins.dedrm.kindlekey import kindlekeys
defaultkeys = kindlekeys()
else: # linux
from wineutils import WineGetKeys
scriptpath = os.path.join(parent.parent.alfdir,u"kindlekey.py")
defaultkeys = WineGetKeys(scriptpath, u".k4i",parent.getwineprefix())
self.default_key = defaultkeys[0]
except:
traceback.print_exc()
self.default_key = u""
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
if len(self.default_key)>0:
data_group_box = QGroupBox(u"", self)
layout.addWidget(data_group_box)
data_group_box_layout = QVBoxLayout()
data_group_box.setLayout(data_group_box_layout)
key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel(u"Unique Key Name:", self))
self.key_ledit = QLineEdit(u"default_key", self)
self.key_ledit.setToolTip(u"<p>Enter an identifying name for the current default Kindle for Mac/PC key.")
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
self.button_box.accepted.connect(self.accept)
else:
default_key_error = QLabel(u"The default encryption key for Kindle for Mac/PC could not be found.", self)
default_key_error.setAlignment(Qt.AlignHCenter)
layout.addWidget(default_key_error)
# if no default, bot buttons do the same
self.button_box.accepted.connect(self.reject)
self.button_box.rejected.connect(self.reject)
layout.addWidget(self.button_box)
self.resize(self.sizeHint())
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
@property
def key_value(self):
return self.default_key
def accept(self):
if len(self.key_name) == 0 or self.key_name.isspace():
errmsg = u"All fields are required!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) < 4:
errmsg = u"Key name must be at <i>least</i> 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
QDialog.accept(self)
class AddSerialDialog(QDialog):
def __init__(self, parent=None,):
QDialog.__init__(self, parent)
self.parent = parent
self.setWindowTitle(u"{0} {1}: Add New EInk Kindle Serial Number".format(PLUGIN_NAME, PLUGIN_VERSION))
layout = QVBoxLayout(self)
self.setLayout(layout)
data_group_box = QGroupBox(u"", self)
layout.addWidget(data_group_box)
data_group_box_layout = QVBoxLayout()
data_group_box.setLayout(data_group_box_layout)
key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel(u"EInk Kindle Serial Number:", self))
self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(u"Enter an eInk Kindle serial number. EInk Kindle serial numbers are 16 characters long and usually start with a 'B' or a '9'. Kindle Serial Numbers are case-sensitive, so be sure to enter the upper and lower case letters unchanged.")
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
layout.addWidget(self.button_box)
self.resize(self.sizeHint())
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
@property
def key_value(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
def accept(self):
if len(self.key_name) == 0 or self.key_name.isspace():
errmsg = u"Please enter an eInk Kindle Serial Number or click Cancel in the dialog."
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) != 16:
errmsg = u"EInk Kindle Serial Numbers must be 16 characters long. This is {0:d} characters long.".format(len(self.key_name))
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
QDialog.accept(self)
class AddPIDDialog(QDialog):
def __init__(self, parent=None,):
QDialog.__init__(self, parent)
self.parent = parent
self.setWindowTitle(u"{0} {1}: Add New Mobipocket PID".format(PLUGIN_NAME, PLUGIN_VERSION))
layout = QVBoxLayout(self)
self.setLayout(layout)
data_group_box = QGroupBox(u"", self)
layout.addWidget(data_group_box)
data_group_box_layout = QVBoxLayout()
data_group_box.setLayout(data_group_box_layout)
key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel(u"PID:", self))
self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(u"Enter a Mobipocket PID. Mobipocket PIDs are 8 or 10 characters long. Mobipocket PIDs are case-sensitive, so be sure to enter the upper and lower case letters unchanged.")
key_group.addWidget(self.key_ledit)
key_label = QLabel(_(''), self)
key_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(key_label)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
layout.addWidget(self.button_box)
self.resize(self.sizeHint())
@property
def key_name(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
@property
def key_value(self):
return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip()
def accept(self):
if len(self.key_name) == 0 or self.key_name.isspace():
errmsg = u"Please enter a Mobipocket PID or click Cancel in the dialog."
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) != 8 and len(self.key_name) != 10:
errmsg = u"Mobipocket PIDs must be 8 or 10 characters long. This is {0:d} characters long.".format(len(self.key_name))
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
QDialog.accept(self)

View File

@@ -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,15 +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._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),
@@ -253,16 +262,31 @@ class PageParser(object):
'img.w' : (1, 'scalar_number', 0, 0), 'img.w' : (1, 'scalar_number', 0, 0),
'img.src' : (1, 'scalar_number', 0, 0), 'img.src' : (1, 'scalar_number', 0, 0),
'img.color_src' : (1, 'scalar_number', 0, 0), 'img.color_src' : (1, 'scalar_number', 0, 0),
'img.gridBeginCenter' : (1, 'scalar_number', 0, 0),
'img.gridEndCenter' : (1, 'scalar_number', 0, 0),
'img.image_type' : (1, 'scalar_number', 0, 0),
'paragraph' : (1, 'snippets', 1, 0), 'paragraph' : (1, 'snippets', 1, 0),
'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),
'word_semantic.gridBottomCenter' : (1, 'scalar_number', 0, 0),
'word_semantic.gridTopCenter' : (1, 'scalar_number', 0, 0),
'word_semantic.gridBeginCenter' : (1, 'scalar_number', 0, 0),
'word_semantic.gridEndCenter' : (1, 'scalar_number', 0, 0),
'word' : (1, 'snippets', 1, 0), 'word' : (1, 'snippets', 1, 0),
'word.type' : (1, 'scalar_text', 0, 0), 'word.type' : (1, 'scalar_text', 0, 0),
@@ -271,14 +295,26 @@ 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.class' : (1, 'scalar_text', 0, 0),
'extratokens.type' : (1, 'scalar_text', 0, 0), 'extratokens.type' : (1, 'scalar_text', 0, 0),
'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0), 'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0),
'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0), 'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0),
@@ -328,7 +364,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),
@@ -595,28 +633,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
@@ -625,35 +665,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

View File

@@ -1,4 +1,4 @@
{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370
{\fonttbl} {\fonttbl}
{\colortbl;\red255\green255\blue255;} {\colortbl;\red255\green255\blue255;}
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 362 B

After

Width:  |  Height:  |  Size: 362 B

View File

@@ -43,4 +43,3 @@ def cli_main(argv=sys.argv):
if __name__ == '__main__': if __name__ == '__main__':
sys.exit(cli_main()) sys.exit(cli_main())

View File

@@ -0,0 +1,208 @@
#!/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 drmcheck
# 1.00 - Initial version, with code from various other scripts
# 1.01 - Moved authorship announcement to usage section.
#
# Changelog epubtest
# 1.00 - Cut to epubtest.py, testing ePub files only by Apprentice Alf
# 1.01 - Added routine for use by Windows DeDRM
#
# Written in 2011 by Paul Durrant
# Released with unlicense. See http://unlicense.org/
#
#############################################################################
#
# This is free and unencumbered software released into the public domain.
#
# Anyone is free to copy, modify, publish, use, compile, sell, or
# distribute this software, either in source code form or as a compiled
# binary, for any purpose, commercial or non-commercial, and by any
# means.
#
# In jurisdictions that recognize copyright laws, the author or authors
# of this software dedicate any and all copyright interest in the
# software to the public domain. We make this dedication for the benefit
# of the public at large and to the detriment of our heirs and
# successors. We intend this dedication to be an overt act of
# relinquishment in perpetuity of all present and future rights to this
# software under copyright law.
#
# 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 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.
#
#############################################################################
#
# It's still polite to give attribution if you do reuse this code.
#
from __future__ import with_statement
__version__ = '1.01'
import sys, struct, os
import zlib
import zipfile
import xml.etree.ElementTree as etree
NSMAP = {'adept': 'http://ns.adobe.com/adept',
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
# encoded using "replace" before writing them.
class SafeUnbuffered:
def __init__(self, stream):
self.stream = stream
self.encoding = stream.encoding
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
try:
from calibre.constants import iswindows, isosx
except:
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
def unicode_argv():
if iswindows:
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
# as a list of Unicode strings and encode them as utf-8
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"epubtest.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]
_FILENAME_LEN_OFFSET = 26
_EXTRA_LEN_OFFSET = 28
_FILENAME_OFFSET = 30
_MAX_SIZE = 64 * 1024
def uncompress(cmpdata):
dc = zlib.decompressobj(-15)
data = ''
while len(cmpdata) > 0:
if len(cmpdata) > _MAX_SIZE :
newdata = cmpdata[0:_MAX_SIZE]
cmpdata = cmpdata[_MAX_SIZE:]
else:
newdata = cmpdata
cmpdata = ''
newdata = dc.decompress(newdata)
unprocessed = dc.unconsumed_tail
if len(unprocessed) == 0:
newdata += dc.flush()
data += newdata
cmpdata += unprocessed
unprocessed = ''
return data
def getfiledata(file, zi):
# get file name length and exta data length to find start of file data
local_header_offset = zi.header_offset
file.seek(local_header_offset + _FILENAME_LEN_OFFSET)
leninfo = file.read(2)
local_name_length, = struct.unpack('<H', leninfo)
file.seek(local_header_offset + _EXTRA_LEN_OFFSET)
exinfo = file.read(2)
extra_field_length, = struct.unpack('<H', exinfo)
file.seek(local_header_offset + _FILENAME_OFFSET + local_name_length + extra_field_length)
data = None
# if not compressed we are good to go
if zi.compress_type == zipfile.ZIP_STORED:
data = file.read(zi.file_size)
# if compressed we must decompress it using zlib
if zi.compress_type == zipfile.ZIP_DEFLATED:
cmpdata = file.read(zi.compress_size)
data = uncompress(cmpdata)
return data
def encryption(infile):
# returns encryption: one of Unencrypted, Adobe, B&N and Unknown
encryption = "Unknown"
try:
with open(infile,'rb') as infileobject:
bookdata = infileobject.read(58)
# Check for Zip
if bookdata[0:0+2] == "PK":
foundrights = False
foundencryption = False
inzip = zipfile.ZipFile(infile,'r')
namelist = set(inzip.namelist())
if 'META-INF/rights.xml' not in namelist or 'META-INF/encryption.xml' not in namelist:
encryption = "Unencrypted"
else:
rights = etree.fromstring(inzip.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:
encryption = "Adobe"
elif len(bookkey) == 64:
encryption = "B&N"
else:
encryption = "Unknown"
except:
traceback.print_exc()
return encryption
def main():
argv=unicode_argv()
print encryption(argv[1])
return 0
if __name__ == "__main__":
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
sys.exit(main())

View File

@@ -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 20082012 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
@@ -58,44 +61,117 @@
# 0.17 - added support for pycrypto's DES as well # 0.17 - added support for pycrypto's DES as well
# 0.18 - on Windows try PyCrypto first and OpenSSL next # 0.18 - on Windows try PyCrypto first and OpenSSL next
# 0.19 - Modify the interface to allow use of import # 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
# 0.23 - moved unicode_argv call inside main for Windows DeDRM compatibility
__version__='0.19' __version__='0.23'
class Unbuffered: import sys, re
import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile, traceback
if 'calibre' in sys.modules:
inCalibre = True
else:
inCalibre = False
# 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): 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 Des = None
if sys.platform.startswith('win'): if iswindows:
# first try with pycrypto # first try with pycrypto
if inCalibre:
from calibre_plugins.dedrm import pycrypto_des
else:
import pycrypto_des import pycrypto_des
Des = pycrypto_des.load_pycrypto() Des = pycrypto_des.load_pycrypto()
if Des == None: if Des == None:
# they try with openssl # they try with openssl
if inCalibre:
from calibre_plugins.dedrm import openssl_des
else:
import openssl_des import openssl_des
Des = openssl_des.load_libcrypto() Des = openssl_des.load_libcrypto()
else: else:
# first try with openssl # first try with openssl
if inCalibre:
from calibre_plugins.dedrm import openssl_des
else:
import openssl_des import openssl_des
Des = openssl_des.load_libcrypto() Des = openssl_des.load_libcrypto()
if Des == None: if Des == None:
# then try with pycrypto # then try with pycrypto
if inCalibre:
from calibre_plugins.dedrm import pycrypto_des
else:
import pycrypto_des import pycrypto_des
Des = pycrypto_des.load_pycrypto() Des = pycrypto_des.load_pycrypto()
# if that did not work then use pure python implementation # if that did not work then use pure python implementation
# of DES and try to speed it up with Psycho # of DES and try to speed it up with Psycho
if Des == None: if Des == None:
if inCalibre:
from calibre_plugins.dedrm import python_des
else:
import python_des import python_des
Des = python_des.Des Des = python_des.Des
# Import Psyco if available # Import Psyco if available
@@ -121,11 +197,17 @@ logging.basicConfig()
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):
@@ -140,12 +222,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):
@@ -163,15 +258,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:]))
@@ -188,21 +283,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]
@@ -220,10 +313,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
@@ -248,10 +339,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]
@@ -266,7 +361,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):
@@ -337,6 +432,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'
@@ -349,7 +450,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:]
@@ -364,53 +465,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)
bookname = os.path.splitext(os.path.basename(infile))[0] 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 decryptBook(infile, outdir, name, cc, make_pmlz):
if make_pmlz :
# ignore specified outdir, use tempdir instead
outdir = tempfile.mkdtemp()
try:
print "Processing..."
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):
@@ -424,36 +518,48 @@ def decryptBook(infile, outdir, name, cc, make_pmlz):
myZipFile.close() myZipFile.close()
# remove temporary directory # remove temporary directory
shutil.rmtree(outdir, True) shutil.rmtree(outdir, True)
print 'output is %s' % zipname print u"Output is {0}".format(pmlzname)
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)
traceback.print_exc()
return 1 return 1
return 0 return 0
def usage(): def usage():
print "Converts DRMed eReader books to PML Source" print u"Converts DRMed eReader books to PML Source"
print "Usage:" print u"Usage:"
print " erdr2pml [options] infile.pdb [outdir] \"your name\" credit_card_number " print u" erdr2pml [options] infile.pdb [outpath] \"your name\" credit_card_number"
print " " print u" "
print "Options: " print u"Options: "
print " -h prints this message" print u" -h prints this message"
print " --make-pmlz create PMLZ instead of using output directory" print u" -p create PMLZ instead of source folder"
print " " print u" --make-pmlz create PMLZ instead of source folder"
print "Note:" print u" "
print " if ommitted, outdir defaults based on 'infile.pdb'" print u"Note:"
print " It's enough to enter the last 8 digits of the credit card number" 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 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 main(argv=None): def cli_main():
print u"eRdr2Pml v{0}. Copyright © 20092012 The Dark Reverser et al.".format(__version__)
argv=unicode_argv()
try: try:
opts, args = getopt.getopt(sys.argv[1:], "h", ["make-pmlz"]) opts, args = getopt.getopt(argv[1:], "hp", ["make-pmlz"])
except getopt.GetoptError, err: except getopt.GetoptError, err:
print str(err) print err.args[0]
usage() usage()
return 1 return 1
make_pmlz = False make_pmlz = False
@@ -461,24 +567,31 @@ def main(argv=None):
if o == "-h": if o == "-h":
usage() usage()
return 0 return 0
elif o == "-p":
make_pmlz = True
elif o == "--make-pmlz": elif o == "--make-pmlz":
make_pmlz = True make_pmlz = True
print "eRdr2Pml v%s. Copyright (c) 2009 The Dark Reverser" % __version__
if len(args)!=3 and len(args)!=4: if len(args)!=3 and len(args)!=4:
usage() usage()
return 1 return 1
if len(args)==3: if len(args)==3:
infile, name, cc = args[0], args[1], args[2] infile, name, cc = args
outdir = infile[:-4] + '_Source' if make_pmlz:
outpath = os.path.splitext(infile)[0] + u".pmlz"
else:
outpath = os.path.splitext(infile)[0] + u"_Source"
elif len(args)==4: elif len(args)==4:
infile, outdir, name, cc = args[0], args[1], args[2], args[3] infile, outpath, name, cc = args
return decryptBook(infile, outdir, name, cc, make_pmlz) print getuser_key(name,cc).encode('hex')
return decryptBook(infile, outpath, make_pmlz, getuser_key(name,cc))
if __name__ == "__main__": if __name__ == "__main__":
sys.exit(main()) sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
sys.exit(cli_main())

View File

@@ -271,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)
@@ -280,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')
@@ -288,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):
@@ -353,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)
@@ -376,10 +387,14 @@ class DocParser(object):
ws_last = int(argres) ws_last = int(argres)
elif name.endswith('word.class'): elif name.endswith('word.class'):
# we only handle spaceafter word class
try:
(cname, space) = argres.split('-',1) (cname, space) = argres.split('-',1)
if space == '' : space = '0' if space == '' : space = '0'
if (cname == 'spaceafter') and (int(space) > 0) : if (cname == 'spaceafter') and (int(space) > 0) :
word_class = 'sa' word_class = 'sa'
except:
pass
elif name.endswith('word.img.src'): elif name.endswith('word.img.src'):
result.append(('img' + word_class, int(argres))) result.append(('img' + word_class, int(argres)))
@@ -512,13 +527,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)
@@ -575,8 +657,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
@@ -585,12 +667,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:
@@ -600,25 +682,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;">&nbsp;</div>\n' hlst.append('<div style="page-break-after: always;">&nbsp;</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'
@@ -632,11 +714,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'
@@ -644,8 +726,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'
@@ -655,13 +737,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,
@@ -687,29 +769,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, gdict, fixedimage):
# create a document parser # create a document parser
dp = DocParser(flatxml, classlst, fileid, bookDir, gdict, fixedimage) dp = DocParser(flatxml, classlst, fileid, bookDir, gdict, fixedimage)
htmlpage = dp.process() htmlpage, tocinfo = dp.process()
return htmlpage return htmlpage, tocinfo

View File

@@ -10,17 +10,94 @@ from struct import unpack
class PParser(object): class PParser(object):
def __init__(self, gd, flatxml): def __init__(self, gd, flatxml, meta_array):
self.gd = gd self.gd = gd
self.flatdoc = flatxml.split('\n') self.flatdoc = flatxml.split('\n')
self.docSize = len(self.flatdoc)
self.temp = [] self.temp = []
foo = self.getData('page.h') or self.getData('book.h')
self.ph = foo[0] self.ph = -1
foo = self.getData('page.w') or self.getData('book.w') self.pw = -1
self.pw = foo[0] startpos = self.posinDoc('page.h') or self.posinDoc('book.h')
self.gx = self.getData('info.glyph.x') for p in startpos:
self.gy = self.getData('info.glyph.y') (name, argres) = self.lineinDoc(p)
self.gid = self.getData('info.glyph.glyphID') 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): def getData(self, path):
result = None result = None
cnt = len(self.flatdoc) cnt = len(self.flatdoc)
@@ -39,6 +116,23 @@ class PParser(object):
for j in xrange(0,len(argres)): for j in xrange(0,len(argres)):
argres[j] = int(argres[j]) argres[j] = int(argres[j])
return result 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): def getDataTemp(self, path):
result = None result = None
cnt = len(self.temp) cnt = len(self.temp)
@@ -58,6 +152,7 @@ class PParser(object):
for j in xrange(0,len(argres)): for j in xrange(0,len(argres)):
argres[j] = int(argres[j]) argres[j] = int(argres[j])
return result return result
def getImages(self): def getImages(self):
result = [] result = []
self.temp = self.flatdoc self.temp = self.flatdoc
@@ -69,6 +164,7 @@ class PParser(object):
src = self.getDataTemp('img.src')[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)) 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 return result
def getGlyphs(self): def getGlyphs(self):
result = [] result = []
if (self.gid != None) and (len(self.gid) > 0): if (self.gid != None) and (len(self.gid) > 0):
@@ -84,68 +180,70 @@ class PParser(object):
return result return result
def convert2SVG(gdict, flat_xml, counter, numfiles, svgDir, raw, meta_array, scaledpi): def convert2SVG(gdict, flat_xml, pageid, previd, nextid, svgDir, raw, meta_array, scaledpi):
ml = '' mlst = []
pp = PParser(gdict, flat_xml) pp = PParser(gdict, flat_xml, meta_array)
ml += '<?xml version="1.0" standalone="no"?>\n' mlst.append('<?xml version="1.0" standalone="no"?>\n')
if (raw): if (raw):
ml += '<!DOCTYPE svg PUBLIC "-//W3C/DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n' mlst.append('<!DOCTYPE svg PUBLIC "-//W3C/DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
ml += '<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('<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))
ml += '<title>Page %d - %s by %s</title>\n' % (counter, meta_array['Title'],meta_array['Authors']) mlst.append('<title>Page %d - %s by %s</title>\n' % (pageid, meta_array['Title'],meta_array['Authors']))
else: else:
ml += '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n' mlst.append('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n')
ml += '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" ><head>\n' mlst.append('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" ><head>\n')
ml += '<title>Page %d - %s by %s</title>\n' % (counter, meta_array['Title'],meta_array['Authors']) mlst.append('<title>Page %d - %s by %s</title>\n' % (pageid, meta_array['Title'],meta_array['Authors']))
ml += '<script><![CDATA[\n' mlst.append('<script><![CDATA[\n')
ml += 'function gd(){var p=window.location.href.replace(/^.*\?dpi=(\d+).*$/i,"$1");return p;}\n' mlst.append('function gd(){var p=window.location.href.replace(/^.*\?dpi=(\d+).*$/i,"$1");return p;}\n')
ml += 'var dpi=%d;\n' % scaledpi mlst.append('var dpi=%d;\n' % scaledpi)
if (counter) : if (previd) :
ml += 'var prevpage="page%04d.xhtml";\n' % (counter - 1) mlst.append('var prevpage="page%04d.xhtml";\n' % (previd))
if (counter < numfiles-1) : if (nextid) :
ml += 'var nextpage="page%04d.xhtml";\n' % (counter + 1) mlst.append('var nextpage="page%04d.xhtml";\n' % (nextid))
ml += 'var pw=%d;var ph=%d;' % (pp.pw, pp.ph) mlst.append('var pw=%d;var ph=%d;' % (pp.pw, pp.ph))
ml += 'function zoomin(){dpi=dpi*(0.8);setsize();}\n' mlst.append('function zoomin(){dpi=dpi*(0.8);setsize();}\n')
ml += 'function zoomout(){dpi=dpi*1.25;setsize();}\n' mlst.append('function zoomout(){dpi=dpi*1.25;setsize();}\n')
ml += '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 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')
ml += 'function ppage(){window.location.href=prevpage+"?dpi="+Math.round(dpi);}\n' mlst.append('function ppage(){window.location.href=prevpage+"?dpi="+Math.round(dpi);}\n')
ml += 'function npage(){window.location.href=nextpage+"?dpi="+Math.round(dpi);}\n' mlst.append('function npage(){window.location.href=nextpage+"?dpi="+Math.round(dpi);}\n')
ml += 'var gt=gd();if(gt>0){dpi=gt;}\n' mlst.append('var gt=gd();if(gt>0){dpi=gt;}\n')
ml += 'window.onload=setsize;\n' mlst.append('window.onload=setsize;\n')
ml += ']]></script>\n' mlst.append(']]></script>\n')
ml += '</head>\n' mlst.append('</head>\n')
ml += '<body onLoad="setsize();" style="background-color:#777;text-align:center;">\n' mlst.append('<body onLoad="setsize();" style="background-color:#777;text-align:center;">\n')
ml += '<div style="white-space:nowrap;">\n' mlst.append('<div style="white-space:nowrap;">\n')
if (counter == 0) : if previd == None:
ml += '<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' 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: else:
ml += '<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: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')
ml += '<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)
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): if (pp.gid != None):
ml += '<defs>\n' mlst.append('<defs>\n')
gdefs = pp.getGlyphs() gdefs = pp.getGlyphs()
for j in xrange(0,len(gdefs)): for j in xrange(0,len(gdefs)):
ml += gdefs[j] mlst.append(gdefs[j])
ml += '</defs>\n' mlst.append('</defs>\n')
img = pp.getImages() img = pp.getImages()
if (img != None): if (img != None):
for j in xrange(0,len(img)): for j in xrange(0,len(img)):
ml += img[j] mlst.append(img[j])
if (pp.gid != None): if (pp.gid != None):
for j in xrange(0,len(pp.gid)): for j in xrange(0,len(pp.gid)):
ml += '<use xlink:href="#gl%d" x="%d" y="%d" />\n' % (pp.gid[j], pp.gx[j], pp.gy[j]) 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): if (img == None or len(img) == 0) and (pp.gid == None or len(pp.gid) == 0):
ml += '<text x="10" y="10" font-family="Helvetica" font-size="100" stroke="black">This page intentionally left blank.</text>\n<text x="10" y="110" font-family="Helvetica" font-size="50" stroke="black">Until this notice unintentionally gave it content. (gensvg.py)</text>\n' 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) : if (raw) :
ml += '</svg>' mlst.append('</svg>')
else : else :
ml += '</svg></a>\n' mlst.append('</svg></a>\n')
if (counter == numfiles - 1) : if nextid == None:
ml += '<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' 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 : else :
ml += '<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('<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')
ml += '</div>\n' mlst.append('</div>\n')
ml += '<div><a href="javascript:zoomin();">zoom in</a> - <a href="javascript:zoomout();">zoom out</a></div>\n' mlst.append('<div><a href="javascript:zoomin();">zoom in</a> - <a href="javascript:zoomout();">zoom out</a></div>\n')
ml += '</body>\n' mlst.append('</body>\n')
ml += '</html>\n' mlst.append('</html>\n')
return ml return "".join(mlst)

View File

@@ -19,13 +19,28 @@ import getopt
from struct import pack from struct import pack
from struct import unpack from struct import unpack
class TpzDRMError(Exception):
pass
# local support routines # local support routines
import convert2xml if 'calibre' in sys.modules:
import flatxml2html inCalibre = True
import flatxml2svg else:
import stylexml2css inCalibre = False
if inCalibre :
from calibre_plugins.dedrm import convert2xml
from calibre_plugins.dedrm import flatxml2html
from calibre_plugins.dedrm import flatxml2svg
from calibre_plugins.dedrm import stylexml2css
else :
import convert2xml
import flatxml2html
import flatxml2svg
import stylexml2css
# global switch
buildXML = False
# Get a 7 bit encoded number from a file # Get a 7 bit encoded number from a file
def readEncodedNumber(file): def readEncodedNumber(file):
@@ -102,8 +117,9 @@ class Dictionary(object):
self.pos = val self.pos = val
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 or string table limits')
# sys.exit(-1)
def getSize(self): def getSize(self):
return self.size return self.size
def getPos(self): def getPos(self):
@@ -284,6 +300,7 @@ def generateBook(bookDir, raw, fixedimage):
if not os.path.exists(svgDir) : if not os.path.exists(svgDir) :
os.makedirs(svgDir) os.makedirs(svgDir)
if buildXML:
xmlDir = os.path.join(bookDir,'xml') xmlDir = os.path.join(bookDir,'xml')
if not os.path.exists(xmlDir) : if not os.path.exists(xmlDir) :
os.makedirs(xmlDir) os.makedirs(xmlDir)
@@ -323,34 +340,49 @@ def generateBook(bookDir, raw, fixedimage):
meta_array = getMetaArray(metaFile) meta_array = getMetaArray(metaFile)
# replace special chars in title and authors like & < > # replace special chars in title and authors like & < >
title = meta_array['Title'] title = meta_array.get('Title','No Title Provided')
title = title.replace('&','&amp;') title = title.replace('&','&amp;')
title = title.replace('<','&lt;') title = title.replace('<','&lt;')
title = title.replace('>','&gt;') title = title.replace('>','&gt;')
meta_array['Title'] = title meta_array['Title'] = title
authors = meta_array['Authors'] authors = meta_array.get('Authors','No Authors Provided')
authors = authors.replace('&','&amp;') authors = authors.replace('&','&amp;')
authors = authors.replace('<','&lt;') authors = authors.replace('<','&lt;')
authors = authors.replace('>','&gt;') authors = authors.replace('>','&gt;')
meta_array['Authors'] = authors meta_array['Authors'] = authors
if buildXML:
xname = os.path.join(xmlDir, 'metadata.xml') xname = os.path.join(xmlDir, 'metadata.xml')
metastr = '' mlst = []
for key in meta_array: for key in meta_array:
metastr += '<meta name="' + key + '" content="' + meta_array[key] + '" />\n' mlst.append('<meta name="' + key + '" content="' + meta_array[key] + '" />\n')
metastr = "".join(mlst)
mlst = None
file(xname, 'wb').write(metastr) file(xname, 'wb').write(metastr)
print 'Processing StyleSheet' print 'Processing StyleSheet'
# get some scaling info from metadata to use while processing styles # get some scaling info from metadata to use while processing styles
# and first page info
fontsize = '135' fontsize = '135'
if 'fontSize' in meta_array: if 'fontSize' in meta_array:
fontsize = meta_array['fontSize'] fontsize = meta_array['fontSize']
# also get the size of a normal text page # 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' spage = '1'
if 'firstTextPage' in meta_array: if 'firstTextPage' in meta_array:
spage = meta_array['firstTextPage'] spage = meta_array['firstTextPage']
pnum = int(spage) 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 # get page height and width from first text page for use in stylesheet scaling
pname = 'page%04d.dat' % (pnum + 1) pname = 'page%04d.dat' % (pnum + 1)
@@ -360,12 +392,37 @@ def generateBook(bookDir, raw, fixedimage):
(ph, pw) = getPageDim(flat_xml) (ph, pw) = getPageDim(flat_xml)
if (ph == '-1') or (ph == '0') : ph = '11000' if (ph == '-1') or (ph == '0') : ph = '11000'
if (pw == '-1') or (pw == '0') : pw = '8500' 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
# print ' ', 'other0000.dat' # 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') xname = os.path.join(bookDir, 'style.css')
flat_xml = convert2xml.fromData(dict, otherFile) 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) cssstr , classlst = stylexml2css.convert2CSS(flat_xml, fontsize, ph, pw)
file(xname, 'wb').write(cssstr) file(xname, 'wb').write(cssstr)
if buildXML:
xname = os.path.join(xmlDir, 'other0000.xml') xname = os.path.join(xmlDir, 'other0000.xml')
file(xname, 'wb').write(convert2xml.getXML(dict, otherFile)) file(xname, 'wb').write(convert2xml.getXML(dict, otherFile))
@@ -387,6 +444,7 @@ def generateBook(bookDir, raw, fixedimage):
fname = os.path.join(glyphsDir,filename) fname = os.path.join(glyphsDir,filename)
flat_xml = convert2xml.fromData(dict, fname) flat_xml = convert2xml.fromData(dict, fname)
if buildXML:
xname = os.path.join(xmlDir, filename.replace('.dat','.xml')) xname = os.path.join(xmlDir, filename.replace('.dat','.xml'))
file(xname, 'wb').write(convert2xml.getXML(dict, fname)) file(xname, 'wb').write(convert2xml.getXML(dict, fname))
@@ -403,101 +461,188 @@ def generateBook(bookDir, raw, fixedimage):
glyfile.close() glyfile.close()
print " " print " "
# start up the html # start up the html
# also build up tocentries while processing html
htmlFileName = "book.html" htmlFileName = "book.html"
htmlstr = '<?xml version="1.0" encoding="utf-8"?>\n' hlst = []
htmlstr += '<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.1 Strict//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11-strict.dtd">\n' hlst.append('<?xml version="1.0" encoding="utf-8"?>\n')
htmlstr += '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">\n' hlst.append('<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.1 Strict//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11-strict.dtd">\n')
htmlstr += '<head>\n' hlst.append('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">\n')
htmlstr += '<meta http-equiv="content-type" content="text/html; charset=utf-8"/>\n' hlst.append('<head>\n')
htmlstr += '<title>' + meta_array['Title'] + ' by ' + meta_array['Authors'] + '</title>\n' hlst.append('<meta http-equiv="content-type" content="text/html; charset=utf-8"/>\n')
htmlstr += '<meta name="Author" content="' + meta_array['Authors'] + '" />\n' hlst.append('<title>' + meta_array['Title'] + ' by ' + meta_array['Authors'] + '</title>\n')
htmlstr += '<meta name="Title" content="' + meta_array['Title'] + '" />\n' hlst.append('<meta name="Author" content="' + meta_array['Authors'] + '" />\n')
htmlstr += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n' hlst.append('<meta name="Title" content="' + meta_array['Title'] + '" />\n')
htmlstr += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n' if 'ASIN' in meta_array:
htmlstr += '<link href="style.css" rel="stylesheet" type="text/css" />\n' hlst.append('<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n')
htmlstr += '</head>\n<body>\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' print 'Processing Pages'
# Books are at 1440 DPI. This is rendering at twice that size for # Books are at 1440 DPI. This is rendering at twice that size for
# readability when rendering to the screen. # readability when rendering to the screen.
scaledpi = 1440.0 scaledpi = 1440.0
svgindex = '<?xml version="1.0" encoding="utf-8"?>\n'
svgindex += '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n'
svgindex += '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" >'
svgindex += '<head>\n'
svgindex += '<title>' + meta_array['Title'] + '</title>\n'
svgindex += '<meta name="Author" content="' + meta_array['Authors'] + '" />\n'
svgindex += '<meta name="Title" content="' + meta_array['Title'] + '" />\n'
svgindex += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
svgindex += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
svgindex += '</head>\n'
svgindex += '<body>\n'
filenames = os.listdir(pageDir) filenames = os.listdir(pageDir)
filenames = sorted(filenames) filenames = sorted(filenames)
numfiles = len(filenames) numfiles = len(filenames)
counter = 0
xmllst = []
elst = []
for filename in filenames: for filename in filenames:
# print ' ', filename # print ' ', filename
print ".", print ".",
fname = os.path.join(pageDir,filename) fname = os.path.join(pageDir,filename)
flat_xml = convert2xml.fromData(dict, fname) 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')) xname = os.path.join(xmlDir, filename.replace('.dat','.xml'))
file(xname, 'wb').write(convert2xml.getXML(dict, fname)) file(xname, 'wb').write(convert2xml.getXML(dict, fname))
# first get the html # first get the html
htmlstr += flatxml2html.convert2HTML(flat_xml, classlst, fname, bookDir, gd, fixedimage) pagehtml, tocinfo = flatxml2html.convert2HTML(flat_xml, classlst, fname, bookDir, gd, fixedimage)
elst.append(tocinfo)
hlst.append(pagehtml)
# now get the svg image of the page # finish up the html string and output it
svgxml = flatxml2svg.convert2SVG(gd, flat_xml, counter, numfiles, svgDir, raw, meta_array, scaledpi) 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) : if (raw) :
pfile = open(os.path.join(svgDir,filename.replace('.dat','.svg')), 'w') pfile = open(os.path.join(svgDir,'page%04d.svg' % pageid),'w')
svgindex += '<a href="svg/page%04d.svg">Page %d</a>\n' % (counter, counter) slst.append('<a href="svg/page%04d.svg">Page %d</a>\n' % (pageid, pageid))
else : else :
pfile = open(os.path.join(svgDir,'page%04d.xhtml' % counter), 'w') pfile = open(os.path.join(svgDir,'page%04d.xhtml' % pageid), 'w')
svgindex += '<a href="svg/page%04d.xhtml">Page %d</a>\n' % (counter, counter) slst.append('<a href="svg/page%04d.xhtml">Page %d</a>\n' % (pageid, pageid))
previd = pageid
pfile.write(svgxml) pfile.write(svgxml)
pfile.close() pfile.close()
counter += 1 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 " " print " "
# finish up the html string and output it
htmlstr += '</body>\n</html>\n'
file(os.path.join(bookDir, htmlFileName), 'wb').write(htmlstr)
# finish up the svg index string and output it
svgindex += '</body>\n</html>\n'
file(os.path.join(bookDir, 'index_svg.xhtml'), 'wb').write(svgindex)
# build the opf file # build the opf file
opfname = os.path.join(bookDir, 'book.opf') opfname = os.path.join(bookDir, 'book.opf')
opfstr = '<?xml version="1.0" encoding="utf-8"?>\n' olst = []
opfstr += '<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="guid_id">\n' 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 # adding metadata
opfstr += ' <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">\n' olst.append(' <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">\n')
opfstr += ' <dc:identifier opf:scheme="GUID" id="guid_id">' + meta_array['GUID'] + '</dc:identifier>\n' if 'GUID' in meta_array:
opfstr += ' <dc:identifier opf:scheme="ASIN">' + meta_array['ASIN'] + '</dc:identifier>\n' olst.append(' <dc:identifier opf:scheme="GUID" id="guid_id">' + meta_array['GUID'] + '</dc:identifier>\n')
opfstr += ' <dc:identifier opf:scheme="oASIN">' + meta_array['oASIN'] + '</dc:identifier>\n' if 'ASIN' in meta_array:
opfstr += ' <dc:title>' + meta_array['Title'] + '</dc:title>\n' olst.append(' <dc:identifier opf:scheme="ASIN">' + meta_array['ASIN'] + '</dc:identifier>\n')
opfstr += ' <dc:creator opf:role="aut">' + meta_array['Authors'] + '</dc:creator>\n' if 'oASIN' in meta_array:
opfstr += ' <dc:language>en</dc:language>\n' olst.append(' <dc:identifier opf:scheme="oASIN">' + meta_array['oASIN'] + '</dc:identifier>\n')
opfstr += ' <dc:date>' + meta_array['UpdateTime'] + '</dc:date>\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: if isCover:
opfstr += ' <meta name="cover" content="bookcover"/>\n' olst.append(' <meta name="cover" content="bookcover"/>\n')
opfstr += ' </metadata>\n' olst.append(' </metadata>\n')
opfstr += '<manifest>\n' olst.append('<manifest>\n')
opfstr += ' <item id="book" href="book.html" media-type="application/xhtml+xml"/>\n' olst.append(' <item id="book" href="book.html" media-type="application/xhtml+xml"/>\n')
opfstr += ' <item id="stylesheet" href="style.css" media-type="text.css"/>\n' olst.append(' <item id="stylesheet" href="style.css" media-type="text/css"/>\n')
# adding image files to manifest # adding image files to manifest
filenames = os.listdir(imgDir) filenames = os.listdir(imgDir)
filenames = sorted(filenames) filenames = sorted(filenames)
@@ -507,17 +652,19 @@ def generateBook(bookDir, raw, fixedimage):
imgext = 'jpeg' imgext = 'jpeg'
if imgext == '.svg': if imgext == '.svg':
imgext = 'svg+xml' imgext = 'svg+xml'
opfstr += ' <item id="' + imgname + '" href="img/' + filename + '" media-type="image/' + imgext + '"/>\n' olst.append(' <item id="' + imgname + '" href="img/' + filename + '" media-type="image/' + imgext + '"/>\n')
if isCover: if isCover:
opfstr += ' <item id="bookcover" href="cover.jpg" media-type="image/jpeg" />\n' olst.append(' <item id="bookcover" href="cover.jpg" media-type="image/jpeg" />\n')
opfstr += '</manifest>\n' olst.append('</manifest>\n')
# adding spine # adding spine
opfstr += '<spine>\n <itemref idref="book" />\n</spine>\n' olst.append('<spine>\n <itemref idref="book" />\n</spine>\n')
if isCover: if isCover:
opfstr += ' <guide>\n' olst.append(' <guide>\n')
opfstr += ' <reference href="cover.jpg" type="cover" title="Cover"/>\n' olst.append(' <reference href="cover.jpg" type="cover" title="Cover"/>\n')
opfstr += ' </guide>\n' olst.append(' </guide>\n')
opfstr += '</package>\n' olst.append('</package>\n')
opfstr = "".join(olst)
olst = None
file(opfname, 'wb').write(opfstr) file(opfname, 'wb').write(opfstr)
print 'Processing Complete' print 'Processing Complete'
@@ -538,7 +685,6 @@ def usage():
def main(argv): def main(argv):
bookDir = '' bookDir = ''
if len(argv) == 0: if len(argv) == 0:
argv = sys.argv argv = sys.argv
@@ -555,7 +701,7 @@ def main(argv):
return 1 return 1
raw = 0 raw = 0
fixedimage = False fixedimage = True
for o, a in opts: for o, a in opts:
if o =="-h": if o =="-h":
usage() usage()

View File

@@ -1,37 +1,116 @@
#! /usr/bin/python #!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import with_statement from __future__ import with_statement
# ignobleepub.pyw, version 3.4 # ignobleepub.pyw, version 3.8
# Copyright © 2009-2010 by i♥cabbages
# To run this program install Python 2.6 from <http://www.python.org/download/> # Released under the terms of the GNU General Public Licence, version 3
# and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto # <http://www.gnu.org/licenses/>
# (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. # Modified 20102013 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: # Revision history:
# 1 - Initial release # 1 - Initial release
# 2 - Added OS X support by using OpenSSL when available # 2 - Added OS X support by using OpenSSL when available
# 3 - screen out improper key lengths to prevent segfaults on Linux # 3 - screen out improper key lengths to prevent segfaults on Linux
# 3.1 - Allow Windows versions of libcrypto to be found # 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.2 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml
# 3.3 - On Windows try PyCrypto first and OpenSSL next # 3.3 - On Windows try PyCrypto first, OpenSSL next
# 3.4 - Modify interace to allow use with import # 3.4 - Modify interface 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
# 3.7 - Tweaked to match ineptepub more closely
# 3.8 - Fixed to retain zip file metadata (e.g. file modification date)
# 3.9 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 4.0 - Work if TkInter is missing
"""
Decrypt Barnes & Noble encrypted ePub books.
"""
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = "4.0"
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 ZipInfo, 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 IGNOBLEError(Exception): class IGNOBLEError(Exception):
pass pass
@@ -41,10 +120,11 @@ 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')
if libcrypto is None: if libcrypto is None:
raise IGNOBLEError('libcrypto not found') raise IGNOBLEError('libcrypto not found')
libcrypto = CDLL(libcrypto) libcrypto = CDLL(libcrypto)
@@ -65,9 +145,6 @@ def _load_crypto_libcrypto():
func.argtypes = argtypes func.argtypes = argtypes
return func 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', AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
[c_char_p, c_int, AES_KEY_p]) [c_char_p, c_int, AES_KEY_p])
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt', AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
@@ -100,7 +177,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)
@@ -122,28 +199,13 @@ def _load_crypto():
AES = _load_crypto() AES = _load_crypto()
"""
Decrypt Barnes & Noble ADEPT encrypted EPUB books.
"""
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml') META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
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#'}
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): class Decryptor(object):
def __init__(self, bookkey, encryption): def __init__(self, bookkey, encryption):
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag) enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
# self._aes = AES.new(bookkey, AES.MODE_CBC)
self._aes = AES(bookkey) self._aes = AES(bookkey)
encryption = etree.fromstring(encryption) encryption = etree.fromstring(encryption)
self._encrypted = encrypted = set() self._encrypted = encrypted = set()
@@ -151,8 +213,8 @@ class Decryptor(object):
enc('CipherReference')) enc('CipherReference'))
for elem in encryption.findall(expr): for elem in encryption.findall(expr):
path = elem.get('URI', None) path = elem.get('URI', None)
path = path.encode('utf-8')
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):
@@ -170,47 +232,156 @@ class Decryptor(object):
data = self.decompress(data) data = self.decompress(data)
return 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
class DecryptionDialog(Tkinter.Frame): def decryptBook(keyb64, inpath, outpath):
if AES is None:
raise IGNOBLEError(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:
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) != 64:
print u"{0:s} is not a secure Barnes & Noble ePub.".format(os.path.basename(inpath))
return 1
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')
zi.compress_type=ZIP_STORED
try:
# if the mimetype is present, get its info, including time-stamp
oldzi = inf.getinfo('mimetype')
# copy across fields to be preserved
zi.date_time = oldzi.date_time
zi.comment = oldzi.comment
zi.extra = oldzi.extra
zi.internal_attr = oldzi.internal_attr
# external attributes are dependent on the create system, so copy both.
zi.external_attr = oldzi.external_attr
zi.create_system = oldzi.create_system
except:
pass
outf.writestr(zi, inf.read('mimetype'))
for path in namelist:
data = inf.read(path)
zi = ZipInfo(path)
zi.compress_type=ZIP_DEFLATED
try:
# get the file info, including time-stamp
oldzi = inf.getinfo(path)
# copy across useful fields
zi.date_time = oldzi.date_time
zi.comment = oldzi.comment
zi.extra = oldzi.extra
zi.internal_attr = oldzi.internal_attr
# external attributes are dependent on the create system, so copy both.
zi.external_attr = oldzi.external_attr
zi.create_system = oldzi.create_system
except:
pass
outf.writestr(zi, decryptor.decrypt(path, data))
except:
print u"Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc())
return 2
return 0
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv()
progname = os.path.basename(argv[0])
if len(argv) != 4:
print u"usage: {0} <keyfile.b64> <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():
try:
import Tkinter
import Tkconstants
import tkMessageBox
import traceback
except:
return cli_main()
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('bnepubkey.b64'): if os.path.exists(u"bnepubkey.b64"):
self.keypath.insert(0, 'bnepubkey.b64') self.keypath.insert(0, u"bnepubkey.b64")
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 B&N EPUB key file', parent=None, title=u"Select Barnes & Noble \'.b64\' key file",
defaultextension='.b64', defaultextension=u".b64",
filetypes=[('base64-encoded files', '.b64'), filetypes=[('base64-encoded files', '.b64'),
('All Files', '.*')]) ('All Files', '.*')])
if keypath: if keypath:
@@ -221,9 +392,8 @@ class DecryptionDialog(Tkinter.Frame):
def get_inpath(self): def get_inpath(self):
inpath = tkFileDialog.askopenfilename( inpath = tkFileDialog.askopenfilename(
parent=None, title='Select B&N-encrypted EPUB file to decrypt', parent=None, title=u"Select B&N-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)
@@ -232,9 +402,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)
@@ -246,90 +415,37 @@ 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.args[0])
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 decryptBook(keypath, inpath, outpath):
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
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:]
return decryptBook(keypath, inpath, outpath)
def gui_main():
root = Tkinter.Tk() root = Tkinter.Tk()
if AES is None: root.title(u"Barnes & Noble ePub Decrypter v.{0}".format(__version__))
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.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)
root.mainloop() root.mainloop()
return 0 return 0
if __name__ == '__main__': if __name__ == '__main__':
if len(sys.argv) > 1: if len(sys.argv) > 1:
sys.exit(cli_main()) sys.exit(cli_main())

View File

@@ -1,13 +1,27 @@
#! /usr/bin/python #!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import with_statement from __future__ import with_statement
# ignoblekeygen.pyw, version 2.3 # ignoblekeygen.pyw, version 2.5
# Copyright © 2009-2010 i♥cabbages
# To run this program install Python 2.6 from <http://www.python.org/download/> # Released under the terms of the GNU General Public Licence, version 3
# and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto # <http://www.gnu.org/licenses/>
# (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. # Modified 20102013 by some_updates, DiapDealer and Apprentice Alf
# Windows users: Before running this program, you must first install Python.
# We recommend ActiveState Python 2.7.X for Windows (x86) from
# http://www.activestate.com/activepython/downloads.
# You must also install PyCrypto from
# http://www.voidspace.org.uk/python/modules.shtml#pycrypto
# (make certain to install the version for Python 2.7).
# Then save this script file as 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 (python ignoblekeygen.pyw) or by double-clicking
# it when it has been associated with PythonLauncher.
# Revision history: # Revision history:
# 1 - Initial release # 1 - Initial release
@@ -15,40 +29,98 @@ from __future__ import with_statement
# 2.1 - Allow Windows versions of libcrypto to be found # 2.1 - Allow Windows versions of libcrypto to be found
# 2.2 - On Windows try PyCrypto first and then OpenSSL next # 2.2 - On Windows try PyCrypto first and then OpenSSL next
# 2.3 - Modify interface to allow use of import # 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
# 2.6 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 2.7 - Work if TkInter is missing
""" """
Generate Barnes & Noble EPUB user key from name and credit card number. Generate Barnes & Noble EPUB user key from name and credit card number.
""" """
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = "2.7"
import sys import sys
import os import os
import hashlib import hashlib
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 '?'. So use shell32.GetCommandLineArgvW to get sys.argv
# as a list of Unicode strings and encode them as utf-8
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"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]
# 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): class IGNOBLEError(Exception):
pass pass
def _load_crypto_libcrypto(): def _load_crypto_libcrypto():
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \ from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
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')
if libcrypto is None: if libcrypto is None:
print 'libcrypto not found'
raise IGNOBLEError('libcrypto not found') raise IGNOBLEError('libcrypto not found')
libcrypto = CDLL(libcrypto) libcrypto = CDLL(libcrypto)
@@ -73,6 +145,7 @@ def _load_crypto_libcrypto():
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt', AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p, [c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
c_int]) c_int])
class AES(object): class AES(object):
def __init__(self, userkey, iv): def __init__(self, userkey, iv):
self._blocksize = len(userkey) self._blocksize = len(userkey)
@@ -91,7 +164,6 @@ def _load_crypto_libcrypto():
return AES return AES
def _load_crypto_pycrypto(): def _load_crypto_pycrypto():
from Crypto.Cipher import AES as _AES from Crypto.Cipher import AES as _AES
@@ -123,55 +195,90 @@ def normalize_name(name):
return ''.join(x for x in name.lower() if x != ' ') return ''.join(x for x in name.lower() if x != ' ')
def generate_keyfile(name, ccn, outpath): 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' name = normalize_name(name) + '\x00'
ccn = ccn + '\x00' ccn = normalize_name(ccn) + '\x00'
name_sha = hashlib.sha1(name).digest()[:16] name_sha = hashlib.sha1(name).digest()[:16]
ccn_sha = hashlib.sha1(ccn).digest()[:16] ccn_sha = hashlib.sha1(ccn).digest()[:16]
both_sha = hashlib.sha1(name + ccn).digest() both_sha = hashlib.sha1(name + ccn).digest()
aes = AES(ccn_sha, name_sha) aes = AES(ccn_sha, name_sha)
crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c)) crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c))
userkey = hashlib.sha1(crypt).digest() userkey = hashlib.sha1(crypt).digest()
with open(outpath, 'wb') as f: return userkey.encode('base64')
f.write(userkey.encode('base64'))
return userkey
class DecryptionDialog(Tkinter.Frame):
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
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():
try:
import Tkinter
import Tkconstants
import tkMessageBox
import traceback
except:
return cli_main()
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='Enter parameters') self.status = Tkinter.Label(self, text=u"Enter parameters")
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='Name').grid(row=1) Tkinter.Label(body, text=u"Account Name").grid(row=0)
self.name = Tkinter.Entry(body, width=30) self.name = Tkinter.Entry(body, width=40)
self.name.grid(row=1, column=1, sticky=sticky) self.name.grid(row=0, column=1, sticky=sticky)
Tkinter.Label(body, text='CC#').grid(row=2) Tkinter.Label(body, text=u"CC#").grid(row=1)
self.ccn = Tkinter.Entry(body, width=30) self.ccn = Tkinter.Entry(body, width=40)
self.ccn.grid(row=2, column=1, sticky=sticky) self.ccn.grid(row=1, column=1, sticky=sticky)
Tkinter.Label(body, text='Output file').grid(row=0) Tkinter.Label(body, text=u"Output file").grid(row=2)
self.keypath = Tkinter.Entry(body, width=30) self.keypath = Tkinter.Entry(body, width=40)
self.keypath.grid(row=0, column=1, sticky=sticky) self.keypath.grid(row=2, column=1, sticky=sticky)
self.keypath.insert(0, 'bnepubkey.b64') self.keypath.insert(2, u"bnepubkey.b64")
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=2, column=2)
buttons = Tkinter.Frame(self) buttons = Tkinter.Frame(self)
buttons.pack() buttons.pack()
botton = Tkinter.Button( botton = Tkinter.Button(
buttons, text="Generate", width=10, command=self.generate) buttons, text=u"Generate", width=10, command=self.generate)
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.asksaveasfilename( keypath = tkFileDialog.asksaveasfilename(
parent=None, title='Select B&N EPUB key file to produce', parent=None, title=u"Select B&N ePub key file to produce",
defaultextension='.b64', defaultextension=u".b64",
filetypes=[('base64-encoded files', '.b64'), filetypes=[('base64-encoded files', '.b64'),
('All Files', '.*')]) ('All Files', '.*')])
if keypath: if keypath:
@@ -185,39 +292,23 @@ class DecryptionDialog(Tkinter.Frame):
ccn = self.ccn.get() ccn = self.ccn.get()
keypath = self.keypath.get() keypath = self.keypath.get()
if not name: if not name:
self.status['text'] = 'Name not specified' self.status['text'] = u"Name not specified"
return return
if not ccn: if not ccn:
self.status['text'] = 'Credit card number not specified' self.status['text'] = u"Credit card number not specified"
return return
if not keypath: if not keypath:
self.status['text'] = 'Output keyfile path not specified' self.status['text'] = u"Output keyfile path not specified"
return return
self.status['text'] = 'Generating...' self.status['text'] = u"Generating..."
try: try:
generate_keyfile(name, ccn, keypath) userkey = generate_key(name, ccn)
except Exception, e: except Exception, e:
self.status['text'] = 'Error: ' + str(e) self.status['text'] = u"Error: (0}".format(e.args[0])
return return
self.status['text'] = 'Keyfile successfully generated' open(keypath,'wb').write(userkey)
self.status['text'] = u"Keyfile successfully generated"
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
def gui_main():
root = Tkinter.Tk() root = Tkinter.Tk()
if AES is None: if AES is None:
root.withdraw() root.withdraw()
@@ -226,7 +317,7 @@ def gui_main():
"This script requires OpenSSL or PyCrypto, which must be installed " "This script requires OpenSSL or PyCrypto, which must be installed "
"separately. Read the top-of-script comment for details.") "separately. Read the top-of-script comment for details.")
return 1 return 1
root.title('Ignoble EPUB Keyfile Generator') root.title(u"Barnes & Noble ePub Keyfile Generator v.{0}".format(__version__))
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)

View File

@@ -1,13 +1,15 @@
#! /usr/bin/python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import with_statement from __future__ import with_statement
# ineptepub.pyw, version 5.6 # ineptepub.pyw, version 5.9
# Copyright © 2009-2010 i♥cabbages # Copyright © 2009-2010 by 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/>
# Modified 20102013 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
@@ -30,23 +32,87 @@ from __future__ import with_statement
# 5.4 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml # 5.4 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml
# 5.5 - On Windows try PyCrypto first, OpenSSL next # 5.5 - On Windows try PyCrypto first, OpenSSL next
# 5.6 - Modify interface to allow use with import # 5.6 - Modify interface to allow use with import
# 5.7 - Fix for potential problem with PyCrypto
# 5.8 - Revised to allow use in calibre plugins to eliminate need for duplicate code
# 5.9 - Fixed to retain zip file metadata (e.g. file modification date)
# 6.0 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 6.1 - Work if TkInter is missing
""" """
Decrypt Adobe ADEPT-encrypted EPUB books. Decrypt Adobe Digital Editions encrypted ePub books.
""" """
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = "6.1"
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 ZipInfo, 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
@@ -56,7 +122,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')
@@ -235,7 +301,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)
@@ -270,19 +336,13 @@ def _load_crypto():
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')
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#'}
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): class Decryptor(object):
def __init__(self, bookkey, encryption): def __init__(self, bookkey, encryption):
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag) enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
@@ -312,47 +372,159 @@ class Decryptor(object):
data = self.decompress(data) data = self.decompress(data)
return data return data
# check file to make check whether it's probably an Adobe Adept encrypted ePub
def adeptBook(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) == 172:
return True
except:
# if we couldn't check, assume it is
return True
return False
class DecryptionDialog(Tkinter.Frame): 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'))
# Padded as per RSAES-PKCS1-v1_5
if bookkey[-17] != '\x00':
print u"Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath))
return 2
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')
zi.compress_type=ZIP_STORED
try:
# if the mimetype is present, get its info, including time-stamp
oldzi = inf.getinfo('mimetype')
# copy across fields to be preserved
zi.date_time = oldzi.date_time
zi.comment = oldzi.comment
zi.extra = oldzi.extra
zi.internal_attr = oldzi.internal_attr
# external attributes are dependent on the create system, so copy both.
zi.external_attr = oldzi.external_attr
zi.create_system = oldzi.create_system
except:
pass
outf.writestr(zi, inf.read('mimetype'))
for path in namelist:
data = inf.read(path)
zi = ZipInfo(path)
zi.compress_type=ZIP_DEFLATED
try:
# get the file info, including time-stamp
oldzi = inf.getinfo(path)
# copy across useful fields
zi.date_time = oldzi.date_time
zi.comment = oldzi.comment
zi.extra = oldzi.extra
zi.internal_attr = oldzi.internal_attr
# external attributes are dependent on the create system, so copy both.
zi.external_attr = oldzi.external_attr
zi.create_system = oldzi.create_system
except:
pass
outf.writestr(zi, decryptor.decrypt(path, data))
except:
print u"Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc())
return 2
return 0
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
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():
try:
import Tkinter
import Tkconstants
import tkMessageBox
import traceback
except:
return cli_main()
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)
@@ -362,9 +534,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)
@@ -373,9 +544,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)
@@ -387,83 +557,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.args[0])
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 decryptBook(keypath, inpath, outpath):
with open(keypath, 'rb') as f:
keyder = f.read()
rsa = RSA(keyder)
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 ADEPTError('%s: not an 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 = 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))
return 0
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:]
return decryptBook(keypath, inpath, outpath)
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)

View File

@@ -1,13 +1,25 @@
#! /usr/bin/env python #! /usr/bin/python
# ineptpdf.pyw, version 7.9 # -*- coding: utf-8 -*-
from __future__ import with_statement from __future__ import with_statement
# To run this program install Python 2.6 from http://www.python.org/download/ # ineptpdf.pyw, version 7.11
# and OpenSSL (already installed on Mac OS X and Linux) OR # Copyright © 2009-2010 by i♥cabbages
# 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 # Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/>
# Modified 20102012 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
# ineptpdf.pyw and double-click on it to run it. # ineptpdf.pyw and double-click on it to run it.
#
# Mac OS X users: Save this script file as ineptpdf.pyw. You can run this
# program from the command line (pythonw ineptpdf.pyw) or by double-clicking
# it when it has been associated with PythonLauncher.
# Revision history: # Revision history:
# 1 - Initial release # 1 - Initial release
@@ -34,12 +46,19 @@ from __future__ import with_statement
# 7.7 - On Windows try PyCrypto first and OpenSSL next # 7.7 - On Windows try PyCrypto first and OpenSSL next
# 7.8 - Modify interface to allow use of import # 7.8 - Modify interface to allow use of import
# 7.9 - Bug fix for some session key errors when len(bookkey) > length required # 7.9 - Bug fix for some session key errors when len(bookkey) > length required
# 7.10 - Various tweaks to fix minor problems.
# 7.11 - More tweaks to fix minor problems.
# 7.12 - Revised to allow use in calibre plugins to eliminate need for duplicate code
# 7.13 - Fixed erroneous mentions of ineptepub
# 7.14 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 8.0 - Work if TkInter is missing
""" """
Decrypts Adobe ADEPT-encrypted PDF files. Decrypts Adobe ADEPT-encrypted PDF files.
""" """
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = "8.0"
import sys import sys
import os import os
@@ -49,10 +68,63 @@ import struct
import hashlib import hashlib
from itertools import chain, islice from itertools import chain, islice
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)
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"ineptpdf.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
@@ -157,6 +229,7 @@ def _load_crypto_libcrypto():
return out.raw return out.raw
class AES(object): class AES(object):
MODE_CBC = 0
@classmethod @classmethod
def new(cls, userkey, mode, iv): def new(cls, userkey, mode, iv):
self = AES() self = AES()
@@ -292,6 +365,7 @@ def _load_crypto_pycrypto():
return self._arc4.decrypt(data) return self._arc4.decrypt(data)
class AES(object): class AES(object):
MODE_CBC = _AES.MODE_CBC
@classmethod @classmethod
def new(cls, userkey, mode, iv): def new(cls, userkey, mode, iv):
self = AES() self = AES()
@@ -1516,9 +1590,7 @@ class PDFDocument(object):
def initialize_ebx(self, password, docid, param): def initialize_ebx(self, password, docid, param):
self.is_printable = self.is_modifiable = self.is_extractable = True self.is_printable = self.is_modifiable = self.is_extractable = True
with open(password, 'rb') as f: rsa = RSA(password)
keyder = f.read()
rsa = RSA(keyder)
length = int_value(param.get('Length', 0)) / 8 length = int_value(param.get('Length', 0)) / 8
rights = str_value(param.get('ADEPT_LICENSE')).decode('base64') rights = str_value(param.get('ADEPT_LICENSE')).decode('base64')
rights = zlib.decompress(rights, -15) rights = zlib.decompress(rights, -15)
@@ -1903,14 +1975,14 @@ class PDFObjStrmParser(PDFParser):
### My own code, for which there is none else to blame ### My own code, for which there is none else to blame
class PDFSerializer(object): class PDFSerializer(object):
def __init__(self, inf, keypath): def __init__(self, inf, userkey):
global GEN_XREF_STM, gen_xref_stm global GEN_XREF_STM, gen_xref_stm
gen_xref_stm = GEN_XREF_STM > 1 gen_xref_stm = GEN_XREF_STM > 1
self.version = inf.read(8) self.version = inf.read(8)
inf.seek(0) inf.seek(0)
self.doc = doc = PDFDocument() self.doc = doc = PDFDocument()
parser = PDFParser(doc, inf) parser = PDFParser(doc, inf)
doc.initialize(keypath) doc.initialize(userkey)
self.objids = objids = set() self.objids = objids = set()
for xref in reversed(doc.xrefs): for xref in reversed(doc.xrefs):
trailer = xref.trailer trailer = xref.trailer
@@ -2093,75 +2165,117 @@ class PDFSerializer(object):
self.write('endobj\n') self.write('endobj\n')
class DecryptionDialog(Tkinter.Frame):
def decryptBook(userkey, inpath, outpath):
if RSA is None:
raise ADEPTError(u"PyCrypto or OpenSSL must be installed.")
with open(inpath, 'rb') as inf:
try:
serializer = PDFSerializer(inf, userkey)
except:
print u"Error serializing pdf {0}. Probably wrong key.".format(os.path.basename(inpath))
return 2
# hope this will fix the 'bad file descriptor' problem
with open(outpath, 'wb') as outf:
# help construct to make sure the method runs to the end
try:
serializer.dump(outf)
except Exception, e:
print u"error writing pdf: {0}".format(e.args[0])
return 2
return 0
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv()
progname = os.path.basename(argv[0])
if len(argv) != 4:
print u"usage: {0} <keyfile.der> <inbook.pdf> <outbook.pdf>".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():
try:
import Tkinter
import Tkconstants
import tkMessageBox
import traceback
except:
return cli_main()
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)
ltext='Select file for decryption\n' self.status = Tkinter.Label(self, text=u"Select files for decryption")
self.status = Tkinter.Label(self, text=ltext)
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(os.path.realpath(keypath)) keypath = os.path.normpath(keypath)
self.keypath.delete(0, Tkconstants.END) self.keypath.delete(0, Tkconstants.END)
self.keypath.insert(0, keypath) self.keypath.insert(0, keypath)
return return
def get_inpath(self): def get_inpath(self):
inpath = tkFileDialog.askopenfilename( inpath = tkFileDialog.askopenfilename(
parent=None, title='Select ADEPT encrypted PDF file to decrypt', parent=None, title=u"Select ADEPT-encrypted PDF file to decrypt",
defaultextension='.pdf', filetypes=[('PDF files', '.pdf'), defaultextension=u".pdf", filetypes=[('PDF files', '.pdf')])
('All files', '.*')])
if inpath: if inpath:
inpath = os.path.normpath(os.path.realpath(inpath)) inpath = os.path.normpath(inpath)
self.inpath.delete(0, Tkconstants.END) self.inpath.delete(0, Tkconstants.END)
self.inpath.insert(0, inpath) self.inpath.insert(0, inpath)
return return
def get_outpath(self): def get_outpath(self):
outpath = tkFileDialog.asksaveasfilename( outpath = tkFileDialog.asksaveasfilename(
parent=None, title='Select unencrypted PDF file to produce', parent=None, title=u"Select unencrypted PDF file to produce",
defaultextension='.pdf', filetypes=[('PDF files', '.pdf'), defaultextension=u".pdf", filetypes=[('PDF files', '.pdf')])
('All files', '.*')])
if outpath: if outpath:
outpath = os.path.normpath(os.path.realpath(outpath)) outpath = os.path.normpath(outpath)
self.outpath.delete(0, Tkconstants.END) self.outpath.delete(0, Tkconstants.END)
self.outpath.insert(0, outpath) self.outpath.insert(0, outpath)
return return
@@ -2171,56 +2285,30 @@ 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):
# keyfile doesn't exist self.status['text'] = u"Specified key file does not exist"
self.status['text'] = 'Specified Adept 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
# patch for non-ascii characters userkey = open(keypath,'rb').read()
argv = [sys.argv[0], keypath, inpath, outpath] self.status['text'] = u"Decrypting..."
self.status['text'] = 'Processing ...'
try: try:
cli_main(argv) decrypt_status = decryptBook(userkey, inpath, outpath)
except Exception, a: except Exception, e:
self.status['text'] = 'Error: ' + str(a) self.status['text'] = u"Error; {0}".format(e.args[0])
return
self.status['text'] = 'File successfully decrypted.\n'+\
'Close this window or decrypt another pdf file.'
return return
if decrypt_status == 0:
self.status['text'] = u"File successfully decrypted"
else:
self.status['text'] = u"The was an error decrypting the file."
def decryptBook(keypath, inpath, outpath):
with open(inpath, 'rb') as inf:
serializer = PDFSerializer(inf, keypath)
# hope this will fix the 'bad file descriptor' problem
with open(outpath, 'wb') as outf:
# help construct to make sure the method runs to the end
serializer.dump(outf)
return 0
def cli_main(argv=sys.argv):
progname = os.path.basename(argv[0])
if RSA 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:]
return decryptBook(keypath, inpath, outpath)
def gui_main():
root = Tkinter.Tk() root = Tkinter.Tk()
if RSA is None: if RSA is None:
root.withdraw() root.withdraw()
@@ -2229,7 +2317,7 @@ def gui_main():
"This script requires OpenSSL or PyCrypto, which must be installed " "This script requires OpenSSL or PyCrypto, which must be installed "
"separately. Read the top-of-script comment for details.") "separately. Read the top-of-script comment for details.")
return 1 return 1
root.title('INEPT PDF Decrypter') root.title(u"Adobe Adept PDF Decrypter v.{0}".format(__version__))
root.resizable(True, False) root.resizable(True, False)
root.minsize(370, 0) root.minsize(370, 0)
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1) DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)

View File

@@ -1,333 +0,0 @@
# engine to remove drm from Kindle for Mac books
# for personal use for archiving and converting your ebooks
# PLEASE DO NOT PIRATE!
# We want all authors and Publishers, and eBook stores to live long and prosperous lives
#
# it borrows heavily from works by CMBDTC, IHeartCabbages, skindle,
# unswindle, DiapDealer, some_updates and many many others
from __future__ import with_statement
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 os, csv, getopt
from struct import pack
from struct import unpack
import zlib
# for handling sub processes
import subprocess
from subprocess import Popen, PIPE, STDOUT
import subasyncio
from subasyncio import Process
#Exception Handling
class K4MDEDRMError(Exception):
pass
class K4MDEDRMFatal(Exception):
pass
#
# crypto routines
#
import hashlib
def MD5(message):
ctx = hashlib.md5()
ctx.update(message)
return ctx.digest()
def SHA1(message):
ctx = hashlib.sha1()
ctx.update(message)
return ctx.digest()
def SHA256(message):
ctx = hashlib.sha256()
ctx.update(message)
return ctx.digest()
# interface to needed routines in openssl's libcrypto
def _load_crypto_libcrypto():
from ctypes import CDLL, byref, POINTER, c_void_p, c_char_p, c_int, c_long, \
Structure, c_ulong, create_string_buffer, addressof, string_at, cast
from ctypes.util import find_library
libcrypto = find_library('crypto')
if libcrypto is None:
raise K4MDEDRMError('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])
PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1',
[c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p])
class LibCrypto(object):
def __init__(self):
self._blocksize = 0
self._keyctx = None
self.iv = 0
def set_decrypt_key(self, userkey, iv):
self._blocksize = len(userkey)
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
raise K4MDEDRMError('AES 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 K4MDEDRMError('Failed to initialize AES key')
def decrypt(self, data):
out = create_string_buffer(len(data))
rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self.iv, 0)
if rv == 0:
raise K4MDEDRMError('AES decryption failed')
return out.raw
def keyivgen(self, passwd):
salt = '16743'
saltlen = 5
passlen = len(passwd)
iter = 0x3e8
keylen = 80
out = create_string_buffer(keylen)
rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out)
return out.raw
return LibCrypto
def _load_crypto():
LibCrypto = None
try:
LibCrypto = _load_crypto_libcrypto()
except (ImportError, K4MDEDRMError):
pass
return LibCrypto
LibCrypto = _load_crypto()
#
# Utility Routines
#
# uses a sub process to get the Hard Drive Serial Number using ioreg
# returns with the first found serial number in that class
def GetVolumeSerialNumber():
sernum = os.getenv('MYSERIALNUMBER')
if sernum != None:
return sernum
cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver'
cmdline = cmdline.encode(sys.getfilesystemencoding())
p = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False)
poll = p.wait('wait')
results = p.read()
reslst = results.split('\n')
cnt = len(reslst)
bsdname = None
sernum = None
foundIt = False
for j in xrange(cnt):
resline = reslst[j]
pp = resline.find('"Serial Number" = "')
if pp >= 0:
sernum = resline[pp+19:-1]
sernum = sernum.strip()
bb = resline.find('"BSD Name" = "')
if bb >= 0:
bsdname = resline[bb+14:-1]
bsdname = bsdname.strip()
if (bsdname == 'disk0') and (sernum != None):
foundIt = True
break
if not foundIt:
sernum = '9999999999'
return sernum
# uses unix env to get username instead of using sysctlbyname
def GetUserName():
username = os.getenv('USER')
return username
MAX_PATH = 255
#
# start of Kindle specific routines
#
global kindleDatabase
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM"
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
# 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
# implements an Pseudo Mac Version of Windows built-in Crypto routine
def CryptUnprotectData(encryptedData):
sp = GetVolumeSerialNumber() + '!@#' + GetUserName()
passwdData = encode(SHA256(sp),charMap1)
crp = LibCrypto()
key_iv = crp.keyivgen(passwdData)
key = key_iv[0:32]
iv = key_iv[32:48]
crp.set_decrypt_key(key,iv)
cleartext = crp.decrypt(encryptedData)
return cleartext
# Locate and open the .kindle-info file
def openKindleInfo():
home = os.getenv('HOME')
kinfopath = home + '/Library/Application Support/Amazon/Kindle/storage/.kindle-info'
if not os.path.exists(kinfopath):
kinfopath = home + '/Library/Application Support/Amazon/Kindle for Mac/storage/.kindle-info'
if not os.path.exists(kinfopath):
raise K4MDEDRMError('Error: .kindle-info file can not be found')
return open(kinfopath,'r')
# Parse the Kindle.info file and return the records as a list of key-values
def parseKindleInfo():
DB = {}
infoReader = openKindleInfo()
infoReader.read(1)
data = infoReader.read()
items = data.split('[')
for item in items:
splito = item.split(':')
DB[splito[0]] =splito[1]
return DB
# Get a record from the Kindle.info file for the key "hashedKey" (already hashed and encoded). Return the decoded and decrypted record
def getKindleInfoValueForHash(hashedKey):
global kindleDatabase
encryptedValue = decode(kindleDatabase[hashedKey],charMap2)
cleartext = CryptUnprotectData(encryptedValue)
return decode(cleartext, charMap1)
# Get a record from the Kindle.info file for the string in "key" (plaintext). Return the decoded and decrypted record
def getKindleInfoValueForKey(key):
return getKindleInfoValueForHash(encodeHash(key,charMap2))
# Find if the original string for a hashed/encoded string is known. If so return the original string othwise return an empty string.
def findNameForHash(hash):
names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
result = ""
for name in names:
if hash == encodeHash(name, charMap2):
result = name
break
return result
# Print all the records from the kindle.info file (option -i)
def printKindleInfo():
for record in kindleDatabase:
name = findNameForHash(record)
if name != "" :
print (name)
print ("--------------------------")
else :
print ("Unknown Record")
print getKindleInfoValueForHash(record)
print "\n"
#
# 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
#
# Main
#
def main(argv=sys.argv):
global kindleDatabase
kindleDatabase = None
#
# Read the encrypted database
#
try:
kindleDatabase = parseKindleInfo()
except Exception, message:
print(message)
if kindleDatabase != None :
printKindleInfo()
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@@ -1,7 +1,11 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import with_statement 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 # engine to remove drm from Kindle for Mac and Kindle for PC books
# for personal use for archiving and converting your ebooks # for personal use for archiving and converting your ebooks
@@ -15,39 +19,54 @@ from __future__ import with_statement
# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle, # This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates # unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates
# and many many others # and many many others
# Special thanks to The Dark Reverser for MobiDeDrm and CMBDTC for cmbdtc_dump
# It can run standalone to convert K4M/K4PC/Mobi files, or it can be installed as a # from which this script borrows most unashamedly.
# plugin for Calibre (http://calibre-ebook.com/about) so that importing
# K4 or Mobi with DRM is no londer a multi-step process.
#
# ***NOTE*** If you are using this script as a calibre plugin for a K4M or K4PC ebook
# then calibre must be installed on the same machine and in the same account as K4PC or K4M
# for the plugin version to function properly.
#
# To create a Calibre plugin, rename this file so that the filename
# ends in '_plugin.py', put it into a ZIP file with all its supporting python routines
# and import that ZIP into Calibre using its plugin configuration GUI.
__version__ = '2.3' # 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.
# 4.9 - Missed some invalid characters in cleanup_name
# 5.0 - Extraction of info from Kindle for PC/Mac moved into kindlekey.py
# - tweaked GetDecryptedBook interface to leave passed parameters unchanged
# 5.1 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 5.2 - Fixed error in command line processing of unicode arguments
class Unbuffered: __version__ = '5.2'
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
import os, csv, getopt import sys, os, re
import string import csv
import binascii import getopt
import zlib
import re import re
import zlib, zipfile, tempfile, shutil import traceback
from struct import pack, unpack, unpack_from import time
import htmlentitydefs
import json
class DrmException(Exception): class DrmException(Exception):
pass pass
@@ -57,169 +76,234 @@ if 'calibre' in sys.modules:
else: else:
inCalibre = False inCalibre = False
def zipUpDir(myzip, tempdir,localname): if inCalibre:
currentdir = tempdir from calibre_plugins.dedrm import mobidedrm
if localname != "": from calibre_plugins.dedrm import topazextract
currentdir = os.path.join(currentdir,localname) from calibre_plugins.dedrm import kgenpids
list = os.listdir(currentdir) else:
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, tempdir, localfilePath)
# cleanup bytestring filenames
# borrowed from calibre from calibre/src/calibre/__init__.py
# added in removal of non-printing chars
# and removal of . at start
# convert spaces to underscores
def cleanup_name(name):
_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+/]')
substitute='_'
one = ''.join(char for char in name if char in string.printable)
one = _filename_sanitize.sub(substitute, one)
one = re.sub(r'\s', ' ', one).strip()
one = re.sub(r'^\.+$', '_', one)
one = one.replace('..', substitute)
# Windows doesn't like path components that end with a period
if one.endswith('.'):
one = one[:-1]+substitute
# Mac and Unix don't like file names that begin with a full stop
if len(one) > 0 and one[0] == '.':
one = substitute+one[1:]
one = one.replace(' ','_')
return one
def decryptBook(infile, outdir, k4, kInfoFiles, serials, pids):
import mobidedrm import mobidedrm
import topazextract import topazextract
import kgenpids 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"\'").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, kDatabases, serials, pids, starttime = time.time()):
# handle the obvious cases at the beginning # handle the obvious cases at the beginning
if not os.path.isfile(infile): if not os.path.isfile(infile):
print "Error: Input file does not exist" raise DRMException (u"Input file does not exist.")
return 1
mobi = True mobi = True
magic3 = file(infile,'rb').read(3) magic3 = open(infile,'rb').read(3)
if magic3 == 'TPZ': if magic3 == 'TPZ':
mobi = False mobi = False
bookname = os.path.splitext(os.path.basename(infile))[0]
if mobi: if mobi:
mb = mobidedrm.MobiBook(infile) mb = mobidedrm.MobiBook(infile)
else: else:
tempdir = tempfile.mkdtemp() mb = topazextract.TopazBook(infile)
mb = topazextract.TopazBook(infile, tempdir)
title = mb.getBookTitle() bookname = unescape(mb.getBookTitle())
print "Processing Book: ", title print u"Decrypting {1} ebook: {0}".format(bookname, mb.getBookType())
filenametitle = cleanup_name(title)
outfilename = bookname
if len(bookname)>4 and len(filenametitle)>4 and bookname[:4] != filenametitle[:4]:
outfilename = outfilename + "_" + filenametitle
# build pid list # copy list of pids
totalpids = list(pids)
# extend PID list with book-specific PIDs
md1, md2 = mb.getPIDMetaInfo() md1, md2 = mb.getPIDMetaInfo()
pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles) totalpids.extend(kgenpids.getPidList(md1, md2, serials, kDatabases))
print u"Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(totalpids))
try: try:
if mobi: mb.processBook(totalpids)
unlocked_file = mb.processBook(pidlst) except:
else: mb.cleanup
mb.processBook(pidlst) raise
except mobidedrm.DrmException, e: print u"Decryption succeeded after {0:.1f} seconds".format(time.time()-starttime)
print "Error: " + str(e) + "\nDRM Removal Failed.\n" return mb
return 1
# kDatabaseFiles is a list of files created by kindlekey
def decryptBook(infile, outdir, kDatabaseFiles, serials, pids):
starttime = time.time()
kDatabases = []
for dbfile in kDatabaseFiles:
kindleDatabase = {}
try:
with open(dbfile, 'r') as keyfilein:
kindleDatabase = json.loads(keyfilein.read())
kDatabases.append([dbfile,kindleDatabase])
except Exception, e: except Exception, e:
if not mobi: print u"Error getting database from file {0:s}: {1:s}".format(dbfile,e)
print "Error: " + str(e) + "\nDRM Removal Failed.\n" traceback.print_exc()
print " Creating DeBug Full Zip Archive of Book"
zipname = os.path.join(outdir, bookname + '_debug' + '.zip')
myzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
zipUpDir(myzip, tempdir, '') try:
myzip.close() book = GetDecryptedBook(infile, kDatabases, serials, pids, starttime)
shutil.rmtree(tempdir, True) except Exception, e:
print u"Error decrypting book after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime)
traceback.print_exc()
return 1 return 1
pass
if mobi: # if we're saving to the same folder as the original, use file name_
outfile = os.path.join(outdir,outfilename + '_nodrm' + '.mobi') # if to a different folder, use book name
file(outfile, 'wb').write(unlocked_file) if os.path.normcase(os.path.normpath(outdir)) == os.path.normcase(os.path.normpath(os.path.dirname(infile))):
return 0 outfilename = os.path.splitext(os.path.basename(infile))[0]
else:
outfilename = cleanup_name(book.getBookTitle())
# topaz: build up zip archives of results # avoid excessively long file names
print " Creating HTML ZIP Archive" if len(outfilename)>150:
zipname = os.path.join(outdir, outfilename + '_nodrm' + '.zip') outfilename = outfilename[:150]
myzip1 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
myzip1.write(os.path.join(tempdir,'book.html'),'book.html')
myzip1.write(os.path.join(tempdir,'book.opf'),'book.opf')
if os.path.isfile(os.path.join(tempdir,'cover.jpg')):
myzip1.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg')
myzip1.write(os.path.join(tempdir,'style.css'),'style.css')
zipUpDir(myzip1, tempdir, 'img')
myzip1.close()
print " Creating SVG ZIP Archive" outfilename = outfilename+u"_nodrm"
zipname = os.path.join(outdir, outfilename + '_SVG' + '.zip') outfile = os.path.join(outdir, outfilename + book.getBookExtension())
myzip2 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
myzip2.write(os.path.join(tempdir,'index_svg.xhtml'),'index_svg.xhtml')
zipUpDir(myzip2, tempdir, 'svg')
zipUpDir(myzip2, tempdir, 'img')
myzip2.close()
print " Creating XML ZIP Archive" book.getFile(outfile)
zipname = os.path.join(outdir, outfilename + '_XML' + '.zip') print u"Saved decrypted book {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename)
myzip3 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
targetdir = os.path.join(tempdir,'xml')
zipUpDir(myzip3, targetdir, '')
zipUpDir(myzip3, tempdir, 'img')
myzip3.close()
shutil.rmtree(tempdir, True) 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()
return 0 return 0
def usage(progname): def usage(progname):
print "Removes DRM protection from K4PC/M, Kindle, Mobi and Topaz ebooks" print u"Removes DRM protection from Mobipocket, Amazon KF8, Amazon Print Replica and Amazon Topaz ebooks"
print "Usage:" print u"Usage:"
print " %s [-k <kindle.info>] [-p <pidnums>] [-s <kindleSerialNumbers>] <infile> <outdir> " % progname print u" {0} [-k <kindle.k4i>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] <infile> <outdir>".format(progname)
# #
# Main # Main
# #
def main(argv=sys.argv): def cli_main():
argv=unicode_argv()
progname = os.path.basename(argv[0]) progname = os.path.basename(argv[0])
print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2013 The Dark Reverser et al.".format(__version__)
k4 = False
kInfoFiles = []
serials = []
pids = []
print ('K4MobiDeDrm v%(__version__)s '
'provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc .' % globals())
print ' '
try: try:
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:") opts, args = getopt.getopt(argv[1:], "k:p:s:")
except getopt.GetoptError, err: except getopt.GetoptError, err:
print str(err) print u"Error in options or arguments: {0}".format(err.args[0])
usage(progname) usage(progname)
sys.exit(2) sys.exit(2)
if len(args)<2: if len(args)<2:
usage(progname) usage(progname)
sys.exit(2) sys.exit(2)
infile = args[0]
outdir = args[1]
kDatabaseFiles = []
serials = []
pids = []
for o, a in opts: for o, a in opts:
if o == "-k": if o == "-k":
if a == None : if a == None :
raise DrmException("Invalid parameter for -k") raise DrmException("Invalid parameter for -k")
kInfoFiles.append(a) kDatabaseFiles.append(a)
if o == "-p": if o == "-p":
if a == None : if a == None :
raise DrmException("Invalid parameter for -p") raise DrmException("Invalid parameter for -p")
@@ -229,141 +313,13 @@ def main(argv=sys.argv):
raise DrmException("Invalid parameter for -s") raise DrmException("Invalid parameter for -s")
serials = a.split(',') serials = a.split(',')
# try with built in Kindle Info files # try with built in Kindle Info files if not on Linux
k4 = True k4 = not sys.platform.startswith('linux')
infile = args[0]
outdir = args[1]
return decryptBook(infile, outdir, k4, kInfoFiles, serials, pids) return decryptBook(infile, outdir, kDatabaseFiles, serials, pids)
if __name__ == '__main__': if __name__ == '__main__':
sys.stdout=Unbuffered(sys.stdout) sys.stdout=SafeUnbuffered(sys.stdout)
sys.exit(main()) sys.stderr=SafeUnbuffered(sys.stderr)
sys.exit(cli_main())
if not __name__ == "__main__" and inCalibre:
from calibre.customize import FileTypePlugin
class K4DeDRM(FileTypePlugin):
name = 'K4PC, K4Mac, Kindle Mobi and Topaz DeDRM' # Name of the plugin
description = 'Removes DRM from K4PC and Mac, Kindle Mobi and Topaz files. \
Provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc.'
supported_platforms = ['osx', 'windows', 'linux'] # Platforms this plugin will run on
author = 'DiapDealer, SomeUpdates' # The author of this plugin
version = (0, 2, 3) # The version number of this plugin
file_types = set(['prc','mobi','azw','azw1','tpz']) # The file types that this plugin will be applied to
on_import = True # Run this plugin during the import
priority = 210 # run this plugin before mobidedrm, k4pcdedrm, k4dedrm
def run(self, path_to_ebook):
from calibre.gui2 import is_ok_to_use_qt
from PyQt4.Qt import QMessageBox
from calibre.ptempfile import PersistentTemporaryDirectory
import kgenpids
import zlib
import zipfile
import topazextract
import mobidedrm
k4 = True
pids = []
serials = []
kInfoFiles = []
# Get supplied list of PIDs to try from plugin customization.
customvalues = self.site_customization.split(',')
for customvalue in customvalues:
customvalue = str(customvalue)
customvalue = customvalue.strip()
if len(customvalue) == 10 or len(customvalue) == 8:
pids.append(customvalue)
else :
if len(customvalue) == 16 and customvalue[0] == 'B':
serials.append(customvalue)
else:
print "%s is not a valid Kindle serial number or PID." % str(customvalue)
# Load any kindle info files (*.info) included Calibre's config directory.
try:
# Find Calibre's configuration directory.
confpath = os.path.split(os.path.split(self.plugin_path)[0])[0]
print 'K4MobiDeDRM: Calibre configuration directory = %s' % confpath
files = os.listdir(confpath)
filefilter = re.compile("\.info$", re.IGNORECASE)
files = filter(filefilter.search, files)
if files:
for filename in files:
fpath = os.path.join(confpath, filename)
kInfoFiles.append(fpath)
print 'K4MobiDeDRM: Kindle info file %s found in config folder.' % filename
except IOError:
print 'K4MobiDeDRM: Error reading kindle info files from config directory.'
pass
mobi = True
magic3 = file(path_to_ebook,'rb').read(3)
if magic3 == 'TPZ':
mobi = False
bookname = os.path.splitext(os.path.basename(path_to_ebook))[0]
if mobi:
mb = mobidedrm.MobiBook(path_to_ebook)
else:
tempdir = PersistentTemporaryDirectory()
mb = topazextract.TopazBook(path_to_ebook, tempdir)
title = mb.getBookTitle()
md1, md2 = mb.getPIDMetaInfo()
pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles)
try:
if mobi:
unlocked_file = mb.processBook(pidlst)
else:
mb.processBook(pidlst)
except mobidedrm.DrmException:
#if you reached here then no luck raise and exception
if is_ok_to_use_qt():
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "Error decoding: %s\n" % path_to_ebook)
d.show()
d.raise_()
d.exec_()
raise Exception("K4MobiDeDRM plugin could not decode the file")
return ""
except topazextract.TpzDRMError:
#if you reached here then no luck raise and exception
if is_ok_to_use_qt():
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "Error decoding: %s\n" % path_to_ebook)
d.show()
d.raise_()
d.exec_()
raise Exception("K4MobiDeDRM plugin could not decode the file")
return ""
print "Success!"
if mobi:
of = self.temporary_file(bookname+'.mobi')
of.write(unlocked_file)
of.close()
return of.name
# topaz: build up zip archives of results
print " Creating HTML ZIP Archive"
of = self.temporary_file(bookname + '.zip')
myzip = zipfile.ZipFile(of.name,'w',zipfile.ZIP_DEFLATED, False)
myzip.write(os.path.join(tempdir,'book.html'),'book.html')
myzip.write(os.path.join(tempdir,'book.opf'),'book.opf')
if os.path.isfile(os.path.join(tempdir,'cover.jpg')):
myzip.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg')
myzip.write(os.path.join(tempdir,'style.css'),'style.css')
zipUpDir(myzip, tempdir, 'img')
myzip.close()
return of.name
def customization_help(self, gui=False):
return 'Enter 10 character PIDs and/or Kindle serial numbers, separated by commas.'

View File

@@ -1,194 +0,0 @@
# standlone set of Mac OSX specific routines needed for K4DeDRM
from __future__ import with_statement
import sys
import os
import subprocess
class DrmException(Exception):
pass
# interface to needed routines in openssl's libcrypto
def _load_crypto_libcrypto():
from ctypes import CDLL, byref, POINTER, c_void_p, c_char_p, c_int, c_long, \
Structure, c_ulong, create_string_buffer, addressof, string_at, cast
from ctypes.util import find_library
libcrypto = find_library('crypto')
if libcrypto is None:
raise DrmException('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])
PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1',
[c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p])
class LibCrypto(object):
def __init__(self):
self._blocksize = 0
self._keyctx = None
self.iv = 0
def set_decrypt_key(self, userkey, iv):
self._blocksize = len(userkey)
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
raise DrmException('AES 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 DrmException('Failed to initialize AES key')
def decrypt(self, data):
out = create_string_buffer(len(data))
rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self.iv, 0)
if rv == 0:
raise DrmException('AES decryption failed')
return out.raw
def keyivgen(self, passwd):
salt = '16743'
saltlen = 5
passlen = len(passwd)
iter = 0x3e8
keylen = 80
out = create_string_buffer(keylen)
rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out)
return out.raw
return LibCrypto
def _load_crypto():
LibCrypto = None
try:
LibCrypto = _load_crypto_libcrypto()
except (ImportError, DrmException):
pass
return LibCrypto
LibCrypto = _load_crypto()
#
# Utility Routines
#
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM"
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
# uses a sub process to get the Hard Drive Serial Number using ioreg
# returns with the serial number of drive whose BSD Name is "disk0"
def GetVolumeSerialNumber():
sernum = os.getenv('MYSERIALNUMBER')
if sernum != None:
return sernum
cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver'
cmdline = cmdline.encode(sys.getfilesystemencoding())
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p.communicate()
reslst = out1.split('\n')
cnt = len(reslst)
bsdname = None
sernum = None
foundIt = False
for j in xrange(cnt):
resline = reslst[j]
pp = resline.find('"Serial Number" = "')
if pp >= 0:
sernum = resline[pp+19:-1]
sernum = sernum.strip()
bb = resline.find('"BSD Name" = "')
if bb >= 0:
bsdname = resline[bb+14:-1]
bsdname = bsdname.strip()
if (bsdname == 'disk0') and (sernum != None):
foundIt = True
break
if not foundIt:
sernum = '9999999999'
return sernum
# uses unix env to get username instead of using sysctlbyname
def GetUserName():
username = os.getenv('USER')
return username
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
import hashlib
def SHA256(message):
ctx = hashlib.sha256()
ctx.update(message)
return ctx.digest()
# implements an Pseudo Mac Version of Windows built-in Crypto routine
def CryptUnprotectData(encryptedData):
sp = GetVolumeSerialNumber() + '!@#' + GetUserName()
passwdData = encode(SHA256(sp),charMap1)
crp = LibCrypto()
key_iv = crp.keyivgen(passwdData)
key = key_iv[0:32]
iv = key_iv[32:48]
crp.set_decrypt_key(key,iv)
cleartext = crp.decrypt(encryptedData)
return cleartext
# Locate and open the .kindle-info file
def openKindleInfo(kInfoFile=None):
if kInfoFile == None:
home = os.getenv('HOME')
cmdline = 'find "' + home + '/Library/Application Support" -name ".kindle-info"'
cmdline = cmdline.encode(sys.getfilesystemencoding())
p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p1.communicate()
reslst = out1.split('\n')
kinfopath = 'NONE'
cnt = len(reslst)
for j in xrange(cnt):
resline = reslst[j]
pp = resline.find('.kindle-info')
if pp >= 0:
kinfopath = resline
break
if not os.path.isfile(kinfopath):
raise DrmException('Error: .kindle-info file can not be found')
return open(kinfopath,'r')
else:
if not os.path.isfile(kInfoFile):
raise DrmException('Error: kindle-info file can not be found')
return open(kInfoFile, 'r')

View File

@@ -1,110 +0,0 @@
# K4PC Windows specific routines
from __future__ import with_statement
import sys, os
from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
string_at, Structure, c_void_p, cast
import _winreg as winreg
import traceback
MAX_PATH = 255
kernel32 = windll.kernel32
advapi32 = windll.advapi32
crypt32 = windll.crypt32
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_"
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
class DrmException(Exception):
pass
class DataBlob(Structure):
_fields_ = [('cbData', c_uint),
('pbData', c_void_p)]
DataBlob_p = POINTER(DataBlob)
def GetSystemDirectory():
GetSystemDirectoryW = kernel32.GetSystemDirectoryW
GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint]
GetSystemDirectoryW.restype = c_uint
def GetSystemDirectory():
buffer = create_unicode_buffer(MAX_PATH + 1)
GetSystemDirectoryW(buffer, len(buffer))
return buffer.value
return GetSystemDirectory
GetSystemDirectory = GetSystemDirectory()
def GetVolumeSerialNumber():
GetVolumeInformationW = kernel32.GetVolumeInformationW
GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint,
POINTER(c_uint), POINTER(c_uint),
POINTER(c_uint), c_wchar_p, c_uint]
GetVolumeInformationW.restype = c_uint
def GetVolumeSerialNumber(path = GetSystemDirectory().split('\\')[0] + '\\'):
vsn = c_uint(0)
GetVolumeInformationW(path, None, 0, byref(vsn), None, None, None, 0)
return str(vsn.value)
return GetVolumeSerialNumber
GetVolumeSerialNumber = GetVolumeSerialNumber()
def GetUserName():
GetUserNameW = advapi32.GetUserNameW
GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
GetUserNameW.restype = c_uint
def GetUserName():
buffer = create_unicode_buffer(32)
size = c_uint(len(buffer))
while not GetUserNameW(buffer, byref(size)):
buffer = create_unicode_buffer(len(buffer) * 2)
size.value = len(buffer)
return buffer.value.encode('utf-16-le')[::2]
return GetUserName
GetUserName = GetUserName()
def CryptUnprotectData():
_CryptUnprotectData = crypt32.CryptUnprotectData
_CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
c_void_p, c_void_p, c_uint, DataBlob_p]
_CryptUnprotectData.restype = c_uint
def CryptUnprotectData(indata, entropy):
indatab = create_string_buffer(indata)
indata = DataBlob(len(indata), cast(indatab, c_void_p))
entropyb = create_string_buffer(entropy)
entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
outdata = DataBlob()
if not _CryptUnprotectData(byref(indata), None, byref(entropy),
None, None, 0, byref(outdata)):
raise DrmException("Failed to Unprotect Data")
return string_at(outdata.pbData, outdata.cbData)
return CryptUnprotectData
CryptUnprotectData = CryptUnprotectData()
#
# Locate and open the Kindle.info file.
#
def openKindleInfo(kInfoFile=None):
if kInfoFile == None:
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info'
if not os.path.isfile(kinfopath):
raise DrmException('Error: kindle.info file can not be found')
return open(kinfopath,'r')
else:
if not os.path.isfile(kInfoFile):
raise DrmException('Error: kindle.info file can not be found')
return open(kInfoFile, 'r')

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import with_statement from __future__ import with_statement
import sys import sys
@@ -7,24 +8,19 @@ import binascii
import zlib import zlib
import re import re
from struct import pack, unpack, unpack_from from struct import pack, unpack, unpack_from
import traceback
class DrmException(Exception): class DrmException(Exception):
pass pass
global kindleDatabase
global charMap1 global charMap1
global charMap2
global charMap3 global charMap3
global charMap4 global charMap4
if sys.platform.startswith('win'):
from k4pcutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap2
if sys.platform.startswith('darwin'):
from k4mutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap2
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M'
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" charMap3 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" charMap4 = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
# crypto digestroutines # crypto digestroutines
import hashlib import hashlib
@@ -42,7 +38,7 @@ def SHA1(message):
# Encode the bytes in data with the characters in map # Encode the bytes in data with the characters in map
def encode(data, map): def encode(data, map):
result = "" result = ''
for char in data: for char in data:
value = ord(char) value = ord(char)
Q = (value ^ 0x80) // len(map) Q = (value ^ 0x80) // len(map)
@@ -57,75 +53,16 @@ def encodeHash(data,map):
# Decode the string in data with the characters in map. Returns the decoded bytes # Decode the string in data with the characters in map. Returns the decoded bytes
def decode(data,map): def decode(data,map):
result = "" result = ''
for i in range (0,len(data)-1,2): for i in range (0,len(data)-1,2):
high = map.find(data[i]) high = map.find(data[i])
low = map.find(data[i+1]) low = map.find(data[i+1])
if (high == -1) or (low == -1) : if (high == -1) or (low == -1) :
break break
value = (((high * len(map)) ^ 0x80) & 0xFF) + low value = (((high * len(map)) ^ 0x80) & 0xFF) + low
result += pack("B",value) result += pack('B',value)
return result return result
# Parse the Kindle.info file and return the records as a list of key-values
def parseKindleInfo(kInfoFile):
DB = {}
infoReader = openKindleInfo(kInfoFile)
infoReader.read(1)
data = infoReader.read()
if sys.platform.startswith('win'):
items = data.split('{')
else :
items = data.split('[')
for item in items:
splito = item.split(':')
DB[splito[0]] =splito[1]
return DB
# Get a record from the Kindle.info file for the key "hashedKey" (already hashed and encoded).
# Return the decoded and decrypted record
def getKindleInfoValueForHash(hashedKey):
global kindleDatabase
global charMap1
global charMap2
encryptedValue = decode(kindleDatabase[hashedKey],charMap2)
if sys.platform.startswith('win'):
return CryptUnprotectData(encryptedValue,"")
else:
cleartext = CryptUnprotectData(encryptedValue)
return decode(cleartext, charMap1)
# Get a record from the Kindle.info file for the string in "key" (plaintext).
# Return the decoded and decrypted record
def getKindleInfoValueForKey(key):
global charMap2
return getKindleInfoValueForHash(encodeHash(key,charMap2))
# Find if the original string for a hashed/encoded string is known.
# If so return the original string othwise return an empty string.
def findNameForHash(hash):
global charMap2
names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
result = ""
for name in names:
if hash == encodeHash(name, charMap2):
result = name
break
return result
# Print all the records from the kindle.info file (option -i)
def printKindleInfo():
for record in kindleDatabase:
name = findNameForHash(record)
if name != "" :
print (name)
print ("--------------------------")
else :
print ("Unknown Record")
print getKindleInfoValueForHash(record)
print "\n"
# #
# PID generation routines # PID generation routines
# #
@@ -145,7 +82,7 @@ def getSixBitsFromBitField(bitField,offset):
# 8 bits to six bits encoding from hash to generate PID string # 8 bits to six bits encoding from hash to generate PID string
def encodePID(hash): def encodePID(hash):
global charMap3 global charMap3
PID = "" PID = ''
for position in range (0,8): for position in range (0,8):
PID += charMap3[getSixBitsFromBitField(hash,position)] PID += charMap3[getSixBitsFromBitField(hash,position)]
return PID return PID
@@ -176,7 +113,7 @@ def generatePidSeed(table,dsn) :
def generateDevicePID(table,dsn,nbRoll): def generateDevicePID(table,dsn,nbRoll):
global charMap4 global charMap4
seed = generatePidSeed(table,dsn) seed = generatePidSeed(table,dsn)
pidAscii = "" pidAscii = ''
pid = [(seed >>24) &0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF,(seed>>24) & 0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF] 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 index = 0
for counter in range (0,nbRoll): for counter in range (0,nbRoll):
@@ -223,94 +160,108 @@ def pidFromSerial(s, l):
# Parse the EXTH header records and use the Kindle serial number to calculate the book pid. # Parse the EXTH header records and use the Kindle serial number to calculate the book pid.
def getKindlePid(pidlst, rec209, token, serialnum): def getKindlePids(rec209, token, serialnum):
pids=[]
if isinstance(serialnum,unicode):
serialnum = serialnum.encode('ascii')
if rec209 != None and token != None:
# Compute book PID # Compute book PID
pidHash = SHA1(serialnum+rec209+token) pidHash = SHA1(serialnum+rec209+token)
bookPID = encodePID(pidHash) bookPID = encodePID(pidHash)
bookPID = checksumPid(bookPID) bookPID = checksumPid(bookPID)
pidlst.append(bookPID) pids.append(bookPID)
# compute fixed pid for old pre 2.5 firmware update pid as well # compute fixed pid for old pre 2.5 firmware update pid as well
bookPID = pidFromSerial(serialnum, 7) + "*" kindlePID = pidFromSerial(serialnum, 7) + "*"
bookPID = checksumPid(bookPID) kindlePID = checksumPid(kindlePID)
pidlst.append(bookPID) pids.append(kindlePID)
return pidlst return pids
# Parse the EXTH header records and parse the Kindleinfo # parse the Kindleinfo file to calculate the book pid.
# file to calculate the book pid.
def getK4Pids(pidlst, rec209, token, kInfoFile=None): keynames = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber']
global kindleDatabase
def getK4Pids(rec209, token, kindleDatabase):
global charMap1 global charMap1
kindleDatabase = None pids = []
try: try:
kindleDatabase = parseKindleInfo(kInfoFile)
except Exception, message:
print(message)
kindleDatabase = None
pass
if kindleDatabase == None :
return pidlst
# Get the Mazama Random number # Get the Mazama Random number
MazamaRandomNumber = getKindleInfoValueForKey("MazamaRandomNumber") MazamaRandomNumber = (kindleDatabase[1])['MazamaRandomNumber'].decode('hex').encode('ascii')
# Get the HDD serial # Get the kindle account token
encodedSystemVolumeSerialNumber = encodeHash(GetVolumeSerialNumber(),charMap1) kindleAccountToken = (kindleDatabase[1])['kindle.account.tokens'].decode('hex').encode('ascii')
# Get the IDString used to decode the Kindle Info file
IDString = (kindleDatabase[1])['IDString'].decode('hex').encode('ascii')
# Get the UserName stored when the Kindle Info file was decoded
UserName = (kindleDatabase[1])['UserName'].decode('hex').encode('ascii')
except KeyError:
print u"Keys not found in the database {0}.".format(kindleDatabase[0])
return pids
# Get the ID string used
encodedIDString = encodeHash(IDString,charMap1)
# Get the current user name # Get the current user name
encodedUsername = encodeHash(GetUserName(),charMap1) encodedUsername = encodeHash(UserName,charMap1)
# concat, hash and encode to calculate the DSN # concat, hash and encode to calculate the DSN
DSN = encode(SHA1(MazamaRandomNumber+encodedSystemVolumeSerialNumber+encodedUsername),charMap1) DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1)
# Compute the device PID (for which I can tell, is used for nothing). # Compute the device PID (for which I can tell, is used for nothing).
table = generatePidEncryptionTable() table = generatePidEncryptionTable()
devicePID = generateDevicePID(table,DSN,4) devicePID = generateDevicePID(table,DSN,4)
devicePID = checksumPid(devicePID) devicePID = checksumPid(devicePID)
pidlst.append(devicePID) pids.append(devicePID)
# Compute book PID # Compute book PIDs
if rec209 == None or token == None:
print "\nNo EXTH record type 209 or token - Perhaps not a K4 file?"
return pidlst
# Get the kindle account token
kindleAccountToken = getKindleInfoValueForKey("kindle.account.tokens")
# book pid # book pid
pidHash = SHA1(DSN+kindleAccountToken+rec209+token) pidHash = SHA1(DSN+kindleAccountToken+rec209+token)
bookPID = encodePID(pidHash) bookPID = encodePID(pidHash)
bookPID = checksumPid(bookPID) bookPID = checksumPid(bookPID)
pidlst.append(bookPID) pids.append(bookPID)
# variant 1 # variant 1
pidHash = SHA1(kindleAccountToken+rec209+token) pidHash = SHA1(kindleAccountToken+rec209+token)
bookPID = encodePID(pidHash) bookPID = encodePID(pidHash)
bookPID = checksumPid(bookPID) bookPID = checksumPid(bookPID)
pidlst.append(bookPID) pids.append(bookPID)
# variant 2 # variant 2
pidHash = SHA1(DSN+rec209+token) pidHash = SHA1(DSN+rec209+token)
bookPID = encodePID(pidHash) bookPID = encodePID(pidHash)
bookPID = checksumPid(bookPID) bookPID = checksumPid(bookPID)
pidlst.append(bookPID) pids.append(bookPID)
return pidlst return pids
def getPidList(md1, md2, k4, pids, serials, kInfoFiles): def getPidList(md1, md2, serials=[], kDatabases=[]):
pidlst = [] pidlst = []
if k4:
pidlst = getK4Pids(pidlst, md1, md2) if kDatabases is None:
for infoFile in kInfoFiles: kDatabases = []
pidlst = getK4Pids(pidlst, md1, md2, infoFile) if serials is None:
serials = []
for kDatabase in kDatabases:
try:
pidlst.extend(getK4Pids(md1, md2, kDatabase))
except Exception, e:
print u"Error getting PIDs from database {0}: {1}".format(kDatabase[0],e.args[0])
traceback.print_exc()
for serialnum in serials: for serialnum in serials:
pidlst = getKindlePid(pidlst, md1, md2, serialnum) try:
for pid in pids: pidlst.extend(getKindlePids(md1, md2, serialnum))
pidlst.append(pid) except Exception, e:
print u"Error getting PIDs from serial number {0}: {1}".format(serialnum ,e.args[0])
traceback.print_exc()
return pidlst return pidlst

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,144 @@
#!/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
# 0.4 Added support for serial numbers starting with '9', fixed unicode bugs.
# 0.5 moved unicode_argv call inside main for Windows DeDRM compatibility
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"kindlepid.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():
print u"Mobipocket PID calculator for Amazon Kindle. Copyright © 2007, 2009 Igor Skochinsky"
argv=unicode_argv()
if len(argv)==2:
serial = 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") or serial.startswith("9"):
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())

View File

@@ -1,5 +1,11 @@
#!/usr/bin/python #!/usr/bin/env python
# -*- coding: utf-8 -*-
# mobidedrm.py, version 0.38
# Copyright © 2008 The Dark Reverser
# #
# Modified 20082012 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.
# #
@@ -46,24 +52,94 @@
# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well # 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well
# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption # 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption
# 0.26 - Now enables Text-To-Speech flag and sets clipping limit to 100% # 0.26 - Now enables Text-To-Speech flag and sets clipping limit to 100%
# 0.27 - Correct pid metadata token generation to match that used by skindle (Thank You Bart!)
# 0.28 - slight additional changes to metadata token generation (None -> '')
# 0.29 - It seems that the ideas about when multibyte trailing characters were
# included in the encryption were wrong. They are for DOC compressed
# files, but they are not for HUFF/CDIC compress files!
# 0.30 - Modified interface slightly to work better with new calibre plugin style
# 0.31 - The multibyte encrytion info is true for version 7 files too.
# 0.32 - Added support for "Print Replica" Kindle ebooks
# 0.33 - Performance improvements for large files (concatenation)
# 0.34 - Performance improvements in decryption (libalfcrypto)
# 0.35 - add interface to get mobi_version
# 0.36 - fixed problem with TEXtREAd and getBookTitle interface
# 0.37 - Fixed double announcement for stand-alone operation
# 0.38 - Unicode used wherever possible, cope with absent alfcrypto
# 0.39 - Fixed problem with TEXtREAd and getBookType interface
# 0.40 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 0.41 - Fixed potential unicode problem in command line calls
__version__ = '0.26'
__version__ = u"0.41"
import sys import sys
import os
import struct
import binascii
try:
from alfcrypto import Pukall_Cipher
except:
print u"AlfCrypto not found. Using python PC1 implementation."
class Unbuffered: # 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): 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)
sys.stdout=Unbuffered(sys.stdout)
import os iswindows = sys.platform.startswith('win')
import struct isosx = sys.platform.startswith('darwin')
import binascii
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]
class DrmException(Exception): class DrmException(Exception):
pass pass
@@ -75,12 +151,18 @@ class DrmException(Exception):
# Implementation of Pukall Cipher 1 # Implementation of Pukall Cipher 1
def PC1(key, src, decryption=True): def PC1(key, src, decryption=True):
# if we can get it from alfcrypto, use that
try:
return Pukall_Cipher().PC1(key,src,decryption)
except NameError:
pass
# use slow python version, since Pukall_Cipher didn't load
sum1 = 0; sum1 = 0;
sum2 = 0; sum2 = 0;
keyXorVal = 0; keyXorVal = 0;
if len(key)!=16: if len(key)!=16:
print "Bad key length!" DrmException (u"PC1: Bad key length")
return None
wkey = [] wkey = []
for i in xrange(8): for i in xrange(8):
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1])) wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
@@ -107,7 +189,7 @@ def PC1(key, src, decryption=True):
return dst return dst
def checksumPid(s): def checksumPid(s):
letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
crc = (~binascii.crc32(s,-1))&0xFFFFFFFF crc = (~binascii.crc32(s,-1))&0xFFFFFFFF
crc = crc ^ (crc >> 16) crc = crc ^ (crc >> 16)
res = s res = s
@@ -155,12 +237,24 @@ class MobiBook:
off = self.sections[section][0] off = self.sections[section][0]
return self.data_file[off:endoff] return self.data_file[off:endoff]
def cleanup(self):
# to match function in Topaz book
pass
def __init__(self, infile): def __init__(self, infile):
print u"MobiDeDrm v{0:s}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__)
try:
from alfcrypto import Pukall_Cipher
except:
print u"AlfCrypto not found. Using python PC1 implementation."
# initial sanity check on file # initial sanity check on file
self.data_file = file(infile, 'rb').read() self.data_file = file(infile, 'rb').read()
self.mobi_data = ''
self.header = self.data_file[0:78] self.header = self.data_file[0:78]
if self.header[0x3C:0x3C+8] != 'BOOKMOBI' and self.header[0x3C:0x3C+8] != 'TEXtREAd': if self.header[0x3C:0x3C+8] != 'BOOKMOBI' and self.header[0x3C:0x3C+8] != 'TEXtREAd':
raise DrmException("invalid file format") raise DrmException(u"Invalid file format")
self.magic = self.header[0x3C:0x3C+8] self.magic = self.header[0x3C:0x3C+8]
self.crypto_type = -1 self.crypto_type = -1
@@ -175,82 +269,91 @@ class MobiBook:
# parse information from section 0 # parse information from section 0
self.sect = self.loadSection(0) self.sect = self.loadSection(0)
self.records, = struct.unpack('>H', self.sect[0x8:0x8+2]) self.records, = struct.unpack('>H', self.sect[0x8:0x8+2])
self.compression, = struct.unpack('>H', self.sect[0x0:0x0+2])
# det default values before PalmDoc test
self.print_replica = False
self.extra_data_flags = 0
self.meta_array = {}
self.mobi_length = 0
self.mobi_codepage = 1252
self.mobi_version = -1
if self.magic == 'TEXtREAd': if self.magic == 'TEXtREAd':
print "Book has format: ", self.magic print u"PalmDoc format book detected."
self.extra_data_flags = 0
self.mobi_length = 0
self.mobi_version = -1
self.meta_array = {}
return return
self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18]) self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18])
self.mobi_codepage, = struct.unpack('>L',self.sect[0x1c:0x20])
self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C]) self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C])
print "MOBI header version = %d, length = %d" %(self.mobi_version, self.mobi_length) print u"MOBI header version {0:d}, header length {1:d}".format(self.mobi_version, self.mobi_length)
self.extra_data_flags = 0
if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5): if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4]) self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4])
print "Extra Data Flags = %d" % self.extra_data_flags print u"Extra Data Flags: {0:d}".format(self.extra_data_flags)
if self.mobi_version < 7: if (self.compression != 17480):
# multibyte utf8 data is included in the encryption for mobi_version 6 and below # multibyte utf8 data is included in the encryption for PalmDoc compression
# so clear that byte so that we leave it to be decrypted. # so clear that byte so that we leave it to be decrypted.
self.extra_data_flags &= 0xFFFE self.extra_data_flags &= 0xFFFE
# if exth region exists parse it for metadata array # if exth region exists parse it for metadata array
self.meta_array = {}
try: try:
exth_flag, = struct.unpack('>L', self.sect[0x80:0x84]) exth_flag, = struct.unpack('>L', self.sect[0x80:0x84])
exth = 'NONE' exth = ''
if exth_flag & 0x40: if exth_flag & 0x40:
exth = self.sect[16 + self.mobi_length:] exth = self.sect[16 + self.mobi_length:]
if (len(exth) >= 4) and (exth[:4] == 'EXTH'): if (len(exth) >= 12) and (exth[:4] == 'EXTH'):
nitems, = struct.unpack('>I', exth[8:12]) nitems, = struct.unpack('>I', exth[8:12])
pos = 12 pos = 12
for i in xrange(nitems): for i in xrange(nitems):
type, size = struct.unpack('>II', exth[pos: pos + 8]) type, size = struct.unpack('>II', exth[pos: pos + 8])
content = exth[pos + 8: pos + size]
self.meta_array[type] = content
# reset the text to speech flag and clipping limit, if present # reset the text to speech flag and clipping limit, if present
if type == 401 and size == 9: if type == 401 and size == 9:
# set clipping limit to 100% # set clipping limit to 100%
self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8) self.patchSection(0, '\144', 16 + self.mobi_length + pos + 8)
content = "\144"
elif type == 404 and size == 9: elif type == 404 and size == 9:
# make sure text to speech is enabled # make sure text to speech is enabled
self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8) self.patchSection(0, '\0', 16 + self.mobi_length + pos + 8)
content = "\0" # print type, size, content, content.encode('hex')
else:
content = exth[pos + 8: pos + size]
#print type, size, content
self.meta_array[type] = content
pos += size pos += size
except: except:
self.meta_array = {}
pass pass
def getBookTitle(self): def getBookTitle(self):
codec_map = {
1252 : 'windows-1252',
65001 : 'utf-8',
}
title = '' title = ''
codec = 'windows-1252'
if self.magic == 'BOOKMOBI':
if 503 in self.meta_array: if 503 in self.meta_array:
title = self.meta_array[503] title = self.meta_array[503]
else : else:
toff, tlen = struct.unpack('>II', self.sect[0x54:0x5c]) toff, tlen = struct.unpack('>II', self.sect[0x54:0x5c])
tend = toff + tlen tend = toff + tlen
title = self.sect[toff:tend] title = self.sect[toff:tend]
if self.mobi_codepage in codec_map.keys():
codec = codec_map[self.mobi_codepage]
if title == '': if title == '':
title = self.header[:32] title = self.header[:32]
title = title.split("\0")[0] title = title.split('\0')[0]
return title return unicode(title, codec)
def getPIDMetaInfo(self): def getPIDMetaInfo(self):
rec209 = None rec209 = ''
token = None token = ''
if 209 in self.meta_array: if 209 in self.meta_array:
rec209 = self.meta_array[209] rec209 = self.meta_array[209]
data = rec209 data = rec209
# Parse the 209 data to find the the exth record with the token data. # The 209 data comes in five byte groups. Interpret the last four bytes
# The last character of the 209 data points to the record with the token. # of each group as a big endian unsigned integer to get a key value
# Always 208 from my experience, but I'll leave the logic in case that changes. # if that key exists in the meta_array, append its contents to the token
for i in xrange(len(data)): for i in xrange(0,len(data),5):
if ord(data[i]) != 0: val, = struct.unpack('>I',data[i+1:i+5])
if self.meta_array[ord(data[i])] != None: sval = self.meta_array.get(val,'')
token = self.meta_array[ord(data[i])] token += sval
return rec209, token return rec209, token
def patch(self, off, new): def patch(self, off, new):
@@ -267,7 +370,7 @@ class MobiBook:
def parseDRM(self, data, count, pidlist): def parseDRM(self, data, count, pidlist):
found_key = None found_key = None
keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96" keyvec1 = '\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96'
for pid in pidlist: for pid in pidlist:
bigpid = pid.ljust(16,'\0') bigpid = pid.ljust(16,'\0')
temp_key = PC1(keyvec1, bigpid, False) temp_key = PC1(keyvec1, bigpid, False)
@@ -285,7 +388,7 @@ class MobiBook:
break break
if not found_key: if not found_key:
# Then try the default encoding that doesn't require a PID # Then try the default encoding that doesn't require a PID
pid = "00000000" pid = '00000000'
temp_key = keyvec1 temp_key = keyvec1
temp_key_sum = sum(map(ord,temp_key)) & 0xff temp_key_sum = sum(map(ord,temp_key)) & 0xff
for i in xrange(count): for i in xrange(count):
@@ -298,93 +401,121 @@ class MobiBook:
break break
return [found_key,pid] return [found_key,pid]
def getFile(self, outpath):
file(outpath,'wb').write(self.mobi_data)
def getBookType(self):
if self.print_replica:
return u"Print Replica"
if self.mobi_version >= 8:
return u"Kindle Format 8"
if self.mobi_version >= 0:
return u"Mobipocket {0:d}".format(self.mobi_version)
return u"PalmDoc"
def getBookExtension(self):
if self.print_replica:
return u".azw4"
if self.mobi_version >= 8:
return u".azw3"
return u".mobi"
def processBook(self, pidlist): def processBook(self, pidlist):
crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2]) crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2])
print 'Crypto Type is: ', crypto_type print u"Crypto Type is: {0:d}".format(crypto_type)
self.crypto_type = crypto_type self.crypto_type = crypto_type
if crypto_type == 0: if crypto_type == 0:
print "This book is not encrypted." print u"This book is not encrypted."
return self.data_file # we must still check for Print Replica
self.print_replica = (self.loadSection(1)[0:4] == '%MOP')
self.mobi_data = self.data_file
return
if crypto_type != 2 and crypto_type != 1: if crypto_type != 2 and crypto_type != 1:
raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type) raise DrmException(u"Cannot decode unknown Mobipocket encryption type {0:d}".format(crypto_type))
if 406 in self.meta_array:
data406 = self.meta_array[406]
val406, = struct.unpack('>Q',data406)
if val406 != 0:
raise DrmException(u"Cannot decode library or rented ebooks.")
goodpids = [] goodpids = []
for pid in pidlist: for pid in pidlist:
if len(pid)==10: if len(pid)==10:
if checksumPid(pid[0:-2]) != pid: if checksumPid(pid[0:-2]) != pid:
print "Warning: PID " + pid + " has incorrect checksum, should have been "+checksumPid(pid[0:-2]) print u"Warning: PID {0} has incorrect checksum, should have been {1}".format(pid,checksumPid(pid[0:-2]))
goodpids.append(pid[0:-2]) goodpids.append(pid[0:-2])
elif len(pid)==8: elif len(pid)==8:
goodpids.append(pid) goodpids.append(pid)
if self.crypto_type == 1: if self.crypto_type == 1:
t1_keyvec = "QDCVEPMU675RUBSZ" t1_keyvec = 'QDCVEPMU675RUBSZ'
if self.magic == 'TEXtREAd': if self.magic == 'TEXtREAd':
bookkey_data = self.sect[0x0E:0x0E+16] bookkey_data = self.sect[0x0E:0x0E+16]
elif self.mobi_version < 0: elif self.mobi_version < 0:
bookkey_data = self.sect[0x90:0x90+16] bookkey_data = self.sect[0x90:0x90+16]
else: else:
bookkey_data = self.sect[self.mobi_length+16:self.mobi_length+32] bookkey_data = self.sect[self.mobi_length+16:self.mobi_length+32]
pid = "00000000" pid = '00000000'
found_key = PC1(t1_keyvec, bookkey_data) found_key = PC1(t1_keyvec, bookkey_data)
else : else :
# calculate the keys # calculate the keys
drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16]) drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16])
if drm_count == 0: if drm_count == 0:
raise DrmException("Not yet initialised with PID. Must be opened with Mobipocket Reader first.") raise DrmException(u"Encryption not initialised. Must be opened with Mobipocket Reader first.")
found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids) found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids)
if not found_key: if not found_key:
raise DrmException("No key found. Most likely the correct PID has not been given.") raise DrmException(u"No key found in {0:d} keys tried.".format(len(goodpids)))
# kill the drm keys # kill the drm keys
self.patchSection(0, "\0" * drm_size, drm_ptr) self.patchSection(0, '\0' * drm_size, drm_ptr)
# kill the drm pointers # kill the drm pointers
self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8) self.patchSection(0, '\xff' * 4 + '\0' * 12, 0xA8)
if pid=="00000000": if pid=='00000000':
print "File has default encryption, no specific PID." print u"File has default encryption, no specific key needed."
else: else:
print "File is encoded with PID "+checksumPid(pid)+"." print u"File is encoded with PID {0}.".format(checksumPid(pid))
# clear the crypto type # clear the crypto type
self.patchSection(0, "\0" * 2, 0xC) self.patchSection(0, "\0" * 2, 0xC)
# decrypt sections # decrypt sections
print "Decrypting. Please wait . . .", print u"Decrypting. Please wait . . .",
new_data = self.data_file[:self.sections[1][0]] mobidataList = []
mobidataList.append(self.data_file[:self.sections[1][0]])
for i in xrange(1, self.records+1): for i in xrange(1, self.records+1):
data = self.loadSection(i) data = self.loadSection(i)
extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags) extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags)
if i%100 == 0: if i%100 == 0:
print ".", print u".",
# print "record %d, extra_size %d" %(i,extra_size) # print "record %d, extra_size %d" %(i,extra_size)
new_data += PC1(found_key, data[0:len(data) - extra_size]) decoded_data = PC1(found_key, data[0:len(data) - extra_size])
if i==1:
self.print_replica = (decoded_data[0:4] == '%MOP')
mobidataList.append(decoded_data)
if extra_size > 0: if extra_size > 0:
new_data += data[-extra_size:] mobidataList.append(data[-extra_size:])
if self.num_sections > self.records+1: if self.num_sections > self.records+1:
new_data += self.data_file[self.sections[self.records+1][0]:] mobidataList.append(self.data_file[self.sections[self.records+1][0]:])
self.data_file = new_data self.mobi_data = "".join(mobidataList)
print "done" print u"done"
return self.data_file return
def getUnencryptedBook(infile,pid): def getUnencryptedBook(infile,pidlist):
if not os.path.isfile(infile): if not os.path.isfile(infile):
raise DrmException('Input File Not Found') raise DrmException(u"Input File Not Found.")
book = MobiBook(infile) book = MobiBook(infile)
return book.processBook([pid]) book.processBook(pidlist)
return book.mobi_data
def getUnencryptedBookWithList(infile,pidlist):
if not os.path.isfile(infile):
raise DrmException('Input File Not Found')
book = MobiBook(infile)
return book.processBook(pidlist)
def main(argv=sys.argv): def cli_main():
print ('MobiDeDrm v%(__version__)s. ' argv=unicode_argv()
'Copyright 2008-2010 The Dark Reverser.' % globals()) progname = os.path.basename(argv[0])
if len(argv)<3 or len(argv)>4: if len(argv)<3 or len(argv)>4:
print "Removes protection from Mobipocket books" print u"MobiDeDrm v{0}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__)
print "Usage:" print u"Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks"
print " %s <infile> <outfile> [<Comma separated list of PIDs to try>]" % sys.argv[0] print u"Usage:"
print u" {0} <infile> <outfile> [<Comma separated list of PIDs to try>]".format(progname)
return 1 return 1
else: else:
infile = argv[1] infile = argv[1]
@@ -392,15 +523,17 @@ def main(argv=sys.argv):
if len(argv) is 4: if len(argv) is 4:
pidlist = argv[3].split(',') pidlist = argv[3].split(',')
else: else:
pidlist = {} pidlist = []
try: try:
stripped_file = getUnencryptedBookWithList(infile, pidlist) stripped_file = getUnencryptedBook(infile, pidlist)
file(outfile, 'wb').write(stripped_file) file(outfile, 'wb').write(stripped_file)
except DrmException, e: except DrmException, e:
print "Error: %s" % e print u"MobiDeDRM v{0} Error: {0:s}".format(__version__,e.args[0])
return 1 return 1
return 0 return 0
if __name__ == "__main__": if __name__ == '__main__':
sys.exit(main()) sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
sys.exit(cli_main())

View File

@@ -65,7 +65,7 @@ def load_libcrypto():
class DES(object): class DES(object):
def __init__(self, key): def __init__(self, key):
if len(key) != 8 : if len(key) != 8 :
raise Error('DES improper key used') raise Exception('DES improper key used')
return return
self.key = key self.key = key
self.keyschedule = DES_KEY_SCHEDULE() self.keyschedule = DES_KEY_SCHEDULE()
@@ -87,4 +87,3 @@ def load_libcrypto():
return ''.join(result) return ''.join(result)
return DES return DES

View File

@@ -0,0 +1,293 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement
__license__ = 'GPL v3'
# Standard Python modules.
import os, sys, re, hashlib
import json
import traceback
from calibre.utils.config import dynamic, config_dir, JSONConfig
from calibre_plugins.dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION
from calibre.constants import iswindows, isosx
class DeDRM_Prefs():
def __init__(self):
JSON_PATH = os.path.join(u"plugins", PLUGIN_NAME.strip().lower().replace(' ', '_') + '.json')
self.dedrmprefs = JSONConfig(JSON_PATH)
self.dedrmprefs.defaults['configured'] = False
self.dedrmprefs.defaults['bandnkeys'] = {}
self.dedrmprefs.defaults['adeptkeys'] = {}
self.dedrmprefs.defaults['ereaderkeys'] = {}
self.dedrmprefs.defaults['kindlekeys'] = {}
self.dedrmprefs.defaults['pids'] = []
self.dedrmprefs.defaults['serials'] = []
self.dedrmprefs.defaults['adobewineprefix'] = ""
self.dedrmprefs.defaults['kindlewineprefix'] = ""
# initialise
# we must actually set the prefs that are dictionaries and lists
# to empty dictionaries and lists, otherwise we are unable to add to them
# as then it just adds to the (memory only) dedrmprefs.defaults versions!
if self.dedrmprefs['bandnkeys'] == {}:
self.dedrmprefs['bandnkeys'] = {}
if self.dedrmprefs['adeptkeys'] == {}:
self.dedrmprefs['adeptkeys'] = {}
if self.dedrmprefs['ereaderkeys'] == {}:
self.dedrmprefs['ereaderkeys'] = {}
if self.dedrmprefs['kindlekeys'] == {}:
self.dedrmprefs['kindlekeys'] = {}
if self.dedrmprefs['pids'] == []:
self.dedrmprefs['pids'] = []
if self.dedrmprefs['serials'] == []:
self.dedrmprefs['serials'] = []
def __getitem__(self,kind = None):
if kind is not None:
return self.dedrmprefs[kind]
return self.dedrmprefs
def set(self, kind, value):
self.dedrmprefs[kind] = value
def writeprefs(self,value = True):
self.dedrmprefs['configured'] = value
def addnamedvaluetoprefs(self, prefkind, keyname, keyvalue):
try:
if keyvalue not in self.dedrmprefs[prefkind].values():
# ensure that the keyname is unique
# by adding a number (starting with 2) to the name if it is not
namecount = 1
newname = keyname
while newname in self.dedrmprefs[prefkind]:
namecount += 1
newname = "{0:s}_{1:d}".format(keyname,namecount)
# add to the preferences
self.dedrmprefs[prefkind][newname] = keyvalue
return (True, newname)
except:
traceback.print_exc()
pass
return (False, keyname)
def addvaluetoprefs(self, prefkind, prefsvalue):
# ensure the keyvalue isn't already in the preferences
try:
if prefsvalue not in self.dedrmprefs[prefkind]:
self.dedrmprefs[prefkind].append(prefsvalue)
return True
except:
traceback.print_exc()
return False
def convertprefs(always = False):
def parseIgnobleString(keystuff):
from calibre_plugins.dedrm.ignoblekeygen import generate_key
userkeys = []
ar = keystuff.split(':')
for keystring in ar:
try:
name, ccn = keystring.split(',')
# Generate Barnes & Noble EPUB user key from name and credit card number.
keyname = u"{0}_{1}".format(name.strip(),ccn.strip()[-4:])
keyvalue = generate_key(name, ccn)
userkeys.append([keyname,keyvalue])
except Exception, e:
traceback.print_exc()
print e.args[0]
pass
return userkeys
def parseeReaderString(keystuff):
from calibre_plugins.dedrm.erdr2pml import getuser_key
userkeys = []
ar = keystuff.split(':')
for keystring in ar:
try:
name, cc = keystring.split(',')
# Generate eReader user key from name and credit card number.
keyname = u"{0}_{1}".format(name.strip(),cc.strip()[-4:])
keyvalue = getuser_key(name,cc).encode('hex')
userkeysappend([keyname,keyvalue])
except Exception, e:
traceback.print_exc()
print e.args[0]
pass
return userkeys
def parseKindleString(keystuff):
pids = []
serials = []
ar = keystuff.split(',')
for keystring in ar:
keystring = str(keystring).strip().replace(" ","")
if len(keystring) == 10 or len(keystring) == 8 and keystring not in pids:
pids.append(keystring)
elif len(keystring) == 16 and keystring[0] == 'B' and keystring not in serials:
serials.append(keystring)
return (pids,serials)
def getConfigFiles(extension, encoding = None):
# get any files with extension 'extension' in the config dir
userkeys = []
files = [f for f in os.listdir(config_dir) if f.endswith(extension)]
for filename in files:
try:
fpath = os.path.join(config_dir, filename)
key = os.path.splitext(filename)[0]
value = open(fpath, 'rb').read()
if encoding is not None:
value = value.encode(encoding)
userkeys.append([key,value])
except:
traceback.print_exc()
pass
return userkeys
dedrmprefs = DeDRM_Prefs()
if (not always) and dedrmprefs['configured']:
# We've already converted old preferences,
# and we're not being forced to do it again, so just return
return
print u"{0} v{1}: Importing configuration data from old DeDRM plugins".format(PLUGIN_NAME, PLUGIN_VERSION)
IGNOBLEPLUGINNAME = "Ignoble Epub DeDRM"
EREADERPLUGINNAME = "eReader PDB 2 PML"
OLDKINDLEPLUGINNAME = "K4PC, K4Mac, Kindle Mobi and Topaz DeDRM"
# get prefs from older tools
kindleprefs = JSONConfig(os.path.join(u"plugins", u"K4MobiDeDRM"))
ignobleprefs = JSONConfig(os.path.join(u"plugins", u"ignoble_epub_dedrm"))
# Handle the old ignoble plugin's customization string by converting the
# old string to stored keys... get that personal data out of plain sight.
from calibre.customize.ui import config
sc = config['plugin_customization']
val = sc.pop(IGNOBLEPLUGINNAME, None)
if val is not None:
print u"{0} v{1}: Converting old Ignoble plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION)
priorkeycount = len(dedrmprefs['bandnkeys'])
userkeys = parseIgnobleString(str(val))
for keypair in userkeys:
name = keypair[0]
value = keypair[1]
dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value)
addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount
print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from old Ignoble plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys")
# Make the json write all the prefs to disk
dedrmprefs.writeprefs(False)
# Handle the old eReader plugin's customization string by converting the
# old string to stored keys... get that personal data out of plain sight.
val = sc.pop(EREADERPLUGINNAME, None)
if val is not None:
print u"{0} v{1}: Converting old eReader plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION)
priorkeycount = len(dedrmprefs['ereaderkeys'])
userkeys = parseeReaderString(str(val))
for keypair in userkeys:
name = keypair[0]
value = keypair[1]
dedrmprefs.addnamedvaluetoprefs('ereaderkeys', name, value)
addedkeycount = len(dedrmprefs['ereaderkeys'])-priorkeycount
print u"{0} v{1}: {2:d} eReader {3} imported from old eReader plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys")
# Make the json write all the prefs to disk
dedrmprefs.writeprefs(False)
# get old Kindle plugin configuration string
val = sc.pop(OLDKINDLEPLUGINNAME, None)
if val is not None:
print u"{0} v{1}: Converting old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION)
priorpidcount = len(dedrmprefs['pids'])
priorserialcount = len(dedrmprefs['serials'])
pids, serials = parseKindleString(val)
for pid in pids:
dedrmprefs.addvaluetoprefs('pids',pid)
for serial in serials:
dedrmprefs.addvaluetoprefs('serials',serial)
addedpidcount = len(dedrmprefs['pids']) - priorpidcount
addedserialcount = len(dedrmprefs['serials']) - priorserialcount
print u"{0} v{1}: {2:d} {3} and {4:d} {5} imported from old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, u"PID" if addedpidcount==1 else u"PIDs", addedserialcount, u"serial number" if addedserialcount==1 else u"serial numbers")
# Make the json write all the prefs to disk
dedrmprefs.writeprefs(False)
# copy the customisations back into calibre preferences, as we've now removed the nasty plaintext
config['plugin_customization'] = sc
# get any .b64 files in the config dir
priorkeycount = len(dedrmprefs['bandnkeys'])
bandnfilekeys = getConfigFiles('.b64')
for keypair in bandnfilekeys:
name = keypair[0]
value = keypair[1]
dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value)
addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount
if addedkeycount > 0:
print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, ignoblecount, u"key file" if ignoblecount==1 else u"key files")
# Make the json write all the prefs to disk
dedrmprefs.writeprefs(False)
# get any .der files in the config dir
priorkeycount = len(dedrmprefs['adeptkeys'])
adeptfilekeys = getConfigFiles('.der','hex')
ineptcount = addConfigFiles('.der', 'adeptkeys')
for keypair in adeptfilekeys:
name = keypair[0]
value = keypair[1]
dedrmprefs.addnamedvaluetoprefs('adeptkeys', name, value)
addedkeycount = len(dedrmprefs['adeptkeys'])-priorkeycount
if addedkeycount > 0:
print u"{0} v{1}: {2:d} Adobe Adept {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, ineptcount, u"keyfile" if ineptcount==1 else u"keyfiles")
# Make the json write all the prefs to disk
dedrmprefs.writeprefs(False)
# get ignoble json prefs
if 'keys' in ignobleprefs:
priorkeycount = len(dedrmprefs['bandnkeys'])
for name in ignobleprefs['keys']:
value = ignobleprefs['keys'][name]
dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value)
addedkeycount = len(dedrmprefs['bandnkeys']) - priorkeycount
# no need to delete old prefs, since they contain no recoverable private data
if addedkeycount > 0:
print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from Ignoble plugin preferences.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys")
# Make the json write all the prefs to disk
dedrmprefs.writeprefs(False)
# get kindle json prefs
priorpidcount = len(dedrmprefs['pids'])
priorserialcount = len(dedrmprefs['serials'])
if 'pids' in kindleprefs:
pids, serials = parseKindleString(kindleprefs['pids'])
for pid in pids:
dedrmprefs.addvaluetoprefs('pids',pid)
if 'serials' in kindleprefs:
pids, serials = parseKindleString(kindleprefs['serials'])
for serial in serials:
dedrmprefs.addvaluetoprefs('serials',serial)
addedpidcount = len(dedrmprefs['pids']) - priorpidcount
if addedpidcount > 0:
print u"{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, u"PID" if addedpidcount==1 else u"PIDs")
addedserialcount = len(dedrmprefs['serials']) - priorserialcount
if addedserialcount > 0:
print u"{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedserialcount, u"serial number" if addedserialcount==1 else u"serial numbers")
try:
if kindleprefs['wineprefix'] != "":
dedrmprefs.set('adobewineprefix',kindleprefs['wineprefix'])
dedrmprefs.set('kindlewineprefix',kindleprefs['wineprefix'])
print u"{0} v{1}: WINEPREFIX (2) imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, kindleprefs['wineprefix'])
except:
traceback.print_exc()
# Make the json write all the prefs to disk
dedrmprefs.writeprefs()
print u"{0} v{1}: Finished setting up configuration data.".format(PLUGIN_NAME, PLUGIN_VERSION)

View File

@@ -28,4 +28,3 @@ def load_pycrypto():
i += 8 i += 8
return ''.join(result) return ''.join(result)
return DES return DES

View File

@@ -0,0 +1,176 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
import sys
import os
import re
import ineptepub
import ignobleepub
import epubtest
import zipfix
import ineptpdf
import erdr2pml
import k4mobidedrm
import traceback
def decryptepub(infile, outdir, rscpath):
errlog = ''
# first fix the epub to make sure we do not get errors
name, ext = os.path.splitext(os.path.basename(infile))
bpath = os.path.dirname(infile)
zippath = os.path.join(bpath,name + '_temp.zip')
rv = zipfix.repairBook(infile, zippath)
if rv != 0:
print "Error while trying to fix epub"
return rv
# determine a good name for the output file
outfile = os.path.join(outdir, name + '_nodrm.epub')
rv = 1
# first try with the Adobe adept epub
if ineptepub.adeptBook(zippath):
# try with any keyfiles (*.der) in the rscpath
files = os.listdir(rscpath)
filefilter = re.compile("\.der$", re.IGNORECASE)
files = filter(filefilter.search, files)
if files:
for filename in files:
keypath = os.path.join(rscpath, filename)
userkey = open(keypath,'rb').read()
try:
rv = ineptepub.decryptBook(userkey, zippath, outfile)
if rv == 0:
print "Decrypted Adobe ePub with key file {0}".format(filename)
break
except Exception, e:
errlog += traceback.format_exc()
errlog += str(e)
rv = 1
# now try with ignoble epub
elif ignobleepub.ignobleBook(zippath):
# try with any keyfiles (*.b64) in the rscpath
files = os.listdir(rscpath)
filefilter = re.compile("\.b64$", re.IGNORECASE)
files = filter(filefilter.search, files)
if files:
for filename in files:
keypath = os.path.join(rscpath, filename)
userkey = open(keypath,'r').read()
#print userkey
try:
rv = ignobleepub.decryptBook(userkey, zippath, outfile)
if rv == 0:
print "Decrypted B&N ePub with key file {0}".format(filename)
break
except Exception, e:
errlog += traceback.format_exc()
errlog += str(e)
rv = 1
else:
encryption = epubtest.encryption(zippath)
if encryption == "Unencrypted":
print "{0} is not DRMed.".format(name)
rv = 0
else:
print "{0} has an unknown encryption.".format(name)
os.remove(zippath)
if rv != 0:
print errlog
return rv
def decryptpdf(infile, outdir, rscpath):
errlog = ''
rv = 1
# determine a good name for the output file
name, ext = os.path.splitext(os.path.basename(infile))
outfile = os.path.join(outdir, name + '_nodrm.pdf')
# try with any keyfiles (*.der) in the rscpath
files = os.listdir(rscpath)
filefilter = re.compile("\.der$", re.IGNORECASE)
files = filter(filefilter.search, files)
if files:
for filename in files:
keypath = os.path.join(rscpath, filename)
userkey = open(keypath,'rb').read()
try:
rv = ineptpdf.decryptBook(userkey, infile, outfile)
if rv == 0:
break
except Exception, e:
errlog += traceback.format_exc()
errlog += str(e)
rv = 1
if rv != 0:
print errlog
return rv
def decryptpdb(infile, outdir, rscpath):
outname = os.path.splitext(os.path.basename(infile))[0] + ".pmlz"
outpath = os.path.join(outdir, outname)
rv = 1
socialpath = os.path.join(rscpath,'sdrmlist.txt')
if os.path.exists(socialpath):
keydata = file(socialpath,'r').read()
keydata = keydata.rstrip(os.linesep)
ar = keydata.split(',')
for i in ar:
try:
name, cc8 = i.split(':')
except ValueError:
print ' Error parsing user supplied social drm data.'
return 1
try:
rv = erdr2pml.decryptBook(infile, outpath, True, erdr2pml.getuser_key(name, cc8))
except Exception, e:
errlog += traceback.format_exc()
errlog += str(e)
rv = 1
if rv == 0:
break
return rv
def decryptk4mobi(infile, outdir, rscpath):
rv = 1
pidnums = []
pidspath = os.path.join(rscpath,'pidlist.txt')
if os.path.exists(pidspath):
pidstr = file(pidspath,'r').read()
pidstr = pidstr.rstrip(os.linesep)
pidstr = pidstr.strip()
if pidstr != '':
pidnums = pidstr.split(',')
serialnums = []
serialnumspath = os.path.join(rscpath,'seriallist.txt')
if os.path.exists(serialnumspath):
serialstr = file(serialnumspath,'r').read()
serialstr = serialstr.rstrip(os.linesep)
serialstr = serialstr.strip()
if serialstr != '':
serialnums = serialstr.split(',')
kDatabaseFiles = []
files = os.listdir(rscpath)
filefilter = re.compile("\.k4i$", re.IGNORECASE)
files = filter(filefilter.search, files)
if files:
for filename in files:
dpath = os.path.join(rscpath,filename)
kDatabaseFiles.append(dpath)
try:
rv = k4mobidedrm.decryptBook(infile, outdir, kDatabaseFiles, serialnums, pidnums)
except Exception, e:
errlog += traceback.format_exc()
errlog += str(e)
rv = 1
return rv

View File

@@ -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

View File

@@ -1,27 +1,108 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*-
class Unbuffered: # topazextract.py
# Mostly written by some_updates based on code from many others
# Changelog
# 4.9 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 5.0 - Fixed potential unicode problem with command line interface
__version__ = '5.0'
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): 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 os, csv, getopt
import zlib, zipfile, tempfile, shutil
from struct import pack
from struct import unpack
class TpzDRMError(Exception): 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.dedrm import kgenpids
else:
inCalibre = False
import kgenpids
class DrmException(Exception):
pass pass
# local support routines
import kgenpids # recursive zip creation support routine
import genbook 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 # Utility routines
# #
@@ -35,7 +116,7 @@ def bookReadEncodedNumber(fo):
data = ord(fo.read(1)) data = ord(fo.read(1))
if data >= 0x80: if data >= 0x80:
datax = (data & 0x7F) datax = (data & 0x7F)
while data >= 0x80 : while data >= 0x80:
data = ord(fo.read(1)) data = ord(fo.read(1))
datax = (datax <<7) + (data & 0x7F) datax = (datax <<7) + (data & 0x7F)
data = datax data = datax
@@ -46,7 +127,7 @@ def bookReadEncodedNumber(fo):
# Get a length prefixed string from file # Get a length prefixed string from file
def bookReadString(fo): def bookReadString(fo):
stringLength = bookReadEncodedNumber(fo) stringLength = bookReadEncodedNumber(fo)
return unpack(str(stringLength)+"s",fo.read(stringLength))[0] return unpack(str(stringLength)+'s',fo.read(stringLength))[0]
# #
# crypto routines # crypto routines
@@ -54,25 +135,28 @@ def bookReadString(fo):
# Context initialisation for the Topaz Crypto # Context initialisation for the Topaz Crypto
def topazCryptoInit(key): def topazCryptoInit(key):
ctx1 = 0x0CAFFE19E return Topaz_Cipher().ctx_init(key)
for keyChar in key:
keyByte = ord(keyChar) # ctx1 = 0x0CAFFE19E
ctx2 = ctx1 # for keyChar in key:
ctx1 = ((((ctx1 >>2) * (ctx1 >>7))&0xFFFFFFFF) ^ (keyByte * keyByte * 0x0F902007)& 0xFFFFFFFF ) # keyByte = ord(keyChar)
return [ctx1,ctx2] # ctx2 = ctx1
# ctx1 = ((((ctx1 >>2) * (ctx1 >>7))&0xFFFFFFFF) ^ (keyByte * keyByte * 0x0F902007)& 0xFFFFFFFF )
# return [ctx1,ctx2]
# decrypt data with the context prepared by topazCryptoInit() # decrypt data with the context prepared by topazCryptoInit()
def topazCryptoDecrypt(data, ctx): def topazCryptoDecrypt(data, ctx):
ctx1 = ctx[0] return Topaz_Cipher().decrypt(data, ctx)
ctx2 = ctx[1] # ctx1 = ctx[0]
plainText = "" # ctx2 = ctx[1]
for dataChar in data: # plainText = ""
dataByte = ord(dataChar) # for dataChar in data:
m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF # dataByte = ord(dataChar)
ctx2 = ctx1 # m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF
ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF) # ctx2 = ctx1
plainText += chr(m) # ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF)
return plainText # plainText += chr(m)
# return plainText
# Decrypt data with the PID # Decrypt data with the PID
def decryptRecord(data,PID): def decryptRecord(data,PID):
@@ -82,13 +166,13 @@ def decryptRecord(data,PID):
# Try to decrypt a dkey record (contains the bookPID) # Try to decrypt a dkey record (contains the bookPID)
def decryptDkeyRecord(data,PID): def decryptDkeyRecord(data,PID):
record = decryptRecord(data,PID) record = decryptRecord(data,PID)
fields = unpack("3sB8sB8s3s",record) fields = unpack('3sB8sB8s3s',record)
if fields[0] != "PID" or fields[5] != "pid" : if fields[0] != 'PID' or fields[5] != 'pid' :
raise TpzDRMError("Didn't find PID magic numbers in record") raise DrmException(u"Didn't find PID magic numbers in record")
elif fields[1] != 8 or fields[3] != 8 : elif fields[1] != 8 or fields[3] != 8 :
raise TpzDRMError("Record didn't contain correct length fields") raise DrmException(u"Record didn't contain correct length fields")
elif fields[2] != PID : elif fields[2] != PID :
raise TpzDRMError("Record didn't contain PID") raise DrmException(u"Record didn't contain PID")
return fields[4] return fields[4]
# Decrypt all dkey records (contain the book PID) # Decrypt all dkey records (contain the book PID)
@@ -101,25 +185,26 @@ def decryptDkeyRecords(data,PID):
try: try:
key = decryptDkeyRecord(data[1:length+1],PID) key = decryptDkeyRecord(data[1:length+1],PID)
records.append(key) records.append(key)
except TpzDRMError: except DrmException:
pass pass
data = data[1+length:] data = data[1+length:]
if len(records) == 0: if len(records) == 0:
raise TpzDRMError("BookKey Not Found") raise DrmException(u"BookKey Not Found")
return records return records
class TopazBook: class TopazBook:
def __init__(self, filename, outdir): def __init__(self, filename):
self.fo = file(filename, 'rb') self.fo = file(filename, 'rb')
self.outdir = outdir self.outdir = tempfile.mkdtemp()
# self.outdir = 'rawdat'
self.bookPayloadOffset = 0 self.bookPayloadOffset = 0
self.bookHeaderRecords = {} self.bookHeaderRecords = {}
self.bookMetadata = {} self.bookMetadata = {}
self.bookKey = None self.bookKey = None
magic = unpack("4s",self.fo.read(4))[0] magic = unpack('4s',self.fo.read(4))[0]
if magic != 'TPZ0': if magic != 'TPZ0':
raise TpzDRMError("Parse Error : Invalid Header, not a Topaz file") raise DrmException(u"Parse Error : Invalid Header, not a Topaz file")
self.parseTopazHeaders() self.parseTopazHeaders()
self.parseMetadata() self.parseMetadata()
@@ -128,6 +213,7 @@ class TopazBook:
# Read and return the data of one header record at the current book file position # Read and return the data of one header record at the current book file position
# [[offset,decompressedLength,compressedLength],...] # [[offset,decompressedLength,compressedLength],...]
nbValues = bookReadEncodedNumber(self.fo) nbValues = bookReadEncodedNumber(self.fo)
if debug: print "%d records in header " % nbValues,
values = [] values = []
for i in range (0,nbValues): for i in range (0,nbValues):
values.append([bookReadEncodedNumber(self.fo),bookReadEncodedNumber(self.fo),bookReadEncodedNumber(self.fo)]) values.append([bookReadEncodedNumber(self.fo),bookReadEncodedNumber(self.fo),bookReadEncodedNumber(self.fo)])
@@ -136,46 +222,51 @@ class TopazBook:
# Read and parse one header record at the current book file position and return the associated data # Read and parse one header record at the current book file position and return the associated data
# [[offset,decompressedLength,compressedLength],...] # [[offset,decompressedLength,compressedLength],...]
if ord(self.fo.read(1)) != 0x63: if ord(self.fo.read(1)) != 0x63:
raise TpzDRMError("Parse Error : Invalid Header") raise DrmException(u"Parse Error : Invalid Header")
tag = bookReadString(self.fo) tag = bookReadString(self.fo)
record = bookReadHeaderRecordData() record = bookReadHeaderRecordData()
return [tag,record] return [tag,record]
nbRecords = bookReadEncodedNumber(self.fo) nbRecords = bookReadEncodedNumber(self.fo)
if debug: print "Headers: %d" % nbRecords
for i in range (0,nbRecords): for i in range (0,nbRecords):
result = parseTopazHeaderRecord() result = parseTopazHeaderRecord()
# print result[0], result[1] if debug: print result[0], ": ", result[1]
self.bookHeaderRecords[result[0]] = result[1] self.bookHeaderRecords[result[0]] = result[1]
if ord(self.fo.read(1)) != 0x64 : if ord(self.fo.read(1)) != 0x64 :
raise TpzDRMError("Parse Error : Invalid Header") raise DrmException(u"Parse Error : Invalid Header")
self.bookPayloadOffset = self.fo.tell() self.bookPayloadOffset = self.fo.tell()
def parseMetadata(self): def parseMetadata(self):
# Parse the metadata record from the book payload and return a list of [key,values] # 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]) self.fo.seek(self.bookPayloadOffset + self.bookHeaderRecords['metadata'][0][0])
tag = bookReadString(self.fo) tag = bookReadString(self.fo)
if tag != "metadata" : if tag != 'metadata' :
raise TpzDRMError("Parse Error : Record Names Don't Match") raise DrmException(u"Parse Error : Record Names Don't Match")
flags = ord(self.fo.read(1)) flags = ord(self.fo.read(1))
nbRecords = ord(self.fo.read(1)) nbRecords = ord(self.fo.read(1))
if debug: print "Metadata Records: %d" % nbRecords
for i in range (0,nbRecords) : for i in range (0,nbRecords) :
record = [bookReadString(self.fo), bookReadString(self.fo)] keyval = bookReadString(self.fo)
self.bookMetadata[record[0]] = record[1] content = bookReadString(self.fo)
if debug: print keyval
if debug: print content
self.bookMetadata[keyval] = content
return self.bookMetadata return self.bookMetadata
def getPIDMetaInfo(self): def getPIDMetaInfo(self):
keysRecord = None keysRecord = self.bookMetadata.get('keys','')
keysRecordRecord = None keysRecordRecord = ''
if 'keys' in self.bookMetadata: if keysRecord != '':
keysRecord = self.bookMetadata['keys'] keylst = keysRecord.split(',')
if keysRecord in self.bookMetadata: for keyval in keylst:
keysRecordRecord = self.bookMetadata[keysRecord] keysRecordRecord += self.bookMetadata.get(keyval,'')
return keysRecord, keysRecordRecord return keysRecord, keysRecordRecord
def getBookTitle(self): def getBookTitle(self):
title = '' title = ''
if 'Title' in self.bookMetadata: if 'Title' in self.bookMetadata:
title = self.bookMetadata['Title'] title = self.bookMetadata['Title']
return title return title.decode('utf-8')
def setBookKey(self, key): def setBookKey(self, key):
self.bookKey = key self.bookKey = key
@@ -188,13 +279,13 @@ class TopazBook:
try: try:
recordOffset = self.bookHeaderRecords[name][index][0] recordOffset = self.bookHeaderRecords[name][index][0]
except: except:
raise TpzDRMError("Parse Error : Invalid Record, record not found") raise DrmException("Parse Error : Invalid Record, record not found")
self.fo.seek(self.bookPayloadOffset + recordOffset) self.fo.seek(self.bookPayloadOffset + recordOffset)
tag = bookReadString(self.fo) tag = bookReadString(self.fo)
if tag != name : if tag != name :
raise TpzDRMError("Parse Error : Invalid Record, record name doesn't match") raise DrmException("Parse Error : Invalid Record, record name doesn't match")
recordIndex = bookReadEncodedNumber(self.fo) recordIndex = bookReadEncodedNumber(self.fo)
if recordIndex < 0 : if recordIndex < 0 :
@@ -202,7 +293,7 @@ class TopazBook:
recordIndex = -recordIndex -1 recordIndex = -recordIndex -1
if recordIndex != index : if recordIndex != index :
raise TpzDRMError("Parse Error : Invalid Record, index doesn't match") raise DrmException("Parse Error : Invalid Record, index doesn't match")
if (self.bookHeaderRecords[name][index][2] > 0): if (self.bookHeaderRecords[name][index][2] > 0):
compressed = True compressed = True
@@ -215,7 +306,7 @@ class TopazBook:
ctx = topazCryptoInit(self.bookKey) ctx = topazCryptoInit(self.bookKey)
record = topazCryptoDecrypt(record,ctx) record = topazCryptoDecrypt(record,ctx)
else : else :
raise TpzDRMError("Error: Attempt to decrypt without bookKey") raise DrmException("Error: Attempt to decrypt without bookKey")
if compressed: if compressed:
record = zlib.decompress(record) record = zlib.decompress(record)
@@ -227,15 +318,20 @@ class TopazBook:
fixedimage=True fixedimage=True
try: try:
keydata = self.getBookPayloadRecord('dkey', 0) keydata = self.getBookPayloadRecord('dkey', 0)
except TpzDRMError, e: except DrmException, e:
print "no dkey record found, book may not be encrypted" print u"no dkey record found, book may not be encrypted"
print "attempting to extrct files without a book key" print u"attempting to extrct files without a book key"
self.createBookDirectory() self.createBookDirectory()
self.extractFiles() self.extractFiles()
print "Successfully Extracted Topaz contents" print u"Successfully Extracted Topaz contents"
if inCalibre:
from calibre_plugins.dedrm import genbook
else:
import genbook
rv = genbook.generateBook(self.outdir, raw, fixedimage) rv = genbook.generateBook(self.outdir, raw, fixedimage)
if rv == 0: if rv == 0:
print "\nBook Successfully generated" print u"Book Successfully generated."
return rv return rv
# try each pid to decode the file # try each pid to decode the file
@@ -243,28 +339,33 @@ class TopazBook:
for pid in pidlst: for pid in pidlst:
# use 8 digit pids here # use 8 digit pids here
pid = pid[0:8] pid = pid[0:8]
print "\nTrying: ", pid print u"Trying: {0}".format(pid)
bookKeys = [] bookKeys = []
data = keydata data = keydata
try: try:
bookKeys+=decryptDkeyRecords(data,pid) bookKeys+=decryptDkeyRecords(data,pid)
except TpzDRMError, e: except DrmException, e:
pass pass
else: else:
bookKey = bookKeys[0] bookKey = bookKeys[0]
print "Book Key Found!" print u"Book Key Found! ({0})".format(bookKey.encode('hex'))
break break
if not bookKey: if not bookKey:
raise TpzDRMError('Decryption Unsucessful; No valid pid found') 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.setBookKey(bookKey)
self.createBookDirectory() self.createBookDirectory()
self.extractFiles() self.extractFiles()
print "Successfully Extracted Topaz contents" print u"Successfully Extracted Topaz contents"
if inCalibre:
from calibre_plugins.dedrm import genbook
else:
import genbook
rv = genbook.generateBook(self.outdir, raw, fixedimage) rv = genbook.generateBook(self.outdir, raw, fixedimage)
if rv == 0: if rv == 0:
print "\nBook Successfully generated" print u"Book Successfully generated"
return rv return rv
def createBookDirectory(self): def createBookDirectory(self):
@@ -272,16 +373,16 @@ class TopazBook:
# create output directory structure # create output directory structure
if not os.path.exists(outdir): if not os.path.exists(outdir):
os.makedirs(outdir) os.makedirs(outdir)
destdir = os.path.join(outdir,'img') destdir = os.path.join(outdir,u"img")
if not os.path.exists(destdir): if not os.path.exists(destdir):
os.makedirs(destdir) os.makedirs(destdir)
destdir = os.path.join(outdir,'color_img') destdir = os.path.join(outdir,u"color_img")
if not os.path.exists(destdir): if not os.path.exists(destdir):
os.makedirs(destdir) os.makedirs(destdir)
destdir = os.path.join(outdir,'page') destdir = os.path.join(outdir,u"page")
if not os.path.exists(destdir): if not os.path.exists(destdir):
os.makedirs(destdir) os.makedirs(destdir)
destdir = os.path.join(outdir,'glyphs') destdir = os.path.join(outdir,u"glyphs")
if not os.path.exists(destdir): if not os.path.exists(destdir):
os.makedirs(destdir) os.makedirs(destdir)
@@ -289,148 +390,149 @@ class TopazBook:
outdir = self.outdir outdir = self.outdir
for headerRecord in self.bookHeaderRecords: for headerRecord in self.bookHeaderRecords:
name = headerRecord name = headerRecord
if name != "dkey" : if name != 'dkey':
ext = '.dat' ext = u".dat"
if name == 'img' : ext = '.jpg' if name == 'img': ext = u".jpg"
if name == 'color' : ext = '.jpg' if name == 'color' : ext = u".jpg"
print "\nProcessing Section: %s " % name print u"Processing Section: {0}\n. . .".format(name),
for index in range (0,len(self.bookHeaderRecords[name])) : for index in range (0,len(self.bookHeaderRecords[name])) :
fnum = "%04d" % index fname = u"{0}{1:04d}{2}".format(name,index,ext)
fname = name + fnum + ext
destdir = outdir destdir = outdir
if name == 'img': if name == 'img':
destdir = os.path.join(outdir,'img') destdir = os.path.join(outdir,u"img")
if name == 'color': if name == 'color':
destdir = os.path.join(outdir,'color_img') destdir = os.path.join(outdir,u"color_img")
if name == 'page': if name == 'page':
destdir = os.path.join(outdir,'page') destdir = os.path.join(outdir,u"page")
if name == 'glyphs': if name == 'glyphs':
destdir = os.path.join(outdir,'glyphs') destdir = os.path.join(outdir,u"glyphs")
outputFile = os.path.join(destdir,fname) outputFile = os.path.join(destdir,fname)
print ".", print u".",
record = self.getBookPayloadRecord(name,index) record = self.getBookPayloadRecord(name,index)
if record != '': if record != '':
file(outputFile, 'wb').write(record) file(outputFile, 'wb').write(record)
print " " 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 zipUpDir(myzip, tempdir,localname): def getBookType(self):
currentdir = tempdir return u"Topaz"
if localname != "":
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, tempdir, localfilePath)
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): def usage(progname):
print "Removes DRM protection from Topaz ebooks and extract the contents" print u"Removes DRM protection from Topaz ebooks and extracts the contents"
print "Usage:" print u"Usage:"
print " %s [-k <kindle.info>] [-p <pidnums>] [-s <kindleSerialNumbers>] <infile> <outdir> " % progname print u" {0} [-k <kindle.k4i>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] <infile> <outdir>".format(progname)
# Main # Main
def main(argv=sys.argv): def cli_main():
argv=unicode_argv()
progname = os.path.basename(argv[0]) progname = os.path.basename(argv[0])
k4 = False print u"TopazExtract v{0}.".format(__version__)
pids = []
serials = []
kInfoFiles = []
try: try:
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:") opts, args = getopt.getopt(argv[1:], "k:p:s:x")
except getopt.GetoptError, err: except getopt.GetoptError, err:
print str(err) print u"Error in options or arguments: {0}".format(err.args[0])
usage(progname) usage(progname)
return 1 return 1
if len(args)<2: if len(args)<2:
usage(progname) usage(progname)
return 1 return 1
for o, a in opts:
if o == "-k":
if a == None :
print "Invalid parameter for -k"
return 1
kInfoFiles.append(a)
if o == "-p":
if a == None :
print "Invalid parameter for -p"
return 1
pids = a.split(',')
if o == "-s":
if a == None :
print "Invalid parameter for -s"
return 1
serials = a.split(',')
k4 = True
infile = args[0] infile = args[0]
outdir = args[1] outdir = args[1]
if not os.path.isfile(infile): if not os.path.isfile(infile):
print "Input File Does Not Exist" print u"Input File {0} Does Not Exist.".format(infile)
return 1 return 1
if not os.path.exists(outdir):
print u"Output Directory {0} Does Not Exist.".format(outdir)
return 1
kDatabaseFiles = []
serials = []
pids = []
for o, a in opts:
if o == '-k':
if a == None :
raise DrmException("Invalid parameter for -k")
kDatabaseFiles.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] bookname = os.path.splitext(os.path.basename(infile))[0]
tempdir = tempfile.mkdtemp()
tb = TopazBook(infile, tempdir) tb = TopazBook(infile)
title = tb.getBookTitle() title = tb.getBookTitle()
print "Processing Book: ", title print u"Processing Book: {0}".format(title)
keysRecord, keysRecordRecord = tb.getPIDMetaInfo() md1, md2 = tb.getPIDMetaInfo()
pidlst = kgenpids.getPidList(keysRecord, keysRecordRecord, k4, pids, serials, kInfoFiles) pids.extend(kgenpids.getPidList(md1, md2, serials, kDatabaseFiles))
try: try:
tb.processBook(pidlst) print u"Decrypting Book"
except TpzDRMError, e: tb.processBook(pids)
print str(e)
print " Creating DeBug Full Zip Archive of Book" print u" Creating HTML ZIP Archive"
zipname = os.path.join(outdir, bookname + '_debug' + '.zip') zipname = os.path.join(outdir, bookname + u"_nodrm.htmlz")
myzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False) tb.getFile(zipname)
zipUpDir(myzip, tempdir, '')
myzip.close() print u" Creating SVG ZIP Archive"
shutil.rmtree(tempdir, True) 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 return 1
print " Creating HTML ZIP Archive" except Exception, e:
zipname = os.path.join(outdir, bookname + '_nodrm' + '.zip') print u"Decryption failed\m{0}".format(traceback.format_exc())
myzip1 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False) try:
myzip1.write(os.path.join(tempdir,'book.html'),'book.html') tb.cleanup()
myzip1.write(os.path.join(tempdir,'book.opf'),'book.opf') except:
if os.path.isfile(os.path.join(tempdir,'cover.jpg')): pass
myzip1.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg') return 1
myzip1.write(os.path.join(tempdir,'style.css'),'style.css')
zipUpDir(myzip1, tempdir, 'img')
myzip1.close()
print " Creating SVG ZIP Archive"
zipname = os.path.join(outdir, bookname + '_SVG' + '.zip')
myzip2 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
myzip2.write(os.path.join(tempdir,'index_svg.xhtml'),'index_svg.xhtml')
zipUpDir(myzip2, tempdir, 'svg')
zipUpDir(myzip2, tempdir, 'img')
myzip2.close()
print " Creating XML ZIP Archive"
zipname = os.path.join(outdir, bookname + '_XML' + '.zip')
myzip3 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
targetdir = os.path.join(tempdir,'xml')
zipUpDir(myzip3, targetdir, '')
zipUpDir(myzip3, tempdir, 'img')
myzip3.close()
shutil.rmtree(tempdir, True)
return 0 return 0
if __name__ == '__main__': if __name__ == '__main__':
sys.exit(main()) sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
sys.exit(cli_main())

View 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

View File

@@ -0,0 +1,60 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import with_statement
__license__ = 'GPL v3'
# Standard Python modules.
import os, sys, re, hashlib
from calibre_plugins.dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION
def WineGetKeys(scriptpath, extension, wineprefix=""):
import subprocess
from subprocess import Popen, PIPE, STDOUT
import subasyncio
from subasyncio import Process
if extension == u".k4i":
import json
basepath, script = os.path.split(scriptpath)
print u"{0} v{1}: Running {2} under Wine".format(PLUGIN_NAME, PLUGIN_VERSION, script)
outdirpath = os.path.join(basepath, u"winekeysdir")
if not os.path.exists(outdirpath):
os.mkdir(outdirpath)
if wineprefix != "" and os.path.exists(wineprefix):
cmdline = u"WINEPREFIX=\"{2}\" wine python.exe \"{0}\" \"{1}\"".format(scriptpath,outdirpath,wineprefix)
else:
cmdline = u"wine python.exe \"{0}\" \"{1}\"".format(scriptpath,outdirpath)
print u"{0} v{1}: Command line: “{2}".format(PLUGIN_NAME, PLUGIN_VERSION, cmdline)
try:
cmdline = cmdline.encode(sys.getfilesystemencoding())
p2 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=sys.stdout, stderr=STDOUT, close_fds=False)
result = p2.wait("wait")
except Exception, e:
print u"{0} v{1}: Wine subprocess call error: {2}".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0])
return []
winekeys = []
# get any files with extension in the output dir
files = [f for f in os.listdir(outdirpath) if f.endswith(extension)]
for filename in files:
try:
fpath = os.path.join(outdirpath, filename)
with open(fpath, 'rb') as keyfile:
if extension == u".k4i":
new_key_value = json.loads(keyfile.read())
else:
new_key_value = keyfile.read()
winekeys.append(new_key_value)
except:
print u"{0} v{1}: Error loading file {2}".format(PLUGIN_NAME, PLUGIN_VERSION, filename)
traceback.print_exc()
os.remove(fpath)
print u"{0} v{1}: Found and decrypted {2} {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(winekeys), u"key file" if len(winekeys) == 1 else u"key files")
return winekeys

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,26 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*-
# zipfix.py, version 1.1
# Copyright © 2010-2013 by some_updates, DiapDealer and Apprentice Alf
# Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/>
# Revision history:
# 1.0 - Initial release
# 1.1 - Updated to handle zip file metadata correctly
"""
Re-write zip (or ePub) fixing problems with file names (and mimetype entry).
"""
__license__ = 'GPL v3'
__version__ = "1.1"
import sys import sys
import zlib import zlib
import zipfile import zipfilerugged
import os import os
import os.path import os.path
import getopt import getopt
@@ -15,7 +33,7 @@ _FILENAME_OFFSET = 30
_MAX_SIZE = 64 * 1024 _MAX_SIZE = 64 * 1024
_MIMETYPE = 'application/epub+zip' _MIMETYPE = 'application/epub+zip'
class ZipInfo(zipfile.ZipInfo): class ZipInfo(zipfilerugged.ZipInfo):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
if 'compress_type' in kwargs: if 'compress_type' in kwargs:
compress_type = kwargs.pop('compress_type') compress_type = kwargs.pop('compress_type')
@@ -27,8 +45,8 @@ class fixZip:
self.ztype = 'zip' self.ztype = 'zip'
if zinput.lower().find('.epub') >= 0 : if zinput.lower().find('.epub') >= 0 :
self.ztype = 'epub' self.ztype = 'epub'
self.inzip = zipfile.ZipFile(zinput,'r') self.inzip = zipfilerugged.ZipFile(zinput,'r')
self.outzip = zipfile.ZipFile(zoutput,'w') 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')
@@ -76,11 +94,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)
@@ -95,25 +113,41 @@ class fixZip:
# if epub write mimetype file first, with no compression # if epub write mimetype file first, with no compression
if self.ztype == 'epub': if self.ztype == 'epub':
nzinfo = ZipInfo('mimetype', compress_type=zipfile.ZIP_STORED) # first get a ZipInfo with current time and no compression
self.outzip.writestr(nzinfo, _MIMETYPE) mimeinfo = ZipInfo('mimetype',compress_type=zipfilerugged.ZIP_STORED)
mimeinfo.internal_attr = 1 # text file
try:
# if the mimetype is present, get its info, including time-stamp
oldmimeinfo = self.inzip.getinfo('mimetype')
# copy across useful fields
mimeinfo.date_time = oldmimeinfo.date_time
mimeinfo.comment = oldmimeinfo.comment
mimeinfo.extra = oldmimeinfo.extra
mimeinfo.internal_attr = oldmimeinfo.internal_attr
mimeinfo.external_attr = oldmimeinfo.external_attr
mimeinfo.create_system = oldmimeinfo.create_system
except:
pass
self.outzip.writestr(mimeinfo, _MIMETYPE)
# write the rest of the files # write the rest of the files
for zinfo in self.inzip.infolist(): for zinfo in self.inzip.infolist():
if zinfo.filename != "mimetype" or self.ztype == '.zip': if zinfo.filename != "mimetype" or self.ztype != 'epub':
data = None data = None
nzinfo = zinfo
try: try:
data = self.inzip.read(zinfo.filename) 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 zinfo.filename = local_name
nzinfo.date_time = zinfo.date_time # create new ZipInfo with only the useful attributes from the old info
nzinfo.compress_type = zinfo.compress_type nzinfo = ZipInfo(zinfo.filename, zinfo.date_time, compress_type=zinfo.compress_type)
nzinfo.flag_bits = 0 nzinfo.comment=zinfo.comment
nzinfo.internal_attr = 0 nzinfo.extra=zinfo.extra
nzinfo.internal_attr=zinfo.internal_attr
nzinfo.external_attr=zinfo.external_attr
nzinfo.create_system=zinfo.create_system
self.outzip.writestr(nzinfo,data) self.outzip.writestr(nzinfo,data)
self.bzf.close() self.bzf.close()
@@ -152,5 +186,3 @@ def main(argv=sys.argv):
if __name__ == '__main__' : if __name__ == '__main__' :
sys.exit(main()) sys.exit(main())

View File

@@ -0,0 +1 @@
chcp 65001 > nul && set PWD="%~dp0" && cd /d "%~dp0DeDRM_lib" && start /min python DeDRM_App.pyw %*

Some files were not shown because too many files have changed in this diff Show More