diff --git a/Adobe_EPUB_Tools/ineptepub.pyw b/Adobe_EPUB_Tools/ineptepub.pyw
index 701fc2e..442c37a 100644
--- a/Adobe_EPUB_Tools/ineptepub.pyw
+++ b/Adobe_EPUB_Tools/ineptepub.pyw
@@ -1,7 +1,7 @@
#! /usr/bin/python
# -*- coding: utf-8 -*-
-# ineptepub.pyw, version 5.2
+# ineptepub.pyw, version 5.4
# Copyright © 2009-2010 i♥cabbages
# Released under the terms of the GNU General Public Licence, version 3 or
@@ -25,6 +25,8 @@
# 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
+
"""
Decrypt Adobe ADEPT-encrypted EPUB books.
"""
@@ -288,6 +290,7 @@ class Decryptor(object):
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):
diff --git a/Adobe_PDF_Tools/ineptkey.pyw b/Adobe_PDF_Tools/ineptkey.pyw
new file mode 100644
index 0000000..bd66e78
--- /dev/null
+++ b/Adobe_PDF_Tools/ineptkey.pyw
@@ -0,0 +1,458 @@
+#! /usr/bin/python
+# -*- coding: utf-8 -*-
+
+# ineptkey.pyw, version 5
+# Copyright © 2009-2010 i♥cabbages
+
+# Released under the terms of the GNU General Public Licence, version 3 or
+# later.
+
+# Windows users: Before running this program, you must first install Python 2.6
+# from and PyCrypto from
+# (make certain
+# to install the version for Python 2.6). Then save this script file as
+# ineptkey.pyw and double-click on it to run it. It will create a file named
+# adeptkey.der in the same directory. This is your ADEPT user key.
+#
+# Mac OS X users: Save this script file as ineptkey.pyw. You can run this
+# program from the command line (pythonw ineptkey.pyw) or by double-clicking
+# 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.
+
+# Revision history:
+# 1 - Initial release, for Adobe Digital Editions 1.7
+# 2 - Better algorithm for finding pLK; improved error handling
+# 3 - Rename to INEPT
+# 4 - Series of changes by joblack (and others?) --
+# 4.1 - quick beta fix for ADE 1.7.2 (anon)
+# 4.2 - added old 1.7.1 processing
+# 4.3 - better key search
+# 4.4 - Make it working on 64-bit Python
+# 5 - Clean up and improve 4.x changes;
+# Clean up and merge OS X support by unknown
+# 5.1 - add support for using OpenSSL on Windows in place of PyCrypto
+# 5.2 - added support for output of key to a particular file
+
+"""
+Retrieve Adobe ADEPT user key.
+"""
+
+from __future__ import with_statement
+
+__license__ = 'GPL v3'
+
+import sys
+import os
+import struct
+import Tkinter
+import Tkconstants
+import tkMessageBox
+import traceback
+
+class ADEPTError(Exception):
+ pass
+
+if sys.platform.startswith('win'):
+ 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_libcrypto, _load_crypto_pycrypto):
+ 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(keypath):
+ 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])]
+ with open(keypath, 'wb') as f:
+ f.write(userkey)
+ return True
+
+elif sys.platform.startswith('darwin'):
+ import xml.etree.ElementTree as etree
+ import Carbon.File
+ import Carbon.Folder
+ import Carbon.Folders
+ import MacOS
+
+ ACTIVATION_PATH = 'Adobe/Digital Editions/activation.dat'
+ NSMAP = {'adept': 'http://ns.adobe.com/adept',
+ 'enc': 'http://www.w3.org/2001/04/xmlenc#'}
+
+ def find_folder(domain, dtype):
+ try:
+ fsref = Carbon.Folder.FSFindFolder(domain, dtype, False)
+ return Carbon.File.pathname(fsref)
+ except MacOS.Error:
+ return None
+
+ def find_app_support_file(subpath):
+ dtype = Carbon.Folders.kApplicationSupportFolderType
+ 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:
+ 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:]
+ with open(keypath, 'wb') as f:
+ 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:
+ def retrieve_key(keypath):
+ tkMessageBox.showerror(
+ "ADEPT Key",
+ "This script only supports Windows and Mac OS X. For Linux "
+ "you should be able to run ADE and this script under Wine (with "
+ "an appropriate version of Windows Python installed).")
+ return False
+
+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 cli_main(argv=sys.argv):
+ keypath = argv[1]
+ 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 main(argv=sys.argv):
+ root = Tkinter.Tk()
+ root.withdraw()
+ progname = os.path.basename(argv[0])
+ keypath = 'adeptkey.der'
+ success = False
+ try:
+ success = retrieve_key(keypath)
+ except ADEPTError, e:
+ tkMessageBox.showerror("ADEPT Key", "Error: " + str(e))
+ except Exception:
+ root.wm_state('normal')
+ root.title('ADEPT Key')
+ text = traceback.format_exc()
+ ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1)
+ root.mainloop()
+ if not success:
+ return 1
+ tkMessageBox.showinfo(
+ "ADEPT Key", "Key successfully retrieved to %s" % (keypath))
+ return 0
+
+if __name__ == '__main__':
+ if len(sys.argv) > 1:
+ sys.exit(cli_main())
+ sys.exit(main())
diff --git a/ineptpdf.pyw b/Adobe_PDF_Tools/ineptpdf.pyw
similarity index 100%
rename from ineptpdf.pyw
rename to Adobe_PDF_Tools/ineptpdf.pyw
diff --git a/ineptpdf8.pyw b/Adobe_PDF_Tools/ineptpdf8.pyw
similarity index 100%
rename from ineptpdf8.pyw
rename to Adobe_PDF_Tools/ineptpdf8.pyw
diff --git a/Barnes_and_Noble_EPUB_Tools/ignobleepub.pyw b/Barnes_and_Noble_EPUB_Tools/ignobleepub.pyw
index 46cd4e8..469713a 100644
--- a/Barnes_and_Noble_EPUB_Tools/ignobleepub.pyw
+++ b/Barnes_and_Noble_EPUB_Tools/ignobleepub.pyw
@@ -144,6 +144,7 @@ class Decryptor(object):
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)
diff --git a/Topaz_Tools/lib/convert2xml.py b/Calibre_Plugins/K4MobiDeDRM_plugin/convert2xml.py
similarity index 98%
rename from Topaz_Tools/lib/convert2xml.py
rename to Calibre_Plugins/K4MobiDeDRM_plugin/convert2xml.py
index d3ccd48..3070ab6 100644
--- a/Topaz_Tools/lib/convert2xml.py
+++ b/Calibre_Plugins/K4MobiDeDRM_plugin/convert2xml.py
@@ -730,7 +730,20 @@ class PageParser(object):
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 '
@@ -801,4 +814,4 @@ def main(argv):
return xmlpage
if __name__ == '__main__':
- sys.exit(main(''))
\ No newline at end of file
+ sys.exit(main(''))
diff --git a/Topaz_Tools/lib/flatxml2html.py b/Calibre_Plugins/K4MobiDeDRM_plugin/flatxml2html.py
similarity index 96%
rename from Topaz_Tools/lib/flatxml2html.py
rename to Calibre_Plugins/K4MobiDeDRM_plugin/flatxml2html.py
index cf3f9f9..81d93bc 100644
--- a/Topaz_Tools/lib/flatxml2html.py
+++ b/Calibre_Plugins/K4MobiDeDRM_plugin/flatxml2html.py
@@ -12,15 +12,14 @@ from struct import unpack
class DocParser(object):
- def __init__(self, flatxml, classlst, fileid, bookDir, fixedimage):
+ def __init__(self, flatxml, classlst, fileid, bookDir, gdict, fixedimage):
self.id = os.path.basename(fileid).replace('.dat','')
self.svgcount = 0
self.docList = flatxml.split('\n')
self.docSize = len(self.docList)
self.classList = {}
self.bookDir = bookDir
- self.glyphPaths = { }
- self.numPaths = 0
+ self.gdict = gdict
tmpList = classlst.split('\n')
for pclass in tmpList:
if pclass != '':
@@ -41,9 +40,8 @@ class DocParser(object):
def getGlyph(self, gid):
result = ''
- id='gl%d' % gid
- return self.glyphPaths[id]
-
+ id='id="gl%d"' % gid
+ return self.gdict.lookup(id)
def glyphs_to_image(self, glyphList):
@@ -52,31 +50,12 @@ class DocParser(object):
e = path.find(' ',b)
return int(path[b:e])
- def extractID(path, key):
- b = path.find(key) + len(key)
- e = path.find('"',b)
- return path[b:e]
-
-
svgDir = os.path.join(self.bookDir,'svg')
- glyfile = os.path.join(svgDir,'glyphs.svg')
imgDir = os.path.join(self.bookDir,'img')
imgname = self.id + '_%04d.svg' % self.svgcount
imgfile = os.path.join(imgDir,imgname)
- # build hashtable of glyph paths keyed by glyph id
- if self.numPaths == 0:
- gfile = open(glyfile, 'r')
- while True:
- path = gfile.readline()
- if (path == ''): break
- glyphid = extractID(path,'id="')
- self.glyphPaths[glyphid] = path
- self.numPaths += 1
- gfile.close()
-
-
# get glyph information
gxList = self.getData('info.glyph.x',0,-1)
gyList = self.getData('info.glyph.y',0,-1)
@@ -720,11 +699,8 @@ class DocParser(object):
-def convert2HTML(flatxml, classlst, fileid, bookDir, fixedimage):
-
+def convert2HTML(flatxml, classlst, fileid, bookDir, gdict, fixedimage):
# create a document parser
- dp = DocParser(flatxml, classlst, fileid, bookDir, fixedimage)
-
+ dp = DocParser(flatxml, classlst, fileid, bookDir, gdict, fixedimage)
htmlpage = dp.process()
-
return htmlpage
diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/flatxml2svg.py b/Calibre_Plugins/K4MobiDeDRM_plugin/flatxml2svg.py
new file mode 100644
index 0000000..6f6795d
--- /dev/null
+++ b/Calibre_Plugins/K4MobiDeDRM_plugin/flatxml2svg.py
@@ -0,0 +1,151 @@
+#! /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('\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 += '\n'
+ if (raw):
+ ml += '\n'
+ ml += '
':
+ htmlpage = htmlpage[0:-4]
+ last_para_continued = False
+
+ return htmlpage
+
+
+
+def convert2HTML(flatxml, classlst, fileid, bookDir, gdict, fixedimage):
+ # create a document parser
+ dp = DocParser(flatxml, classlst, fileid, bookDir, gdict, fixedimage)
+ htmlpage = dp.process()
+ return htmlpage
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/flatxml2svg.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/flatxml2svg.py
new file mode 100644
index 0000000..6f6795d
--- /dev/null
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/flatxml2svg.py
@@ -0,0 +1,151 @@
+#! /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('\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 += '\n'
+ if (raw):
+ ml += '\n'
+ ml += '\n' % (pp.pw / scaledpi, pp.ph / scaledpi, pp.pw -1, pp.ph -1)
+ ml += 'Page %d - %s by %s\n' % (counter, meta_array['Title'],meta_array['Authors'])
+ else:
+ ml += '\n'
+ ml += '\n'
+ ml += 'Page %d - %s by %s\n' % (counter, meta_array['Title'],meta_array['Authors'])
+ ml += '\n'
+ ml += '\n'
+ ml += '\n'
+ ml += '\n'
+ ml += '\n'
+ ml += '\n'
+ ml += '\n'
+ return ml
+
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/genbook.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/genbook.py
new file mode 100644
index 0000000..a483dec
--- /dev/null
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/genbook.py
@@ -0,0 +1,561 @@
+#! /usr/bin/python
+# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
+
+class Unbuffered:
+ def __init__(self, stream):
+ self.stream = stream
+ def write(self, data):
+ self.stream.write(data)
+ self.stream.flush()
+ def __getattr__(self, attr):
+ return getattr(self.stream, attr)
+
+import sys
+sys.stdout=Unbuffered(sys.stdout)
+
+import csv
+import os
+import getopt
+from struct import pack
+from struct import unpack
+
+
+# local support routines
+import convert2xml
+import flatxml2html
+import flatxml2svg
+import stylexml2css
+
+
+# Get a 7 bit encoded number from a file
+def readEncodedNumber(file):
+ flag = False
+ c = file.read(1)
+ if (len(c) == 0):
+ return None
+ data = ord(c)
+ if data == 0xFF:
+ flag = True
+ c = file.read(1)
+ if (len(c) == 0):
+ return None
+ data = ord(c)
+ if data >= 0x80:
+ datax = (data & 0x7F)
+ while data >= 0x80 :
+ c = file.read(1)
+ if (len(c) == 0):
+ return None
+ data = ord(c)
+ datax = (datax <<7) + (data & 0x7F)
+ data = datax
+ if flag:
+ data = -data
+ return data
+
+# Get a length prefixed string from the file
+def lengthPrefixString(data):
+ return encodeNumber(len(data))+data
+
+def readString(file):
+ stringLength = readEncodedNumber(file)
+ if (stringLength == None):
+ return None
+ sv = file.read(stringLength)
+ if (len(sv) != stringLength):
+ return ""
+ return unpack(str(stringLength)+"s",sv)[0]
+
+def getMetaArray(metaFile):
+ # parse the meta file
+ result = {}
+ fo = file(metaFile,'rb')
+ size = readEncodedNumber(fo)
+ for i in xrange(size):
+ tag = readString(fo)
+ value = readString(fo)
+ result[tag] = value
+ # print tag, value
+ fo.close()
+ return result
+
+
+# dictionary of all text strings by index value
+class Dictionary(object):
+ def __init__(self, dictFile):
+ self.filename = dictFile
+ self.size = 0
+ self.fo = file(dictFile,'rb')
+ self.stable = []
+ self.size = readEncodedNumber(self.fo)
+ for i in xrange(self.size):
+ self.stable.append(self.escapestr(readString(self.fo)))
+ self.pos = 0
+ def escapestr(self, str):
+ str = str.replace('&','&')
+ str = str.replace('<','<')
+ str = str.replace('>','>')
+ str = str.replace('=','=')
+ return str
+ def lookup(self,val):
+ if ((val >= 0) and (val < self.size)) :
+ self.pos = val
+ return self.stable[self.pos]
+ else:
+ print "Error - %d outside of string table limits" % val
+ sys.exit(-1)
+ def getSize(self):
+ return self.size
+ def getPos(self):
+ return self.pos
+
+
+class PageDimParser(object):
+ def __init__(self, flatxml):
+ self.flatdoc = flatxml.split('\n')
+ # find tag if within pos to end inclusive
+ def findinDoc(self, tagpath, pos, end) :
+ result = None
+ docList = self.flatdoc
+ cnt = len(docList)
+ if end == -1 :
+ end = cnt
+ else:
+ end = min(cnt,end)
+ foundat = -1
+ for j in xrange(pos, end):
+ item = docList[j]
+ if item.find('=') >= 0:
+ (name, argres) = item.split('=')
+ else :
+ name = item
+ argres = ''
+ if name.endswith(tagpath) :
+ result = argres
+ foundat = j
+ break
+ return foundat, result
+ def process(self):
+ (pos, sph) = self.findinDoc('page.h',0,-1)
+ (pos, spw) = self.findinDoc('page.w',0,-1)
+ if (sph == None): sph = '-1'
+ if (spw == None): spw = '-1'
+ return sph, spw
+
+def getPageDim(flatxml):
+ # create a document parser
+ dp = PageDimParser(flatxml)
+ (ph, pw) = dp.process()
+ return ph, pw
+
+class GParser(object):
+ def __init__(self, flatxml):
+ self.flatdoc = flatxml.split('\n')
+ self.dpi = 1440
+ self.gh = self.getData('info.glyph.h')
+ self.gw = self.getData('info.glyph.w')
+ self.guse = self.getData('info.glyph.use')
+ if self.guse :
+ self.count = len(self.guse)
+ else :
+ self.count = 0
+ self.gvtx = self.getData('info.glyph.vtx')
+ self.glen = self.getData('info.glyph.len')
+ self.gdpi = self.getData('info.glyph.dpi')
+ self.vx = self.getData('info.vtx.x')
+ self.vy = self.getData('info.vtx.y')
+ self.vlen = self.getData('info.len.n')
+ if self.vlen :
+ self.glen.append(len(self.vlen))
+ elif self.glen:
+ self.glen.append(0)
+ if self.vx :
+ self.gvtx.append(len(self.vx))
+ elif self.gvtx :
+ self.gvtx.append(0)
+ def getData(self, path):
+ result = None
+ cnt = len(self.flatdoc)
+ for j in xrange(cnt):
+ item = self.flatdoc[j]
+ if item.find('=') >= 0:
+ (name, argt) = item.split('=')
+ argres = argt.split('|')
+ else:
+ name = item
+ argres = []
+ if (name == path):
+ result = argres
+ break
+ if (len(argres) > 0) :
+ for j in xrange(0,len(argres)):
+ argres[j] = int(argres[j])
+ return result
+ def getGlyphDim(self, gly):
+ maxh = (self.gh[gly] * self.dpi) / self.gdpi[gly]
+ maxw = (self.gw[gly] * self.dpi) / self.gdpi[gly]
+ return maxh, maxw
+ def getPath(self, gly):
+ path = ''
+ if (gly < 0) or (gly >= self.count):
+ return path
+ tx = self.vx[self.gvtx[gly]:self.gvtx[gly+1]]
+ ty = self.vy[self.gvtx[gly]:self.gvtx[gly+1]]
+ p = 0
+ for k in xrange(self.glen[gly], self.glen[gly+1]):
+ if (p == 0):
+ zx = tx[0:self.vlen[k]+1]
+ zy = ty[0:self.vlen[k]+1]
+ else:
+ zx = tx[self.vlen[k-1]+1:self.vlen[k]+1]
+ zy = ty[self.vlen[k-1]+1:self.vlen[k]+1]
+ p += 1
+ j = 0
+ while ( j < len(zx) ):
+ if (j == 0):
+ # Start Position.
+ path += 'M %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly])
+ elif (j <= len(zx)-3):
+ # Cubic Bezier Curve
+ path += 'C %d %d %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[j+1] * self.dpi / self.gdpi[gly], zy[j+1] * self.dpi / self.gdpi[gly], zx[j+2] * self.dpi / self.gdpi[gly], zy[j+2] * self.dpi / self.gdpi[gly])
+ j += 2
+ elif (j == len(zx)-2):
+ # Cubic Bezier Curve to Start Position
+ path += 'C %d %d %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[j+1] * self.dpi / self.gdpi[gly], zy[j+1] * self.dpi / self.gdpi[gly], zx[0] * self.dpi / self.gdpi[gly], zy[0] * self.dpi / self.gdpi[gly])
+ j += 1
+ elif (j == len(zx)-1):
+ # Quadratic Bezier Curve to Start Position
+ path += 'Q %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[0] * self.dpi / self.gdpi[gly], zy[0] * self.dpi / self.gdpi[gly])
+
+ j += 1
+ path += 'z'
+ return path
+
+
+
+# dictionary of all text strings by index value
+class GlyphDict(object):
+ def __init__(self):
+ self.gdict = {}
+ def lookup(self, id):
+ # id='id="gl%d"' % val
+ if id in self.gdict:
+ return self.gdict[id]
+ return None
+ def addGlyph(self, val, path):
+ id='id="gl%d"' % val
+ self.gdict[id] = path
+
+
+def generateBook(bookDir, raw, fixedimage):
+ # sanity check Topaz file extraction
+ if not os.path.exists(bookDir) :
+ print "Can not find directory with unencrypted book"
+ return 1
+
+ dictFile = os.path.join(bookDir,'dict0000.dat')
+ if not os.path.exists(dictFile) :
+ print "Can not find dict0000.dat file"
+ return 1
+
+ pageDir = os.path.join(bookDir,'page')
+ if not os.path.exists(pageDir) :
+ print "Can not find page directory in unencrypted book"
+ return 1
+
+ imgDir = os.path.join(bookDir,'img')
+ if not os.path.exists(imgDir) :
+ print "Can not find image directory in unencrypted book"
+ return 1
+
+ glyphsDir = os.path.join(bookDir,'glyphs')
+ if not os.path.exists(glyphsDir) :
+ print "Can not find glyphs directory in unencrypted book"
+ return 1
+
+ metaFile = os.path.join(bookDir,'metadata0000.dat')
+ if not os.path.exists(metaFile) :
+ print "Can not find metadata0000.dat in unencrypted book"
+ return 1
+
+ svgDir = os.path.join(bookDir,'svg')
+ if not os.path.exists(svgDir) :
+ os.makedirs(svgDir)
+
+ xmlDir = os.path.join(bookDir,'xml')
+ if not os.path.exists(xmlDir) :
+ os.makedirs(xmlDir)
+
+ otherFile = os.path.join(bookDir,'other0000.dat')
+ if not os.path.exists(otherFile) :
+ print "Can not find other0000.dat in unencrypted book"
+ return 1
+
+ print "Updating to color images if available"
+ spath = os.path.join(bookDir,'color_img')
+ dpath = os.path.join(bookDir,'img')
+ filenames = os.listdir(spath)
+ filenames = sorted(filenames)
+ for filename in filenames:
+ imgname = filename.replace('color','img')
+ sfile = os.path.join(spath,filename)
+ dfile = os.path.join(dpath,imgname)
+ imgdata = file(sfile,'rb').read()
+ file(dfile,'wb').write(imgdata)
+
+ print "Creating cover.jpg"
+ isCover = False
+ cpath = os.path.join(bookDir,'img')
+ cpath = os.path.join(cpath,'img0000.jpg')
+ if os.path.isfile(cpath):
+ cover = file(cpath, 'rb').read()
+ cpath = os.path.join(bookDir,'cover.jpg')
+ file(cpath, 'wb').write(cover)
+ isCover = True
+
+
+ print 'Processing Dictionary'
+ dict = Dictionary(dictFile)
+
+ print 'Processing Meta Data and creating OPF'
+ meta_array = getMetaArray(metaFile)
+
+ xname = os.path.join(xmlDir, 'metadata.xml')
+ metastr = ''
+ for key in meta_array:
+ metastr += '\n'
+ file(xname, 'wb').write(metastr)
+
+ print 'Processing StyleSheet'
+ # get some scaling info from metadata to use while processing styles
+ fontsize = '135'
+ if 'fontSize' in meta_array:
+ fontsize = meta_array['fontSize']
+
+ # also get the size of a normal text page
+ spage = '1'
+ if 'firstTextPage' in meta_array:
+ spage = meta_array['firstTextPage']
+ pnum = int(spage)
+
+ # get page height and width from first text page for use in stylesheet scaling
+ pname = 'page%04d.dat' % (pnum + 1)
+ fname = os.path.join(pageDir,pname)
+ flat_xml = convert2xml.fromData(dict, fname)
+
+ (ph, pw) = getPageDim(flat_xml)
+ if (ph == '-1') or (ph == '0') : ph = '11000'
+ if (pw == '-1') or (pw == '0') : pw = '8500'
+
+ # print ' ', 'other0000.dat'
+ xname = os.path.join(bookDir, 'style.css')
+ flat_xml = convert2xml.fromData(dict, otherFile)
+ cssstr , classlst = stylexml2css.convert2CSS(flat_xml, fontsize, ph, pw)
+ file(xname, 'wb').write(cssstr)
+ xname = os.path.join(xmlDir, 'other0000.xml')
+ file(xname, 'wb').write(convert2xml.getXML(dict, otherFile))
+
+ print 'Processing Glyphs'
+ gd = GlyphDict()
+ filenames = os.listdir(glyphsDir)
+ filenames = sorted(filenames)
+ glyfname = os.path.join(svgDir,'glyphs.svg')
+ glyfile = open(glyfname, 'w')
+ glyfile.write('\n')
+ glyfile.write('\n')
+ glyfile.write('\n')
+ glyfile.write('Glyphs for %s\n' % meta_array['Title'])
+ glyfile.write('\n')
+ counter = 0
+ for filename in filenames:
+ # print ' ', filename
+ print '.',
+ fname = os.path.join(glyphsDir,filename)
+ flat_xml = convert2xml.fromData(dict, fname)
+
+ xname = os.path.join(xmlDir, filename.replace('.dat','.xml'))
+ file(xname, 'wb').write(convert2xml.getXML(dict, fname))
+
+ gp = GParser(flat_xml)
+ for i in xrange(0, gp.count):
+ path = gp.getPath(i)
+ maxh, maxw = gp.getGlyphDim(i)
+ fullpath = '\n' % (counter * 256 + i, path, maxw, maxh)
+ glyfile.write(fullpath)
+ gd.addGlyph(counter * 256 + i, fullpath)
+ counter += 1
+ glyfile.write('\n')
+ glyfile.write('\n')
+ glyfile.close()
+ print " "
+
+ # start up the html
+ htmlFileName = "book.html"
+ htmlstr = '\n'
+ htmlstr += '\n'
+ htmlstr += '\n'
+ htmlstr += '\n'
+ htmlstr += '\n'
+ htmlstr += '' + meta_array['Title'] + ' by ' + meta_array['Authors'] + '\n'
+ htmlstr += '\n'
+ htmlstr += '\n'
+ htmlstr += '\n'
+ htmlstr += '\n'
+ htmlstr += '\n'
+ htmlstr += '\n\n'
+
+ print 'Processing Pages'
+ # Books are at 1440 DPI. This is rendering at twice that size for
+ # readability when rendering to the screen.
+ scaledpi = 1440.0
+
+ svgindex = '\n'
+ svgindex += '\n'
+ svgindex += ''
+ svgindex += '\n'
+ svgindex += '' + meta_array['Title'] + '\n'
+ svgindex += '\n'
+ svgindex += '\n'
+ svgindex += '\n'
+ svgindex += '\n'
+ svgindex += '\n'
+ svgindex += '\n'
+
+ filenames = os.listdir(pageDir)
+ filenames = sorted(filenames)
+ numfiles = len(filenames)
+ counter = 0
+
+ for filename in filenames:
+ # print ' ', filename
+ print ".",
+
+ fname = os.path.join(pageDir,filename)
+ flat_xml = convert2xml.fromData(dict, fname)
+
+ xname = os.path.join(xmlDir, filename.replace('.dat','.xml'))
+ file(xname, 'wb').write(convert2xml.getXML(dict, fname))
+
+ # first get the html
+ htmlstr += flatxml2html.convert2HTML(flat_xml, classlst, fname, bookDir, gd, fixedimage)
+
+ # now get the svg image of the page
+ svgxml = flatxml2svg.convert2SVG(gd, flat_xml, counter, numfiles, svgDir, raw, meta_array, scaledpi)
+
+ if (raw) :
+ pfile = open(os.path.join(svgDir,filename.replace('.dat','.svg')), 'w')
+ svgindex += 'Page %d\n' % (counter, counter)
+ else :
+ pfile = open(os.path.join(svgDir,'page%04d.xhtml' % counter), 'w')
+ svgindex += 'Page %d\n' % (counter, counter)
+
+
+ pfile.write(svgxml)
+ pfile.close()
+
+ counter += 1
+
+ print " "
+
+ # finish up the html string and output it
+ htmlstr += '\n\n'
+ file(os.path.join(bookDir, htmlFileName), 'wb').write(htmlstr)
+
+ # finish up the svg index string and output it
+ svgindex += '\n\n'
+ file(os.path.join(bookDir, 'index_svg.xhtml'), 'wb').write(svgindex)
+
+ # build the opf file
+ opfname = os.path.join(bookDir, 'book.opf')
+ opfstr = '\n'
+ opfstr += '\n'
+ # adding metadata
+ opfstr += ' \n'
+ opfstr += ' ' + meta_array['GUID'] + '\n'
+ opfstr += ' ' + meta_array['ASIN'] + '\n'
+ opfstr += ' ' + meta_array['oASIN'] + '\n'
+ opfstr += ' ' + meta_array['Title'] + '\n'
+ opfstr += ' ' + meta_array['Authors'] + '\n'
+ opfstr += ' en\n'
+ opfstr += ' ' + meta_array['UpdateTime'] + '\n'
+ if isCover:
+ opfstr += ' \n'
+ opfstr += ' \n'
+ opfstr += '\n'
+ opfstr += ' \n'
+ opfstr += ' \n'
+ # adding image files to manifest
+ filenames = os.listdir(imgDir)
+ filenames = sorted(filenames)
+ for filename in filenames:
+ imgname, imgext = os.path.splitext(filename)
+ if imgext == '.jpg':
+ imgext = 'jpeg'
+ if imgext == '.svg':
+ imgext = 'svg+xml'
+ opfstr += ' \n'
+ if isCover:
+ opfstr += ' \n'
+ opfstr += '\n'
+ # adding spine
+ opfstr += '\n \n\n'
+ if isCover:
+ opfstr += ' \n'
+ opfstr += ' \n'
+ opfstr += ' \n'
+ opfstr += '\n'
+ file(opfname, 'wb').write(opfstr)
+
+ print 'Processing Complete'
+
+ return 0
+
+def usage():
+ print "genbook.py generates a book from the extract Topaz Files"
+ print "Usage:"
+ print " genbook.py [-r] [-h [--fixed-image] "
+ print " "
+ print "Options:"
+ print " -h : help - print this usage message"
+ print " -r : generate raw svg files (not wrapped in xhtml)"
+ print " --fixed-image : genearate any Fixed Area as an svg image in the html"
+ print " "
+
+
+def main(argv):
+ bookDir = ''
+
+ if len(argv) == 0:
+ argv = sys.argv
+
+ try:
+ opts, args = getopt.getopt(argv[1:], "rh:",["fixed-image"])
+
+ except getopt.GetoptError, err:
+ print str(err)
+ usage()
+ return 1
+
+ if len(opts) == 0 and len(args) == 0 :
+ usage()
+ return 1
+
+ raw = 0
+ fixedimage = False
+ for o, a in opts:
+ if o =="-h":
+ usage()
+ return 0
+ if o =="-r":
+ raw = 1
+ if o =="--fixed-image":
+ fixedimage = True
+
+ bookDir = args[0]
+
+ rv = generateBook(bookDir, raw, fixedimage)
+ return rv
+
+
+if __name__ == '__main__':
+ sys.exit(main(''))
diff --git a/Topaz_Tools/lib/genxml.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/genxml.py
similarity index 100%
rename from Topaz_Tools/lib/genxml.py
rename to DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/genxml.py
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignobleepub.pyw b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignobleepub.pyw
index 46cd4e8..469713a 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignobleepub.pyw
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignobleepub.pyw
@@ -144,6 +144,7 @@ class Decryptor(object):
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)
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptepub.pyw b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptepub.pyw
index 701fc2e..442c37a 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptepub.pyw
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptepub.pyw
@@ -1,7 +1,7 @@
#! /usr/bin/python
# -*- coding: utf-8 -*-
-# ineptepub.pyw, version 5.2
+# ineptepub.pyw, version 5.4
# Copyright © 2009-2010 i♥cabbages
# Released under the terms of the GNU General Public Licence, version 3 or
@@ -25,6 +25,8 @@
# 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
+
"""
Decrypt Adobe ADEPT-encrypted EPUB books.
"""
@@ -288,6 +290,7 @@ class Decryptor(object):
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):
diff --git a/Kindle_Mobi_Tools/K4_Mobi_DeDRM_Combined_Tool/lib/k4mdumpkinfo.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mdumpkinfo.py
similarity index 100%
rename from Kindle_Mobi_Tools/K4_Mobi_DeDRM_Combined_Tool/lib/k4mdumpkinfo.py
rename to DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mdumpkinfo.py
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py
index df05f89..5059fc4 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py
@@ -28,7 +28,7 @@
from __future__ import with_statement
-__version__ = '1.2'
+__version__ = '1.4'
class Unbuffered:
def __init__(self, stream):
@@ -44,344 +44,60 @@ import os, csv, getopt
import binascii
import zlib
import re
+import zlib, zipfile, tempfile, shutil
from struct import pack, unpack, unpack_from
-
-#Exception Handling
class DrmException(Exception):
pass
-#
-# crypto digestroutines
-#
-
-import hashlib
-
-def MD5(message):
- ctx = hashlib.md5()
- ctx.update(message)
- return ctx.digest()
-
-def SHA1(message):
- ctx = hashlib.sha1()
- ctx.update(message)
- return ctx.digest()
-
-# determine if we are running as a calibre plugin
if 'calibre' in sys.modules:
inCalibre = True
- global openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap1, charMap2, charMap3, charMap4
else:
inCalibre = False
-#
-# start of Kindle specific routines
-#
-
-if not inCalibre:
- import mobidedrm
- if sys.platform.startswith('win'):
- from k4pcutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap1, charMap2, charMap3, charMap4
- if sys.platform.startswith('darwin'):
- from k4mutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap1, charMap2, charMap3, charMap4
-
-global kindleDatabase
-
-# 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
- 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):
- 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
-
-# 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):
- 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
-
-# convert from 8 digit PID to 10 digit PID with checksum
-def checksumPid(s):
- letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
- crc = (~binascii.crc32(s,-1))&0xFFFFFFFF
- 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
-
-
-class MobiPeek:
- def loadSection(self, section):
- before, after = self.sections[section:section+2]
- self.f.seek(before)
- return self.f.read(after - before)
- def __init__(self, filename):
- self.f = file(filename, 'rb')
- self.header = self.f.read(78)
- self.ident = self.header[0x3C:0x3C+8]
- if self.ident != 'BOOKMOBI' and self.ident != 'TEXtREAd':
- raise DrmException('invalid file format')
- self.num_sections, = unpack_from('>H', self.header, 76)
- sections = self.f.read(self.num_sections*8)
- self.sections = unpack_from('>%dL' % (self.num_sections*2), sections, 0)[::2] + (0xfffffff, )
- self.sect0 = self.loadSection(0)
- self.f.close()
- def getBookTitle(self):
- # get book title
- toff, tlen = unpack('>II', self.sect0[0x54:0x5c])
- tend = toff + tlen
- title = self.sect0[toff:tend]
- return title
- def getexthData(self):
- # if exth region exists then grab it
- # get length of this header
- length, type, codepage, unique_id, version = unpack('>LLLLL', self.sect0[20:40])
- exth_flag, = unpack('>L', self.sect0[0x80:0x84])
- exth = ''
- if exth_flag & 0x40:
- exth = self.sect0[16 + length:]
- return exth
- def isNotEncrypted(self):
- lock_type, = unpack('>H', self.sect0[0xC:0xC+2])
- if lock_type == 0:
- return True
- return False
-
-# DiapDealer's stuff: Parse the EXTH header records and parse the Kindleinfo
-# file to calculate the book pid.
-def getK4Pids(exth, title, kInfoFile=None):
- global kindleDatabase
- try:
- kindleDatabase = parseKindleInfo(kInfoFile)
- except Exception, message:
- print(message)
-
- if kindleDatabase != None :
- # 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)
-
- print("\nDSN: " + DSN)
-
- # Compute the device PID (for which I can tell, is used for nothing).
- # But hey, stuff being printed out is apparently cool.
- table = generatePidEncryptionTable()
- devicePID = generateDevicePID(table,DSN,4)
-
- print("Device PID: " + checksumPid(devicePID))
-
- # Compute book PID
- exth_records = {}
- nitems, = unpack('>I', exth[8:12])
- pos = 12
-
- exth_records[209] = None
- # Parse the exth records, storing data indexed by type
- for i in xrange(nitems):
- type, size = unpack('>II', exth[pos: pos + 8])
- content = exth[pos + 8: pos + size]
-
- exth_records[type] = content
- pos += size
-
- # Grab the contents of the type 209 exth record
- if exth_records[209] != None:
- data = exth_records[209]
- else:
- raise DrmException("\nNo EXTH record type 209 - Perhaps not a K4 file?")
-
- # Parse the 209 data to find the the exth record with the token data.
- # The last character of the 209 data points to the record with the token.
- # Always 208 from my experience, but I'll leave the logic in case that changes.
- for i in xrange(len(data)):
- if ord(data[i]) != 0:
- if exth_records[ord(data[i])] != None:
- token = exth_records[ord(data[i])]
-
- # Get the kindle account token
- kindleAccountToken = getKindleInfoValueForKey("kindle.account.tokens")
-
- print("Account Token: " + kindleAccountToken)
-
- pidHash = SHA1(DSN+kindleAccountToken+exth_records[209]+token)
-
- bookPID = encodePID(pidHash)
- bookPID = checksumPid(bookPID)
-
- if exth_records[503] != None:
- print "Pid for " + exth_records[503] + ": " + bookPID
- else:
- print "Pid for " + title + ":" + bookPID
- return bookPID
-
- raise DrmException("\nCould not access K4 data - Perhaps K4 is not installed/configured?")
- return null
+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 K4PC, K4M, and Mobi ebooks"
+ print "Removes DRM protection from K4PC/M, Kindle, Mobi and Topaz ebooks"
print "Usage:"
- print " %s [-k ] [-p ] " % progname
+ print " %s [-k ] [-p ] [-s ] " % progname
#
# Main
#
def main(argv=sys.argv):
- global kindleDatabase
import mobidedrm
-
+ import topazextract
+ import kgenpids
progname = os.path.basename(argv[0])
+
+ k4 = False
kInfoFiles = []
- pidnums = ""
+ 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:")
+ 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)
@@ -394,108 +110,145 @@ def main(argv=sys.argv):
if o == "-p":
if a == None :
raise DrmException("Invalid parameter for -p")
- pidnums = a
+ 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
- kindleDatabase = None
infile = args[0]
- outfile = args[1]
- DecodeErrorString = ""
- try:
- # first try with K4PC/K4M
- ex = MobiPeek(infile)
- if ex.isNotEncrypted():
- print "File was Not Encrypted"
- return 2
- title = ex.getBookTitle()
- exth = ex.getexthData()
- if exth=='':
- raise DrmException("Not a Kindle Mobipocket file")
- pid = getK4Pids(exth, title)
- unlocked_file = mobidedrm.getUnencryptedBook(infile, pid)
- except DrmException, e:
- DecodeErrorString += "Error trying default K4 info: " + str(e) + "\n"
- pass
- except mobidedrm.DrmException, e:
- DecodeErrorString += "Error trying default K4 info: " + str(e) + "\n"
- pass
+ outdir = args[1]
+
+ # 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:
- file(outfile, 'wb').write(unlocked_file)
- return 0
-
- # now try alternate kindle.info files
- if kInfoFiles:
- for infoFile in kInfoFiles:
- kindleDatabase = None
- try:
- title = ex.getBookTitle()
- exth = ex.getexthData()
- if exth=='':
- raise DrmException("Not a Kindle Mobipocket file")
- pid = getK4Pids(exth, title, infoFile)
- unlocked_file = mobidedrm.getUnencryptedBook(infile, pid)
- except DrmException, e:
- DecodeErrorString += "Error trying " + infoFile + " K4 info: " + str(e) + "\n"
- pass
- except mobidedrm.DrmException, e:
- DecodeErrorString += "Error trying " + infoFile + " K4 info: " + str(e) + "\n"
- pass
- else:
- file(outfile, 'wb').write(unlocked_file)
- return 0
-
- # Lastly, try from the pid list
- pids = pidnums.split(',')
- for pid in pids:
- try:
- print 'Trying: "'+ pid + '"'
- unlocked_file = mobidedrm.getUnencryptedBook(infile, pid)
- except mobidedrm.DrmException:
- pass
+ tempdir = tempfile.mkdtemp()
+ mb = topazextract.TopazBook(infile, tempdir)
+
+ title = mb.getBookTitle()
+ print "Processing Book: ", title
+
+ # 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:
- file(outfile, 'wb').write(unlocked_file)
- return 0
+ mb.processBook(pidlst)
- # we could not unencrypt book
- print DecodeErrorString
- print "Error: Could Not Unencrypt Book"
- return 1
+ except mobidedrm.DrmException, e:
+ print " ... not suceessful " + str(e) + "\n"
+ return 1
+ except topazextract.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()
+ return 1
+ if mobi:
+ outfile = os.path.join(outdir,bookname + '_nodrm' + '.azw')
+ 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, 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)
+ return 0
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, Mobi DeDRM' # Name of the plugin
- description = 'Removes DRM from K4PC, K4Mac, and Mobi files. \
+ 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, 1, 2) # The version number of this plugin
- file_types = set(['prc','mobi','azw']) # The file types that this plugin will be applied to
+ version = (0, 1, 7) # 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 = 200 # run this plugin before mobidedrm, k4pcdedrm, k4dedrm
+ 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
- global kindleDatabase
- global openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap1, charMap2, charMap3, charMap4
- if sys.platform.startswith('win'):
- from k4pcutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap1, charMap2, charMap3, charMap4
- if sys.platform.startswith('darwin'):
- from k4mutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap1, charMap2, charMap3, charMap4
+ from calibre.ptempfile import PersistentTemporaryDirectory
+
+ import kgenpids
+ import zlib
+ import zipfile
+ import topazextract
import mobidedrm
- # Get supplied list of PIDs to try from plugin customization.
- pidnums = self.site_customization
-
- # Load any kindle info files (*.info) included Calibre's config directory.
+ 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]
@@ -513,70 +266,68 @@ if not __name__ == "__main__" and inCalibre:
print 'K4MobiDeDRM: Error reading kindle info files from config directory.'
pass
- # first try with book specifc pid from K4PC or K4M
- try:
- kindleDatabase = None
- ex = MobiPeek(path_to_ebook)
- if ex.isNotEncrypted():
- return path_to_ebook
- title = ex.getBookTitle()
- exth = ex.getexthData()
- if exth=='':
- raise DrmException("Not a Kindle Mobipocket file")
- pid = getK4Pids(exth, title)
- unlocked_file = mobidedrm.getUnencryptedBook(path_to_ebook,pid)
- except DrmException:
- pass
- except mobidedrm.DrmException:
- 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:
- of = self.temporary_file('.mobi')
+ 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
-
- # Now try alternate kindle info files
- if kInfoFiles:
- for infoFile in kInfoFiles:
- kindleDatabase = None
- try:
- title = ex.getBookTitle()
- exth = ex.getexthData()
- if exth=='':
- raise DrmException("Not a Kindle Mobipocket file")
- pid = getK4Pids(exth, title, infoFile)
- unlocked_file = mobidedrm.getUnencryptedBook(path_to_ebook,pid)
- except DrmException:
- pass
- except mobidedrm.DrmException:
- pass
- else:
- of = self.temporary_file('.mobi')
- of.write(unlocked_file)
- of.close()
- return of.name
- # now try from the pid list
- pids = pidnums.split(',')
- for pid in pids:
- try:
- unlocked_file = mobidedrm.getUnencryptedBook(path_to_ebook, pid)
- except mobidedrm.DrmException:
- pass
- else:
- of = self.temporary_file('.mobi')
- of.write(unlocked_file)
- of.close()
- return of.name
-
- #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 ""
+ # 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 each 10 character PID separated by a comma (no spaces).'
+ return 'Enter 10 character PIDs and/or Kindle serial numbers, separated by commas.'
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mutils.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mutils.py
index 33771eb..4aa14dd 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mutils.py
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mutils.py
@@ -1,159 +1,14 @@
# standlone set of Mac OSX specific routines needed for K4DeDRM
from __future__ import with_statement
-
import sys
import os
+import subprocess
+
-#Exception Handling
class K4MDrmException(Exception):
pass
-import signal
-import threading
-import subprocess
-from subprocess import Popen, PIPE, STDOUT
-
-# **heavily** chopped up and modfied version of asyncproc.py
-# to make it actually work on Windows as well as Mac/Linux
-# For the original see:
-# "http://www.lysator.liu.se/~bellman/download/"
-# author is "Thomas Bellman "
-# available under GPL version 3 or Later
-
-# create an asynchronous subprocess whose output can be collected in
-# a non-blocking manner
-
-# What a mess! Have to use threads just to get non-blocking io
-# in a cross-platform manner
-
-# luckily all thread use is hidden within this class
-
-class Process(object):
- def __init__(self, *params, **kwparams):
- if len(params) <= 3:
- kwparams.setdefault('stdin', subprocess.PIPE)
- if len(params) <= 4:
- kwparams.setdefault('stdout', subprocess.PIPE)
- if len(params) <= 5:
- kwparams.setdefault('stderr', subprocess.PIPE)
- self.__pending_input = []
- self.__collected_outdata = []
- self.__collected_errdata = []
- self.__exitstatus = None
- self.__lock = threading.Lock()
- self.__inputsem = threading.Semaphore(0)
- self.__quit = False
-
- self.__process = subprocess.Popen(*params, **kwparams)
-
- if self.__process.stdin:
- self.__stdin_thread = threading.Thread(
- name="stdin-thread",
- target=self.__feeder, args=(self.__pending_input,
- self.__process.stdin))
- self.__stdin_thread.setDaemon(True)
- self.__stdin_thread.start()
-
- if self.__process.stdout:
- self.__stdout_thread = threading.Thread(
- name="stdout-thread",
- target=self.__reader, args=(self.__collected_outdata,
- self.__process.stdout))
- self.__stdout_thread.setDaemon(True)
- self.__stdout_thread.start()
-
- if self.__process.stderr:
- self.__stderr_thread = threading.Thread(
- name="stderr-thread",
- target=self.__reader, args=(self.__collected_errdata,
- self.__process.stderr))
- self.__stderr_thread.setDaemon(True)
- self.__stderr_thread.start()
-
- def pid(self):
- return self.__process.pid
-
- def kill(self, signal):
- self.__process.send_signal(signal)
-
- # check on subprocess (pass in 'nowait') to act like poll
- def wait(self, flag):
- if flag.lower() == 'nowait':
- rc = self.__process.poll()
- else:
- rc = self.__process.wait()
- if rc != None:
- if self.__process.stdin:
- self.closeinput()
- if self.__process.stdout:
- self.__stdout_thread.join()
- if self.__process.stderr:
- self.__stderr_thread.join()
- return self.__process.returncode
-
- def terminate(self):
- if self.__process.stdin:
- self.closeinput()
- self.__process.terminate()
-
- # thread gets data from subprocess stdout
- def __reader(self, collector, source):
- while True:
- data = os.read(source.fileno(), 65536)
- self.__lock.acquire()
- collector.append(data)
- self.__lock.release()
- if data == "":
- source.close()
- break
- return
-
- # thread feeds data to subprocess stdin
- def __feeder(self, pending, drain):
- while True:
- self.__inputsem.acquire()
- self.__lock.acquire()
- if not pending and self.__quit:
- drain.close()
- self.__lock.release()
- break
- data = pending.pop(0)
- self.__lock.release()
- drain.write(data)
-
- # non-blocking read of data from subprocess stdout
- def read(self):
- self.__lock.acquire()
- outdata = "".join(self.__collected_outdata)
- del self.__collected_outdata[:]
- self.__lock.release()
- return outdata
-
- # non-blocking read of data from subprocess stderr
- def readerr(self):
- self.__lock.acquire()
- errdata = "".join(self.__collected_errdata)
- del self.__collected_errdata[:]
- self.__lock.release()
- return errdata
-
- # non-blocking write to stdin of subprocess
- def write(self, data):
- if self.__process.stdin is None:
- raise ValueError("Writing to process with stdin not a pipe")
- self.__lock.acquire()
- self.__pending_input.append(data)
- self.__inputsem.release()
- self.__lock.release()
-
- # close stdinput of subprocess
- def closeinput(self):
- self.__lock.acquire()
- self.__quit = True
- self.__inputsem.release()
- self.__lock.release()
-
# interface to needed routines in openssl's libcrypto
def _load_crypto_libcrypto():
@@ -236,6 +91,15 @@ 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():
@@ -244,10 +108,9 @@ def GetVolumeSerialNumber():
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')
+ 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
@@ -274,11 +137,6 @@ def GetUserName():
username = os.getenv('USER')
return username
-# Various character maps used to decrypt books. Probably supposed to act as obfuscation
-charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
-charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM"
-charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
-charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
def encode(data, map):
result = ""
@@ -309,16 +167,16 @@ def CryptUnprotectData(encryptedData):
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 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False)
- poll = p1.wait('wait')
- results = p1.read()
- reslst = results.split('\n')
+ 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):
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kgenpids.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kgenpids.py
new file mode 100644
index 0000000..5c44bfa
--- /dev/null
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kgenpids.py
@@ -0,0 +1,312 @@
+#!/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:
+ # 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)
+ 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:
+ print "\nNo EXTH record type 209 - 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
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py
index 183432c..cc83224 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py
@@ -24,7 +24,7 @@
# 0.14 - Working out when the extra data flags are present has been problematic
# Versions 7 through 9 have tried to tweak the conditions, but have been
# only partially successful. Closer examination of lots of sample
-# files reveals that a confusin has arisen because trailing data entries
+# files reveals that a confusion has arisen because trailing data entries
# are not encrypted, but it turns out that the multibyte entries
# in utf8 file are encrypted. (Although neither kind gets compressed.)
# This knowledge leads to a simplification of the test for the
@@ -39,13 +39,13 @@
# Removed the disabled Calibre plug-in code
# Permit use of 8-digit PIDs
# 0.19 - It seems that multibyte entries aren't encrypted in a v6 file either.
-# 0.20 - Corretion: It seems that multibyte entries are encrypted in a v6 file.
+# 0.20 - Correction: It seems that multibyte entries are encrypted in a v6 file.
+# 0.21 - Added support for multiple pids
+# 0.22 - revised structure to hold MobiBook as a class to allow an extended interface
-__version__ = '0.20'
+__version__ = '0.22'
import sys
-import struct
-import binascii
class Unbuffered:
def __init__(self, stream):
@@ -55,10 +55,19 @@ class Unbuffered:
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
+sys.stdout=Unbuffered(sys.stdout)
+
+import struct
+import binascii
class DrmException(Exception):
pass
+
+#
+# MobiBook Utility Routines
+#
+
# Implementation of Pukall Cipher 1
def PC1(key, src, decryption=True):
sum1 = 0;
@@ -70,7 +79,6 @@ def PC1(key, src, decryption=True):
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;
@@ -131,7 +139,9 @@ def getSizeOfTrailingDataEntries(ptr, size, flags):
num += (ord(ptr[size - num - 1]) & 0x3) + 1
return num
-class DrmStripper:
+
+
+class MobiBook:
def loadSection(self, section):
if (section + 1 == self.num_sections):
endoff = len(self.data_file)
@@ -140,6 +150,78 @@ class DrmStripper:
off = self.sections[section][0]
return self.data_file[off:endoff]
+ def __init__(self, infile):
+ # initial sanity check on file
+ self.data_file = file(infile, 'rb').read()
+ self.header = self.data_file[0:78]
+ if self.header[0x3C:0x3C+8] != 'BOOKMOBI':
+ raise DrmException("invalid file format")
+
+ # build up section offset and flag info
+ self.num_sections, = struct.unpack('>H', self.header[76:78])
+ self.sections = []
+ for i in xrange(self.num_sections):
+ offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', self.data_file[78+i*8:78+i*8+8])
+ flags, val = a1, a2<<16|a3<<8|a4
+ self.sections.append( (offset, flags, val) )
+
+ # parse information from section 0
+ self.sect = self.loadSection(0)
+ self.records, = struct.unpack('>H', self.sect[0x8:0x8+2])
+ self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18])
+ self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C])
+ print "MOBI header version = %d, length = %d" %(self.mobi_version, self.mobi_length)
+ self.extra_data_flags = 0
+ if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
+ self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4])
+ print "Extra Data Flags = %d" % self.extra_data_flags
+ if self.mobi_version < 7:
+ # multibyte utf8 data is included in the encryption for mobi_version 6 and below
+ # so clear that byte so that we leave it to be decrypted.
+ self.extra_data_flags &= 0xFFFE
+
+ # if exth region exists parse it for metadata array
+ self.meta_array = {}
+ exth_flag, = struct.unpack('>L', self.sect[0x80:0x84])
+ exth = ''
+ if exth_flag & 0x40:
+ exth = self.sect[16 + self.mobi_length:]
+ nitems, = struct.unpack('>I', exth[8:12])
+ pos = 12
+ for i in xrange(nitems):
+ type, size = struct.unpack('>II', exth[pos: pos + 8])
+ content = exth[pos + 8: pos + size]
+ self.meta_array[type] = content
+ pos += size
+
+ def getBookTitle(self):
+ title = ''
+ if 503 in self.meta_array:
+ title = self.meta_array[503]
+ else :
+ toff, tlen = struct.unpack('>II', self.sect[0x54:0x5c])
+ tend = toff + tlen
+ title = self.sect[toff:tend]
+ if title == '':
+ title = self.header[:32]
+ title = title.split("\0")[0]
+ return title
+
+ def getPIDMetaInfo(self):
+ rec209 = None
+ token = None
+ if 209 in self.meta_array:
+ rec209 = self.meta_array[209]
+ data = rec209
+ # Parse the 209 data to find the the exth record with the token data.
+ # The last character of the 209 data points to the record with the token.
+ # Always 208 from my experience, but I'll leave the logic in case that changes.
+ for i in xrange(len(data)):
+ if ord(data[i]) != 0:
+ if self.meta_array[ord(data[i])] != None:
+ token = self.meta_array[ord(data[i])]
+ return rec209, token
+
def patch(self, off, new):
self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):]
@@ -152,134 +234,122 @@ class DrmStripper:
assert off + in_off + len(new) <= endoff
self.patch(off + in_off, new)
- def parseDRM(self, data, count, pid):
- pid = pid.ljust(16,'\0')
- keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96"
- temp_key = PC1(keyvec1, pid, False)
- temp_key_sum = sum(map(ord,temp_key)) & 0xff
+ def parseDRM(self, data, count, pidlist):
found_key = None
- for i in xrange(count):
- verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
- cookie = PC1(temp_key, cookie)
- ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
- if verification == ver and cksum == temp_key_sum and (flags & 0x1F) == 1:
- found_key = finalkey
+ keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96"
+ for pid in pidlist:
+ bigpid = pid.ljust(16,'\0')
+ temp_key = PC1(keyvec1, bigpid, False)
+ temp_key_sum = sum(map(ord,temp_key)) & 0xff
+ found_key = None
+ for i in xrange(count):
+ verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
+ if cksum == temp_key_sum:
+ cookie = PC1(temp_key, cookie)
+ ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
+ if verification == ver and (flags & 0x1F) == 1:
+ found_key = finalkey
+ break
+ if found_key != None:
break
if not found_key:
# Then try the default encoding that doesn't require a PID
+ pid = "00000000"
temp_key = keyvec1
temp_key_sum = sum(map(ord,temp_key)) & 0xff
for i in xrange(count):
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
- cookie = PC1(temp_key, cookie)
- ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
- if verification == ver and cksum == temp_key_sum:
- found_key = finalkey
- break
- return found_key
+ if cksum == temp_key_sum:
+ cookie = PC1(temp_key, cookie)
+ ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
+ if verification == ver:
+ found_key = finalkey
+ break
+ return [found_key,pid]
- def __init__(self, data_file, pid):
- if len(pid)==10:
- if checksumPid(pid[0:-2]) != pid:
- raise DrmException("invalid PID checksum")
- pid = pid[0:-2]
- elif len(pid)==8:
- print "PID without checksum given. With checksum PID is "+checksumPid(pid)
- else:
- raise DrmException("Invalid PID length")
-
- self.data_file = data_file
- header = data_file[0:72]
- if header[0x3C:0x3C+8] != 'BOOKMOBI':
- raise DrmException("invalid file format")
- self.num_sections, = struct.unpack('>H', data_file[76:78])
-
- self.sections = []
- for i in xrange(self.num_sections):
- offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', data_file[78+i*8:78+i*8+8])
- flags, val = a1, a2<<16|a3<<8|a4
- self.sections.append( (offset, flags, val) )
-
- sect = self.loadSection(0)
- records, = struct.unpack('>H', sect[0x8:0x8+2])
- mobi_length, = struct.unpack('>L',sect[0x14:0x18])
- mobi_version, = struct.unpack('>L',sect[0x68:0x6C])
- extra_data_flags = 0
- print "MOBI header version = %d, length = %d" %(mobi_version, mobi_length)
- if (mobi_length >= 0xE4) and (mobi_version >= 5):
- extra_data_flags, = struct.unpack('>H', sect[0xF2:0xF4])
- print "Extra Data Flags = %d" %extra_data_flags
- if mobi_version < 7:
- # multibyte utf8 data is included in the encryption for mobi_version 6 and below
- # so clear that byte so that we leave it to be decrypted.
- extra_data_flags &= 0xFFFE
-
- crypto_type, = struct.unpack('>H', sect[0xC:0xC+2])
+ def processBook(self, pidlist):
+ crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2])
if crypto_type == 0:
print "This book is not encrypted."
+ return self.data_file
+ if crypto_type == 1:
+ raise DrmException("Cannot decode Mobipocket encryption type 1")
+ if crypto_type != 2:
+ raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type)
+
+ goodpids = []
+ for pid in pidlist:
+ if len(pid)==10:
+ if checksumPid(pid[0:-2]) != pid:
+ print "Warning: PID " + pid + " has incorrect checksum, should have been "+checksumPid(pid[0:-2])
+ goodpids.append(pid[0:-2])
+ elif len(pid)==8:
+ goodpids.append(pid)
+
+ # calculate the keys
+ drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16])
+ if drm_count == 0:
+ raise DrmException("Not yet initialised with PID. Must be opened with Mobipocket Reader first.")
+ found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids)
+ if not found_key:
+ raise DrmException("No key found. Most likely the correct PID has not been given.")
+
+ if pid=="00000000":
+ print "File has default encryption, no specific PID."
else:
- if crypto_type == 1:
- raise DrmException("cannot decode Mobipocket encryption type 1")
- if crypto_type != 2:
- raise DrmException("unknown encryption type: %d" % crypto_type)
+ print "File is encoded with PID "+checksumPid(pid)+"."
- # calculate the keys
- drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', sect[0xA8:0xA8+16])
- if drm_count == 0:
- raise DrmException("no PIDs found in this file")
- found_key = self.parseDRM(sect[drm_ptr:drm_ptr+drm_size], drm_count, pid)
- if not found_key:
- raise DrmException("no key found. maybe the PID is incorrect")
+ # kill the drm keys
+ self.patchSection(0, "\0" * drm_size, drm_ptr)
+ # kill the drm pointers
+ self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8)
+ # clear the crypto type
+ self.patchSection(0, "\0" * 2, 0xC)
- # kill the drm keys
- self.patchSection(0, "\0" * drm_size, drm_ptr)
- # kill the drm pointers
- self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8)
- # clear the crypto type
- self.patchSection(0, "\0" * 2, 0xC)
-
- # decrypt sections
- print "Decrypting. Please wait . . .",
- new_data = self.data_file[:self.sections[1][0]]
- for i in xrange(1, records+1):
- data = self.loadSection(i)
- extra_size = getSizeOfTrailingDataEntries(data, len(data), extra_data_flags)
- if i%100 == 0:
- print ".",
- # print "record %d, extra_size %d" %(i,extra_size)
- new_data += PC1(found_key, data[0:len(data) - extra_size])
- if extra_size > 0:
- new_data += data[-extra_size:]
- #self.patchSection(i, PC1(found_key, data[0:len(data) - extra_size]))
- if self.num_sections > records+1:
- new_data += self.data_file[self.sections[records+1][0]:]
- self.data_file = new_data
- print "done"
-
- def getResult(self):
+ # decrypt sections
+ print "Decrypting. Please wait . . .",
+ new_data = self.data_file[:self.sections[1][0]]
+ for i in xrange(1, self.records+1):
+ data = self.loadSection(i)
+ extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags)
+ if i%100 == 0:
+ print ".",
+ # print "record %d, extra_size %d" %(i,extra_size)
+ new_data += PC1(found_key, data[0:len(data) - extra_size])
+ if extra_size > 0:
+ new_data += data[-extra_size:]
+ if self.num_sections > self.records+1:
+ new_data += self.data_file[self.sections[self.records+1][0]:]
+ self.data_file = new_data
+ print "done"
return self.data_file
def getUnencryptedBook(infile,pid):
- sys.stdout=Unbuffered(sys.stdout)
- data_file = file(infile, 'rb').read()
- strippedFile = DrmStripper(data_file, pid)
- return strippedFile.getResult()
+ if not os.path.isfile(infile):
+ raise DrmException('Input File Not Found')
+ book = MobiBook(infile)
+ return book.processBook([pid])
+
+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):
- sys.stdout=Unbuffered(sys.stdout)
print ('MobiDeDrm v%(__version__)s. '
'Copyright 2008-2010 The Dark Reverser.' % globals())
if len(argv)<4:
print "Removes protection from Mobipocket books"
print "Usage:"
- print " %s " % sys.argv[0]
+ print " %s " % sys.argv[0]
return 1
else:
infile = argv[1]
outfile = argv[2]
- pid = argv[3]
+ pidlist = argv[3].split(',')
try:
- stripped_file = getUnencryptedBook(infile, pid)
+ stripped_file = getUnencryptedBookWithList(infile, pidlist)
file(outfile, 'wb').write(stripped_file)
except DrmException, e:
print "Error: %s" % e
diff --git a/Kindle_Mobi_Tools/K4_Mobi_DeDRM_Combined_Tool/lib/scrolltextwidget.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/scrolltextwidget.py
similarity index 100%
rename from Kindle_Mobi_Tools/K4_Mobi_DeDRM_Combined_Tool/lib/scrolltextwidget.py
rename to DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/scrolltextwidget.py
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/stylexml2css.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/stylexml2css.py
new file mode 100644
index 0000000..73f798f
--- /dev/null
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/stylexml2css.py
@@ -0,0 +1,243 @@
+#! /usr/bin/python
+# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
+# For use with Topaz Scripts Version 2.6
+
+import csv
+import sys
+import os
+import getopt
+from struct import pack
+from struct import unpack
+
+
+class DocParser(object):
+ def __init__(self, flatxml, fontsize, ph, pw):
+ self.flatdoc = flatxml.split('\n')
+ self.fontsize = int(fontsize)
+ self.ph = int(ph) * 1.0
+ self.pw = int(pw) * 1.0
+
+ stags = {
+ 'paragraph' : 'p',
+ 'graphic' : '.graphic'
+ }
+
+ attr_val_map = {
+ 'hang' : 'text-indent: ',
+ 'indent' : 'text-indent: ',
+ 'line-space' : 'line-height: ',
+ 'margin-bottom' : 'margin-bottom: ',
+ 'margin-left' : 'margin-left: ',
+ 'margin-right' : 'margin-right: ',
+ 'margin-top' : 'margin-top: ',
+ 'space-after' : 'padding-bottom: ',
+ }
+
+ attr_str_map = {
+ 'align-center' : 'text-align: center; margin-left: auto; margin-right: auto;',
+ 'align-left' : 'text-align: left;',
+ 'align-right' : 'text-align: right;',
+ 'align-justify' : 'text-align: justify;',
+ 'display-inline' : 'display: inline;',
+ 'pos-left' : 'text-align: left;',
+ 'pos-right' : 'text-align: right;',
+ 'pos-center' : 'text-align: center; margin-left: auto; margin-right: auto;',
+ }
+
+
+ # find tag if within pos to end inclusive
+ def findinDoc(self, tagpath, pos, end) :
+ result = None
+ docList = self.flatdoc
+ cnt = len(docList)
+ if end == -1 :
+ end = cnt
+ else:
+ end = min(cnt,end)
+ foundat = -1
+ for j in xrange(pos, end):
+ item = docList[j]
+ if item.find('=') >= 0:
+ (name, argres) = item.split('=',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 process(self):
+
+ classlst = ''
+ csspage = '.cl-center { text-align: center; margin-left: auto; margin-right: auto; }\n'
+ csspage += '.cl-right { text-align: right; }\n'
+ csspage += '.cl-left { text-align: left; }\n'
+ csspage += '.cl-justify { text-align: justify; }\n'
+
+ # generate a list of each \n'
- final += '\n'
- in_tags = []
- def makeText(s):
- s = s.replace('&', '&')
- #s = s.replace('"', '"')
- s = s.replace('<', '<')
- s = s.replace('>', '>')
- s = s.replace('\n', '
\n')
- return s
- while True:
- r = self.next()
- if not r:
- break
- text, cmd, attr = r
- if text:
- final += makeText(text)
- if cmd:
- def getTag(ti, end):
- cmd, attr = ti
- r = self.html_tags[cmd][end]
- if type(r) != str:
- r = r(attr)
- return r
- if cmd in self.html_tags:
- pair = (cmd, attr)
- if cmd not in [a for (a,b) in in_tags]:
- final += getTag(pair, False)
- in_tags.append(pair)
- else:
- j = len(in_tags)
- while True:
- j = j - 1
- final += getTag(in_tags[j], True)
- if in_tags[j][0] == cmd:
- break
- del in_tags[j]
- while j < len(in_tags):
- final += getTag(in_tags[j], False)
- j = j + 1
-
- if cmd in self.html_one_tags:
- final += self.html_one_tags[cmd]
- if cmd == 'm':
- unquotedimagepath = "images/" + attr
- imagepath = urllib.quote( unquotedimagepath )
- final += '' % imagepath
- if cmd == 'Q':
- final += ' ' % attr
- if cmd == 'C0':
- final += '' % attr
- if cmd == 'C1':
- final += '' % attr
- if cmd == 'C2':
- final += '' % attr
- if cmd == 'C3':
- final += '' % attr
- if cmd == 'C4':
- final += '' % attr
- if cmd == 'a':
- final += self.pml_chars.get(attr, '%d;' % attr)
- if cmd == 'U':
- final += '%d;' % attr
- final += '