From b1feca321df4804b0f178ce96c476a70b9016113 Mon Sep 17 00:00:00 2001 From: Apprentice Alf Date: Wed, 2 Oct 2013 19:59:40 +0100 Subject: [PATCH] tools v6.0.8 --- .../DeDRM.app/Contents/Info.plist | 6 +- .../DeDRM.app/Contents/Resources/__init__.py | 9 +- .../DeDRM.app/Contents/Resources/android.py | 157 ++ .../Contents/Resources/android_readme.txt | 6 + .../Contents/Resources/flatxml2html.py | 6 +- .../Contents/Resources/k4mobidedrm.py | 11 +- .../DeDRM.app/Contents/Resources/kindlekey.py | 32 +- .../DeDRM.app/Contents/Resources/mobidedrm.py | 2 + .../Contents/Resources/stylexml2css.py | 7 +- .../Contents/Resources/topazextract.py | 2 +- .../DeDRM_App/DeDRM_lib/DeDRM_app.pyw | 2 +- .../DeDRM_App/DeDRM_lib/lib/__init__.py | 9 +- .../DeDRM_App/DeDRM_lib/lib/android.py | 157 ++ .../DeDRM_lib/lib/android_readme.txt | 6 + .../DeDRM_App/DeDRM_lib/lib/flatxml2html.py | 6 +- .../DeDRM_App/DeDRM_lib/lib/k4mobidedrm.py | 11 +- .../DeDRM_App/DeDRM_lib/lib/kindlekey.py | 32 +- .../DeDRM_App/DeDRM_lib/lib/mobidedrm.py | 2 + .../DeDRM_App/DeDRM_lib/lib/stylexml2css.py | 7 +- .../DeDRM_App/DeDRM_lib/lib/topazextract.py | 2 +- DeDRM_calibre_plugin/DeDRM_plugin.zip | Bin 334161 -> 336887 bytes DeDRM_calibre_plugin/DeDRM_plugin/__init__.py | 9 +- DeDRM_calibre_plugin/DeDRM_plugin/android.py | 157 ++ .../DeDRM_plugin/android_readme.txt | 6 + DeDRM_calibre_plugin/DeDRM_plugin/dialogs.py | 45 - .../DeDRM_plugin/encodebase64.py | 227 +- DeDRM_calibre_plugin/DeDRM_plugin/epubtest.py | 659 +---- DeDRM_calibre_plugin/DeDRM_plugin/erdr2pml.py | 1378 ++++----- .../DeDRM_plugin/flatxml2html.py | 928 +++++-- .../DeDRM_plugin/flatxml2svg.py | 856 ++---- DeDRM_calibre_plugin/DeDRM_plugin/genbook.py | 1129 +++++--- .../DeDRM_plugin/ignobleepub.py | 340 ++- .../DeDRM_plugin/ignoblekeygen.py | 508 +--- .../DeDRM_plugin/ineptepub.py | 2097 ++------------ DeDRM_calibre_plugin/DeDRM_plugin/ineptpdf.py | 2466 +++++++++++++++-- .../DeDRM_plugin/k4mobidedrm.py | 541 ++-- DeDRM_calibre_plugin/DeDRM_plugin/kgenpids.py | 2039 ++------------ .../DeDRM_plugin/kindlekey.py | 1938 ++++++++++++- .../DeDRM_plugin/kindlepid.py | Bin 87160 -> 4738 bytes .../DeDRM_plugin/libalfcrypto.dylib | Bin 23859 -> 87160 bytes .../DeDRM_plugin/libalfcrypto32.so | Bin 33417 -> 23859 bytes .../DeDRM_plugin/libalfcrypto64.so | Bin 21895 -> 33417 bytes .../DeDRM_plugin/mobidedrm.py | 620 ++++- .../DeDRM_plugin/openssl_des.py | 89 + .../DeDRM_plugin/plugin-import-name-dedrm.txt | 292 -- DeDRM_calibre_plugin/DeDRM_plugin/prefs.py | 292 ++ .../DeDRM_plugin/stylexml2css.py | 7 +- .../DeDRM_plugin/subasyncio.py | 148 + .../DeDRM_plugin/topazextract.py | 680 ++++- .../DeDRM_plugin/utilities.py | 561 +--- .../DeDRM_plugin/wineutils.py | 60 + .../DeDRM_plugin/zipfilerugged.py | 1437 +++++++++- DeDRM_calibre_plugin/DeDRM_plugin/zipfix.py | 218 +- .../Kindle_for_Mac_and_PC/kindlekey.pyw | 32 +- 54 files changed, 11419 insertions(+), 8812 deletions(-) create mode 100644 DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/android.py create mode 100644 DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/android_readme.txt create mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/android.py create mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/android_readme.txt create mode 100644 DeDRM_calibre_plugin/DeDRM_plugin/android.py create mode 100644 DeDRM_calibre_plugin/DeDRM_plugin/android_readme.txt delete mode 100644 DeDRM_calibre_plugin/DeDRM_plugin/dialogs.py create mode 100644 DeDRM_calibre_plugin/DeDRM_plugin/prefs.py create mode 100644 DeDRM_calibre_plugin/DeDRM_plugin/subasyncio.py create mode 100644 DeDRM_calibre_plugin/DeDRM_plugin/wineutils.py diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist index 9a030d4..cfd9fcc 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist @@ -24,19 +24,19 @@ CFBundleExecutable droplet CFBundleGetInfoString - DeDRM AppleScript 6.0.7. Written 2010–2013 by Apprentice Alf and others. + DeDRM AppleScript 6.0.8. Written 2010–2013 by Apprentice Alf and others. CFBundleIconFile DeDRM CFBundleIdentifier com.apple.ScriptEditor.id.707CCCD5-0C6C-4BEB-B67C-B6E866ADE85A CFBundleInfoDictionaryVersion - 6.0.7 + 6.0.8 CFBundleName DeDRM CFBundlePackageType APPL CFBundleShortVersionString - 6.0.7 + 6.0.8 CFBundleSignature dplt LSRequiresCarbon diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/__init__.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/__init__.py index caed6e8..37d4cb1 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/__init__.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/__init__.py @@ -31,14 +31,17 @@ __docformat__ = 'restructuredtext en' # 6.0.3 - Fixes for Kindle for Mac and Windows non-ascii user names # 6.0.4 - Fixes for stand-alone scripts and applications # and pdb files in plugin and initial conversion of prefs. +# 6.0.5 - Fix a key issue # 6.0.6 - Fix up an incorrect function call +# 6.0.7 - Error handling for incomplete PDF metadata +# 6.0.8 - Fixes a Wine key issue and topaz support """ Decrypt DRMed ebooks. """ PLUGIN_NAME = u"DeDRM" -PLUGIN_VERSION_TUPLE = (6, 0, 7) +PLUGIN_VERSION_TUPLE = (6, 0, 8) PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE]) # Include an html helpfile in the plugin's zipfile with the following name. RESOURCE_NAME = PLUGIN_NAME + '_Help.htm' @@ -313,7 +316,7 @@ class DeDRM(FileTypePlugin): from wineutils import WineGetKeys scriptpath = os.path.join(self.alfdir,u"adobekey.py") - defaultkeys = self.WineGetKeys(scriptpath, u".der",dedrmprefs['adobewineprefix']) + defaultkeys = WineGetKeys(scriptpath, u".der",dedrmprefs['adobewineprefix']) except: pass @@ -391,7 +394,7 @@ class DeDRM(FileTypePlugin): from wineutils import WineGetKeys scriptpath = os.path.join(self.alfdir,u"kindlekey.py") - defaultkeys = self.WineGetKeys(scriptpath, u".k4i",dedrmprefs['kindlewineprefix']) + defaultkeys = WineGetKeys(scriptpath, u".k4i",dedrmprefs['kindlewineprefix']) except: pass diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/android.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/android.py new file mode 100644 index 0000000..ddb94f5 --- /dev/null +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/android.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python +#fileencoding: utf-8 + +import os +import sys +import zlib +import tarfile +from hashlib import md5 +from cStringIO import StringIO +from binascii import a2b_hex, b2a_hex + +STORAGE = 'AmazonSecureStorage.xml' + +class AndroidObfuscation(object): + '''AndroidObfuscation + For the key, it's written in java, and run in android dalvikvm + ''' + + key = a2b_hex('0176e04c9408b1702d90be333fd53523') + + def encrypt(self, plaintext): + cipher = self._get_cipher() + padding = len(self.key) - len(plaintext) % len(self.key) + plaintext += chr(padding) * padding + return b2a_hex(cipher.encrypt(plaintext)) + + def decrypt(self, ciphertext): + cipher = self._get_cipher() + plaintext = cipher.decrypt(a2b_hex(ciphertext)) + return plaintext[:-ord(plaintext[-1])] + + def _get_cipher(self): + try: + from Crypto.Cipher import AES + return AES.new(self.key) + except ImportError: + from aescbc import AES, noPadding + return AES(self.key, padding=noPadding()) + +class AndroidObfuscationV2(AndroidObfuscation): + '''AndroidObfuscationV2 + ''' + + count = 503 + password = 'Thomsun was here!' + + def __init__(self, salt): + key = self.password + salt + for _ in range(self.count): + key = md5(key).digest() + self.key = key[:8] + self.iv = key[8:16] + + def _get_cipher(self): + try : + from Crypto.Cipher import DES + return DES.new(self.key, DES.MODE_CBC, self.iv) + except ImportError: + from python_des import Des, CBC + return Des(self.key, CBC, self.iv) + +def parse_preference(path): + ''' parse android's shared preference xml ''' + storage = {} + read = open(path) + for line in read: + line = line.strip() + # value + if line.startswith(' adb backup com.amazon.kindle + ''' + output = None + read = open(path, 'rb') + head = read.read(24) + if head == 'ANDROID BACKUP\n1\n1\nnone\n': + output = StringIO(zlib.decompress(read.read())) + read.close() + + if not output: + return False + + tar = tarfile.open(fileobj=output) + for member in tar.getmembers(): + if member.name.strip().endswith(STORAGE): + write = open(STORAGE, 'w') + write.write(tar.extractfile(member).read()) + write.close() + break + + return True + +__all__ = [ 'get_storage', 'get_serials', 'parse_preference', + 'AndroidObfuscation', 'AndroidObfuscationV2', 'STORAGE'] + +if __name__ == '__main__': + print get_serials() \ No newline at end of file diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/android_readme.txt b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/android_readme.txt new file mode 100644 index 0000000..9e7d035 --- /dev/null +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/android_readme.txt @@ -0,0 +1,6 @@ +1.1 get AmazonSecureStorage.xml from /data/data/com.amazon.kindle/shared_prefs/AmazonSecureStorage.xml + +1.2 on android 4.0+, run `adb backup com.amazon.kindle` from PC will get backup.ab + now android.py can convert backup.ab to AmazonSecureStorage.xml + +2. run `k4mobidedrm.py -a AmazonSecureStorage.xml ' diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/flatxml2html.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/flatxml2html.py index 4d83368..991591b 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/flatxml2html.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/flatxml2html.py @@ -458,7 +458,11 @@ class DocParser(object): (wtype, num) = pdesc[j] if wtype == 'ocr' : - word = self.ocrtext[num] + try: + word = self.ocrtext[num] + except: + word = "" + sep = ' ' if handle_links: diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py index 929ce57..504105b 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py @@ -80,10 +80,12 @@ if inCalibre: from calibre_plugins.dedrm import mobidedrm from calibre_plugins.dedrm import topazextract from calibre_plugins.dedrm import kgenpids + from calibre_plugins.dedrm import android else: import mobidedrm import topazextract import kgenpids + import android # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get @@ -273,7 +275,7 @@ def decryptBook(infile, outdir, kDatabaseFiles, serials, pids): def usage(progname): print u"Removes DRM protection from Mobipocket, Amazon KF8, Amazon Print Replica and Amazon Topaz ebooks" print u"Usage:" - print u" {0} [-k ] [-p ] [-s ] ".format(progname) + print u" {0} [-k ] [-p ] [-s ] [ -a ] ".format(progname) # # Main @@ -284,7 +286,7 @@ def cli_main(): print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2013 The Dark Reverser et al.".format(__version__) try: - opts, args = getopt.getopt(argv[1:], "k:p:s:") + opts, args = getopt.getopt(argv[1:], "k:p:s:a:") except getopt.GetoptError, err: print u"Error in options or arguments: {0}".format(err.args[0]) usage(progname) @@ -312,6 +314,11 @@ def cli_main(): if a == None : raise DrmException("Invalid parameter for -s") serials = a.split(',') + if o == '-a': + if a == None: + continue + serials.extend(android.get_serials(a)) + serials.extend(android.get_serials()) # try with built in Kindle Info files if not on Linux k4 = not sys.platform.startswith('linux') diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kindlekey.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kindlekey.py index f58e973..8852769 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kindlekey.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kindlekey.py @@ -19,6 +19,7 @@ from __future__ import with_statement # 1.6 - Fixed a problem getting the disk serial numbers # 1.7 - Work if TkInter is missing # 1.8 - Fixes for Kindle for Mac, and non-ascii in Windows user names +# 1.9 - Fixes for Unicode in Windows user names """ @@ -26,7 +27,7 @@ Retrieve Kindle for PC/Mac user key. """ __license__ = 'GPL v3' -__version__ = '1.8' +__version__ = '1.9' import sys, os, re from struct import pack, unpack, unpack_from @@ -907,18 +908,34 @@ if iswindows: return CryptUnprotectData CryptUnprotectData = CryptUnprotectData() + # Returns Environmental Variables that contain unicode + def getEnvironmentVariable(name): + import ctypes + name = unicode(name) # make sure string argument is unicode + n = ctypes.windll.kernel32.GetEnvironmentVariableW(name, None, 0) + if n == 0: + return None + buf = ctypes.create_unicode_buffer(u'\0'*n) + ctypes.windll.kernel32.GetEnvironmentVariableW(name, buf, n) + return buf.value # Locate all of the kindle-info style files and return as list def getKindleInfoFiles(): kInfoFiles = [] # some 64 bit machines do not have the proper registry key for some reason - # or the pythonn interface to the 32 vs 64 bit registry is broken + # or the python interface to the 32 vs 64 bit registry is broken path = "" if 'LOCALAPPDATA' in os.environ.keys(): - path = os.environ['LOCALAPPDATA'] + # Python 2.x does not return unicode env. Use Python 3.x + path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%") + # this is just another alternative. + # path = getEnvironmentVariable('LOCALAPPDATA') + if not os.path.isdir(path): + path = "" else: # User Shell Folders show take precedent over Shell Folders if present try: + # this will still break regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\") path = winreg.QueryValueEx(regkey, 'Local AppData')[0] if not os.path.isdir(path): @@ -937,13 +954,14 @@ if iswindows: if path == "": print ('Could not find the folder in which to look for kinfoFiles.') else: - print('searching for kinfoFiles in ' + path) + # Probably not the best. To Fix (shouldn't ignore in encoding) or use utf-8 + print(u'searching for kinfoFiles in ' + path.encode('ascii', 'ignore')) # look for (K4PC 1.9.0 and later) .kinf2011 file kinfopath = path +'\\Amazon\\Kindle\\storage\\.kinf2011' if os.path.isfile(kinfopath): found = True - print('Found K4PC 1.9+ kinf2011 file: ' + kinfopath) + print('Found K4PC 1.9+ kinf2011 file: ' + kinfopath.encode('ascii','ignore')) kInfoFiles.append(kinfopath) # look for (K4PC 1.6.0 and later) rainier.2.1.1.kinf file @@ -1142,7 +1160,7 @@ if iswindows: cleartext = CryptUnprotectData(encryptedValue, entropy, 1) DB[keyname] = cleartext - if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB: + if 'kindle.account.tokens' in DB: print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(GetIDString(), GetUserName().decode("latin-1")) # store values used in decryption DB['IDString'] = GetIDString() @@ -1758,7 +1776,7 @@ elif isosx: break except: pass - if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB: + if 'kindle.account.tokens' in DB: # store values used in decryption print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString, GetUserName()) DB['IDString'] = IDString diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py index 7b69edc..89cc695 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py @@ -156,6 +156,8 @@ def PC1(key, src, decryption=True): return Pukall_Cipher().PC1(key,src,decryption) except NameError: pass + except TypeError: + pass # use slow python version, since Pukall_Cipher didn't load sum1 = 0; diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/stylexml2css.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/stylexml2css.py index c111850..daa108a 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/stylexml2css.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/stylexml2css.py @@ -178,7 +178,12 @@ class DocParser(object): if val == "": val = 0 - if not ((attr == 'hang') and (int(val) == 0)) : + if not ((attr == 'hang') and (int(val) == 0)): + try: + f = float(val) + except: + print "Warning: unrecognised val, ignoring" + val = 0 pv = float(val)/scale cssargs[attr] = (self.attr_val_map[attr], pv) keep = True diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/topazextract.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/topazextract.py index 97f6583..fb5eb7a 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/topazextract.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/topazextract.py @@ -356,7 +356,7 @@ class TopazBook: self.setBookKey(bookKey) self.createBookDirectory() - self.extractFiles() + self.extractFiles() print u"Successfully Extracted Topaz contents" if inCalibre: from calibre_plugins.dedrm import genbook diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/DeDRM_app.pyw b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/DeDRM_app.pyw index e73226b..7225b6d 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/DeDRM_app.pyw +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/DeDRM_app.pyw @@ -12,7 +12,7 @@ # 6.0.4 - Fix for other potential unicode problems # 6.0.5 - Fix typo -__version__ = '6.0.7' +__version__ = '6.0.8' import sys import os, os.path diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/__init__.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/__init__.py index caed6e8..37d4cb1 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/__init__.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/__init__.py @@ -31,14 +31,17 @@ __docformat__ = 'restructuredtext en' # 6.0.3 - Fixes for Kindle for Mac and Windows non-ascii user names # 6.0.4 - Fixes for stand-alone scripts and applications # and pdb files in plugin and initial conversion of prefs. +# 6.0.5 - Fix a key issue # 6.0.6 - Fix up an incorrect function call +# 6.0.7 - Error handling for incomplete PDF metadata +# 6.0.8 - Fixes a Wine key issue and topaz support """ Decrypt DRMed ebooks. """ PLUGIN_NAME = u"DeDRM" -PLUGIN_VERSION_TUPLE = (6, 0, 7) +PLUGIN_VERSION_TUPLE = (6, 0, 8) PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE]) # Include an html helpfile in the plugin's zipfile with the following name. RESOURCE_NAME = PLUGIN_NAME + '_Help.htm' @@ -313,7 +316,7 @@ class DeDRM(FileTypePlugin): from wineutils import WineGetKeys scriptpath = os.path.join(self.alfdir,u"adobekey.py") - defaultkeys = self.WineGetKeys(scriptpath, u".der",dedrmprefs['adobewineprefix']) + defaultkeys = WineGetKeys(scriptpath, u".der",dedrmprefs['adobewineprefix']) except: pass @@ -391,7 +394,7 @@ class DeDRM(FileTypePlugin): from wineutils import WineGetKeys scriptpath = os.path.join(self.alfdir,u"kindlekey.py") - defaultkeys = self.WineGetKeys(scriptpath, u".k4i",dedrmprefs['kindlewineprefix']) + defaultkeys = WineGetKeys(scriptpath, u".k4i",dedrmprefs['kindlewineprefix']) except: pass diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/android.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/android.py new file mode 100644 index 0000000..ddb94f5 --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/android.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python +#fileencoding: utf-8 + +import os +import sys +import zlib +import tarfile +from hashlib import md5 +from cStringIO import StringIO +from binascii import a2b_hex, b2a_hex + +STORAGE = 'AmazonSecureStorage.xml' + +class AndroidObfuscation(object): + '''AndroidObfuscation + For the key, it's written in java, and run in android dalvikvm + ''' + + key = a2b_hex('0176e04c9408b1702d90be333fd53523') + + def encrypt(self, plaintext): + cipher = self._get_cipher() + padding = len(self.key) - len(plaintext) % len(self.key) + plaintext += chr(padding) * padding + return b2a_hex(cipher.encrypt(plaintext)) + + def decrypt(self, ciphertext): + cipher = self._get_cipher() + plaintext = cipher.decrypt(a2b_hex(ciphertext)) + return plaintext[:-ord(plaintext[-1])] + + def _get_cipher(self): + try: + from Crypto.Cipher import AES + return AES.new(self.key) + except ImportError: + from aescbc import AES, noPadding + return AES(self.key, padding=noPadding()) + +class AndroidObfuscationV2(AndroidObfuscation): + '''AndroidObfuscationV2 + ''' + + count = 503 + password = 'Thomsun was here!' + + def __init__(self, salt): + key = self.password + salt + for _ in range(self.count): + key = md5(key).digest() + self.key = key[:8] + self.iv = key[8:16] + + def _get_cipher(self): + try : + from Crypto.Cipher import DES + return DES.new(self.key, DES.MODE_CBC, self.iv) + except ImportError: + from python_des import Des, CBC + return Des(self.key, CBC, self.iv) + +def parse_preference(path): + ''' parse android's shared preference xml ''' + storage = {} + read = open(path) + for line in read: + line = line.strip() + # value + if line.startswith(' adb backup com.amazon.kindle + ''' + output = None + read = open(path, 'rb') + head = read.read(24) + if head == 'ANDROID BACKUP\n1\n1\nnone\n': + output = StringIO(zlib.decompress(read.read())) + read.close() + + if not output: + return False + + tar = tarfile.open(fileobj=output) + for member in tar.getmembers(): + if member.name.strip().endswith(STORAGE): + write = open(STORAGE, 'w') + write.write(tar.extractfile(member).read()) + write.close() + break + + return True + +__all__ = [ 'get_storage', 'get_serials', 'parse_preference', + 'AndroidObfuscation', 'AndroidObfuscationV2', 'STORAGE'] + +if __name__ == '__main__': + print get_serials() \ No newline at end of file diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/android_readme.txt b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/android_readme.txt new file mode 100644 index 0000000..9e7d035 --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/android_readme.txt @@ -0,0 +1,6 @@ +1.1 get AmazonSecureStorage.xml from /data/data/com.amazon.kindle/shared_prefs/AmazonSecureStorage.xml + +1.2 on android 4.0+, run `adb backup com.amazon.kindle` from PC will get backup.ab + now android.py can convert backup.ab to AmazonSecureStorage.xml + +2. run `k4mobidedrm.py -a AmazonSecureStorage.xml ' diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/flatxml2html.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/flatxml2html.py index 4d83368..991591b 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/flatxml2html.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/flatxml2html.py @@ -458,7 +458,11 @@ class DocParser(object): (wtype, num) = pdesc[j] if wtype == 'ocr' : - word = self.ocrtext[num] + try: + word = self.ocrtext[num] + except: + word = "" + sep = ' ' if handle_links: diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/k4mobidedrm.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/k4mobidedrm.py index 929ce57..504105b 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/k4mobidedrm.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/k4mobidedrm.py @@ -80,10 +80,12 @@ if inCalibre: from calibre_plugins.dedrm import mobidedrm from calibre_plugins.dedrm import topazextract from calibre_plugins.dedrm import kgenpids + from calibre_plugins.dedrm import android else: import mobidedrm import topazextract import kgenpids + import android # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get @@ -273,7 +275,7 @@ def decryptBook(infile, outdir, kDatabaseFiles, serials, pids): def usage(progname): print u"Removes DRM protection from Mobipocket, Amazon KF8, Amazon Print Replica and Amazon Topaz ebooks" print u"Usage:" - print u" {0} [-k ] [-p ] [-s ] ".format(progname) + print u" {0} [-k ] [-p ] [-s ] [ -a ] ".format(progname) # # Main @@ -284,7 +286,7 @@ def cli_main(): print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2013 The Dark Reverser et al.".format(__version__) try: - opts, args = getopt.getopt(argv[1:], "k:p:s:") + opts, args = getopt.getopt(argv[1:], "k:p:s:a:") except getopt.GetoptError, err: print u"Error in options or arguments: {0}".format(err.args[0]) usage(progname) @@ -312,6 +314,11 @@ def cli_main(): if a == None : raise DrmException("Invalid parameter for -s") serials = a.split(',') + if o == '-a': + if a == None: + continue + serials.extend(android.get_serials(a)) + serials.extend(android.get_serials()) # try with built in Kindle Info files if not on Linux k4 = not sys.platform.startswith('linux') diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kindlekey.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kindlekey.py index f58e973..8852769 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kindlekey.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kindlekey.py @@ -19,6 +19,7 @@ from __future__ import with_statement # 1.6 - Fixed a problem getting the disk serial numbers # 1.7 - Work if TkInter is missing # 1.8 - Fixes for Kindle for Mac, and non-ascii in Windows user names +# 1.9 - Fixes for Unicode in Windows user names """ @@ -26,7 +27,7 @@ Retrieve Kindle for PC/Mac user key. """ __license__ = 'GPL v3' -__version__ = '1.8' +__version__ = '1.9' import sys, os, re from struct import pack, unpack, unpack_from @@ -907,18 +908,34 @@ if iswindows: return CryptUnprotectData CryptUnprotectData = CryptUnprotectData() + # Returns Environmental Variables that contain unicode + def getEnvironmentVariable(name): + import ctypes + name = unicode(name) # make sure string argument is unicode + n = ctypes.windll.kernel32.GetEnvironmentVariableW(name, None, 0) + if n == 0: + return None + buf = ctypes.create_unicode_buffer(u'\0'*n) + ctypes.windll.kernel32.GetEnvironmentVariableW(name, buf, n) + return buf.value # Locate all of the kindle-info style files and return as list def getKindleInfoFiles(): kInfoFiles = [] # some 64 bit machines do not have the proper registry key for some reason - # or the pythonn interface to the 32 vs 64 bit registry is broken + # or the python interface to the 32 vs 64 bit registry is broken path = "" if 'LOCALAPPDATA' in os.environ.keys(): - path = os.environ['LOCALAPPDATA'] + # Python 2.x does not return unicode env. Use Python 3.x + path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%") + # this is just another alternative. + # path = getEnvironmentVariable('LOCALAPPDATA') + if not os.path.isdir(path): + path = "" else: # User Shell Folders show take precedent over Shell Folders if present try: + # this will still break regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\") path = winreg.QueryValueEx(regkey, 'Local AppData')[0] if not os.path.isdir(path): @@ -937,13 +954,14 @@ if iswindows: if path == "": print ('Could not find the folder in which to look for kinfoFiles.') else: - print('searching for kinfoFiles in ' + path) + # Probably not the best. To Fix (shouldn't ignore in encoding) or use utf-8 + print(u'searching for kinfoFiles in ' + path.encode('ascii', 'ignore')) # look for (K4PC 1.9.0 and later) .kinf2011 file kinfopath = path +'\\Amazon\\Kindle\\storage\\.kinf2011' if os.path.isfile(kinfopath): found = True - print('Found K4PC 1.9+ kinf2011 file: ' + kinfopath) + print('Found K4PC 1.9+ kinf2011 file: ' + kinfopath.encode('ascii','ignore')) kInfoFiles.append(kinfopath) # look for (K4PC 1.6.0 and later) rainier.2.1.1.kinf file @@ -1142,7 +1160,7 @@ if iswindows: cleartext = CryptUnprotectData(encryptedValue, entropy, 1) DB[keyname] = cleartext - if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB: + if 'kindle.account.tokens' in DB: print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(GetIDString(), GetUserName().decode("latin-1")) # store values used in decryption DB['IDString'] = GetIDString() @@ -1758,7 +1776,7 @@ elif isosx: break except: pass - if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB: + if 'kindle.account.tokens' in DB: # store values used in decryption print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString, GetUserName()) DB['IDString'] = IDString diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/mobidedrm.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/mobidedrm.py index 7b69edc..89cc695 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/mobidedrm.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/mobidedrm.py @@ -156,6 +156,8 @@ def PC1(key, src, decryption=True): return Pukall_Cipher().PC1(key,src,decryption) except NameError: pass + except TypeError: + pass # use slow python version, since Pukall_Cipher didn't load sum1 = 0; diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/stylexml2css.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/stylexml2css.py index c111850..daa108a 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/stylexml2css.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/stylexml2css.py @@ -178,7 +178,12 @@ class DocParser(object): if val == "": val = 0 - if not ((attr == 'hang') and (int(val) == 0)) : + if not ((attr == 'hang') and (int(val) == 0)): + try: + f = float(val) + except: + print "Warning: unrecognised val, ignoring" + val = 0 pv = float(val)/scale cssargs[attr] = (self.attr_val_map[attr], pv) keep = True diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/topazextract.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/topazextract.py index 97f6583..fb5eb7a 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/topazextract.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/topazextract.py @@ -356,7 +356,7 @@ class TopazBook: self.setBookKey(bookKey) self.createBookDirectory() - self.extractFiles() + self.extractFiles() print u"Successfully Extracted Topaz contents" if inCalibre: from calibre_plugins.dedrm import genbook diff --git a/DeDRM_calibre_plugin/DeDRM_plugin.zip b/DeDRM_calibre_plugin/DeDRM_plugin.zip index 7c4878ac2a4db8fcac318e129bd2a29f211fd7a8..58d817484d8597a28a344d4cfdc375c419fe25b1 100644 GIT binary patch delta 44266 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 delta 41630 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 A^Z)<= diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/__init__.py b/DeDRM_calibre_plugin/DeDRM_plugin/__init__.py index caed6e8..37d4cb1 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/__init__.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/__init__.py @@ -31,14 +31,17 @@ __docformat__ = 'restructuredtext en' # 6.0.3 - Fixes for Kindle for Mac and Windows non-ascii user names # 6.0.4 - Fixes for stand-alone scripts and applications # and pdb files in plugin and initial conversion of prefs. +# 6.0.5 - Fix a key issue # 6.0.6 - Fix up an incorrect function call +# 6.0.7 - Error handling for incomplete PDF metadata +# 6.0.8 - Fixes a Wine key issue and topaz support """ Decrypt DRMed ebooks. """ PLUGIN_NAME = u"DeDRM" -PLUGIN_VERSION_TUPLE = (6, 0, 7) +PLUGIN_VERSION_TUPLE = (6, 0, 8) PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE]) # Include an html helpfile in the plugin's zipfile with the following name. RESOURCE_NAME = PLUGIN_NAME + '_Help.htm' @@ -313,7 +316,7 @@ class DeDRM(FileTypePlugin): from wineutils import WineGetKeys scriptpath = os.path.join(self.alfdir,u"adobekey.py") - defaultkeys = self.WineGetKeys(scriptpath, u".der",dedrmprefs['adobewineprefix']) + defaultkeys = WineGetKeys(scriptpath, u".der",dedrmprefs['adobewineprefix']) except: pass @@ -391,7 +394,7 @@ class DeDRM(FileTypePlugin): from wineutils import WineGetKeys scriptpath = os.path.join(self.alfdir,u"kindlekey.py") - defaultkeys = self.WineGetKeys(scriptpath, u".k4i",dedrmprefs['kindlewineprefix']) + defaultkeys = WineGetKeys(scriptpath, u".k4i",dedrmprefs['kindlewineprefix']) except: pass diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/android.py b/DeDRM_calibre_plugin/DeDRM_plugin/android.py new file mode 100644 index 0000000..ddb94f5 --- /dev/null +++ b/DeDRM_calibre_plugin/DeDRM_plugin/android.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python +#fileencoding: utf-8 + +import os +import sys +import zlib +import tarfile +from hashlib import md5 +from cStringIO import StringIO +from binascii import a2b_hex, b2a_hex + +STORAGE = 'AmazonSecureStorage.xml' + +class AndroidObfuscation(object): + '''AndroidObfuscation + For the key, it's written in java, and run in android dalvikvm + ''' + + key = a2b_hex('0176e04c9408b1702d90be333fd53523') + + def encrypt(self, plaintext): + cipher = self._get_cipher() + padding = len(self.key) - len(plaintext) % len(self.key) + plaintext += chr(padding) * padding + return b2a_hex(cipher.encrypt(plaintext)) + + def decrypt(self, ciphertext): + cipher = self._get_cipher() + plaintext = cipher.decrypt(a2b_hex(ciphertext)) + return plaintext[:-ord(plaintext[-1])] + + def _get_cipher(self): + try: + from Crypto.Cipher import AES + return AES.new(self.key) + except ImportError: + from aescbc import AES, noPadding + return AES(self.key, padding=noPadding()) + +class AndroidObfuscationV2(AndroidObfuscation): + '''AndroidObfuscationV2 + ''' + + count = 503 + password = 'Thomsun was here!' + + def __init__(self, salt): + key = self.password + salt + for _ in range(self.count): + key = md5(key).digest() + self.key = key[:8] + self.iv = key[8:16] + + def _get_cipher(self): + try : + from Crypto.Cipher import DES + return DES.new(self.key, DES.MODE_CBC, self.iv) + except ImportError: + from python_des import Des, CBC + return Des(self.key, CBC, self.iv) + +def parse_preference(path): + ''' parse android's shared preference xml ''' + storage = {} + read = open(path) + for line in read: + line = line.strip() + # value + if line.startswith(' adb backup com.amazon.kindle + ''' + output = None + read = open(path, 'rb') + head = read.read(24) + if head == 'ANDROID BACKUP\n1\n1\nnone\n': + output = StringIO(zlib.decompress(read.read())) + read.close() + + if not output: + return False + + tar = tarfile.open(fileobj=output) + for member in tar.getmembers(): + if member.name.strip().endswith(STORAGE): + write = open(STORAGE, 'w') + write.write(tar.extractfile(member).read()) + write.close() + break + + return True + +__all__ = [ 'get_storage', 'get_serials', 'parse_preference', + 'AndroidObfuscation', 'AndroidObfuscationV2', 'STORAGE'] + +if __name__ == '__main__': + print get_serials() \ No newline at end of file diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/android_readme.txt b/DeDRM_calibre_plugin/DeDRM_plugin/android_readme.txt new file mode 100644 index 0000000..9e7d035 --- /dev/null +++ b/DeDRM_calibre_plugin/DeDRM_plugin/android_readme.txt @@ -0,0 +1,6 @@ +1.1 get AmazonSecureStorage.xml from /data/data/com.amazon.kindle/shared_prefs/AmazonSecureStorage.xml + +1.2 on android 4.0+, run `adb backup com.amazon.kindle` from PC will get backup.ab + now android.py can convert backup.ab to AmazonSecureStorage.xml + +2. run `k4mobidedrm.py -a AmazonSecureStorage.xml ' diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/dialogs.py b/DeDRM_calibre_plugin/DeDRM_plugin/dialogs.py deleted file mode 100644 index 6bb8c37..0000000 --- a/DeDRM_calibre_plugin/DeDRM_plugin/dialogs.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# base64.py, version 1.0 -# Copyright © 2010 Apprentice Alf - -# Released under the terms of the GNU General Public Licence, version 3 or -# later. - -# Revision history: -# 1 - Initial release. To allow Applescript to do base64 encoding - -""" -Provide base64 encoding. -""" - -from __future__ import with_statement - -__license__ = 'GPL v3' - -import sys -import os -import base64 - -def usage(progname): - print "Applies base64 encoding to the supplied file, sending to standard output" - print "Usage:" - print " %s " % progname - -def cli_main(argv=sys.argv): - progname = os.path.basename(argv[0]) - - if len(argv)<2: - usage(progname) - sys.exit(2) - - keypath = argv[1] - with open(keypath, 'rb') as f: - keyder = f.read() - print keyder.encode('base64') - return 0 - - -if __name__ == '__main__': - sys.exit(cli_main()) diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/encodebase64.py b/DeDRM_calibre_plugin/DeDRM_plugin/encodebase64.py index 11f1427..6bb8c37 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/encodebase64.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/encodebase64.py @@ -1,208 +1,45 @@ -#!/usr/bin/python -# -# This is a python script. You need a Python interpreter to run it. -# For example, ActiveState Python, which exists for windows. -# -# Changelog drmcheck -# 1.00 - Initial version, with code from various other scripts -# 1.01 - Moved authorship announcement to usage section. -# -# Changelog epubtest -# 1.00 - Cut to epubtest.py, testing ePub files only by Apprentice Alf -# 1.01 - Added routine for use by Windows DeDRM -# -# Written in 2011 by Paul Durrant -# Released with unlicense. See http://unlicense.org/ -# -############################################################################# -# -# This is free and unencumbered software released into the public domain. -# -# Anyone is free to copy, modify, publish, use, compile, sell, or -# distribute this software, either in source code form or as a compiled -# binary, for any purpose, commercial or non-commercial, and by any -# means. -# -# In jurisdictions that recognize copyright laws, the author or authors -# of this software dedicate any and all copyright interest in the -# software to the public domain. We make this dedication for the benefit -# of the public at large and to the detriment of our heirs and -# successors. We intend this dedication to be an overt act of -# relinquishment in perpetuity of all present and future rights to this -# software under copyright law. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR -# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -############################################################################# -# -# It's still polite to give attribution if you do reuse this code. -# +#!/usr/bin/env python +# -*- coding: utf-8 -*- -from __future__ import with_statement - -__version__ = '1.01' - -import sys, struct, os -import zlib -import zipfile -import xml.etree.ElementTree as etree - -NSMAP = {'adept': 'http://ns.adobe.com/adept', - 'enc': 'http://www.w3.org/2001/04/xmlenc#'} - -# Wrap a stream so that output gets flushed immediately -# and also make sure that any unicode strings get -# encoded using "replace" before writing them. -class SafeUnbuffered: - def __init__(self, stream): - self.stream = stream - self.encoding = stream.encoding - if self.encoding == None: - self.encoding = "utf-8" - def write(self, data): - if isinstance(data,unicode): - data = data.encode(self.encoding,"replace") - self.stream.write(data) - self.stream.flush() - def __getattr__(self, attr): - return getattr(self.stream, attr) - -try: - from calibre.constants import iswindows, isosx -except: - iswindows = sys.platform.startswith('win') - isosx = sys.platform.startswith('darwin') - -def unicode_argv(): - if iswindows: - # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode - # strings. - - # Versions 2.x of Python don't support Unicode in sys.argv on - # Windows, with the underlying Windows API instead replacing multi-byte - # characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv - # as a list of Unicode strings and encode them as utf-8 +# base64.py, version 1.0 +# Copyright © 2010 Apprentice Alf - from ctypes import POINTER, byref, cdll, c_int, windll - from ctypes.wintypes import LPCWSTR, LPWSTR +# Released under the terms of the GNU General Public Licence, version 3 or +# later. - GetCommandLineW = cdll.kernel32.GetCommandLineW - GetCommandLineW.argtypes = [] - GetCommandLineW.restype = LPCWSTR +# Revision history: +# 1 - Initial release. To allow Applescript to do base64 encoding - CommandLineToArgvW = windll.shell32.CommandLineToArgvW - CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] - CommandLineToArgvW.restype = POINTER(LPWSTR) +""" +Provide base64 encoding. +""" - cmd = GetCommandLineW() - argc = c_int(0) - argv = CommandLineToArgvW(cmd, byref(argc)) - if argc.value > 0: - # Remove Python executable and commands if present - start = argc.value - len(sys.argv) - return [argv[i] for i in - xrange(start, argc.value)] - # if we don't have any arguments at all, just pass back script name - # this should never happen - return [u"epubtest.py"] - else: - argvencoding = sys.stdin.encoding - if argvencoding == None: - argvencoding = "utf-8" - return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv] - -_FILENAME_LEN_OFFSET = 26 -_EXTRA_LEN_OFFSET = 28 -_FILENAME_OFFSET = 30 -_MAX_SIZE = 64 * 1024 - - -def uncompress(cmpdata): - dc = zlib.decompressobj(-15) - data = '' - while len(cmpdata) > 0: - if len(cmpdata) > _MAX_SIZE : - newdata = cmpdata[0:_MAX_SIZE] - cmpdata = cmpdata[_MAX_SIZE:] - else: - newdata = cmpdata - cmpdata = '' - newdata = dc.decompress(newdata) - unprocessed = dc.unconsumed_tail - if len(unprocessed) == 0: - newdata += dc.flush() - data += newdata - cmpdata += unprocessed - unprocessed = '' - return data - -def getfiledata(file, zi): - # get file name length and exta data length to find start of file data - local_header_offset = zi.header_offset - - file.seek(local_header_offset + _FILENAME_LEN_OFFSET) - leninfo = file.read(2) - local_name_length, = struct.unpack('" % progname - return data +def cli_main(argv=sys.argv): + progname = os.path.basename(argv[0]) -def encryption(infile): - # returns encryption: one of Unencrypted, Adobe, B&N and Unknown - encryption = "Unknown" - try: - with open(infile,'rb') as infileobject: - bookdata = infileobject.read(58) - # Check for Zip - if bookdata[0:0+2] == "PK": - foundrights = False - foundencryption = False - inzip = zipfile.ZipFile(infile,'r') - namelist = set(inzip.namelist()) - if 'META-INF/rights.xml' not in namelist or 'META-INF/encryption.xml' not in namelist: - encryption = "Unencrypted" - else: - rights = etree.fromstring(inzip.read('META-INF/rights.xml')) - adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) - expr = './/%s' % (adept('encryptedKey'),) - bookkey = ''.join(rights.findtext(expr)) - if len(bookkey) == 172: - encryption = "Adobe" - elif len(bookkey) == 64: - encryption = "B&N" - else: - encryption = "Unknown" - except: - traceback.print_exc() - return encryption + if len(argv)<2: + usage(progname) + sys.exit(2) -def main(): - argv=unicode_argv() - print encryption(argv[1]) + keypath = argv[1] + with open(keypath, 'rb') as f: + keyder = f.read() + print keyder.encode('base64') return 0 -if __name__ == "__main__": - sys.stdout=SafeUnbuffered(sys.stdout) - sys.stderr=SafeUnbuffered(sys.stderr) - sys.exit(main()) + +if __name__ == '__main__': + sys.exit(cli_main()) diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/epubtest.py b/DeDRM_calibre_plugin/DeDRM_plugin/epubtest.py index 1dfef42..11f1427 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/epubtest.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/epubtest.py @@ -1,82 +1,60 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# erdr2pml.py -# Copyright © 2008 The Dark Reverser +#!/usr/bin/python # -# Modified 2008–2012 by some_updates, DiapDealer and Apprentice Alf - # This is a python script. You need a Python interpreter to run it. # For example, ActiveState Python, which exists for windows. -# Changelog # -# Based on ereader2html version 0.08 plus some later small fixes +# Changelog drmcheck +# 1.00 - Initial version, with code from various other scripts +# 1.01 - Moved authorship announcement to usage section. +# +# Changelog epubtest +# 1.00 - Cut to epubtest.py, testing ePub files only by Apprentice Alf +# 1.01 - Added routine for use by Windows DeDRM +# +# Written in 2011 by Paul Durrant +# Released with unlicense. See http://unlicense.org/ +# +############################################################################# +# +# This is free and unencumbered software released into the public domain. +# +# Anyone is free to copy, modify, publish, use, compile, sell, or +# distribute this software, either in source code form or as a compiled +# binary, for any purpose, commercial or non-commercial, and by any +# means. +# +# In jurisdictions that recognize copyright laws, the author or authors +# of this software dedicate any and all copyright interest in the +# software to the public domain. We make this dedication for the benefit +# of the public at large and to the detriment of our heirs and +# successors. We intend this dedication to be an overt act of +# relinquishment in perpetuity of all present and future rights to this +# software under copyright law. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +############################################################################# +# +# It's still polite to give attribution if you do reuse this code. # -# 0.01 - Initial version -# 0.02 - Support more eReader files. Support bold text and links. Fix PML decoder parsing bug. -# 0.03 - Fix incorrect variable usage at one place. -# 0.03b - enhancement by DeBockle (version 259 support) -# Custom version 0.03 - no change to eReader support, only usability changes -# - start of pep-8 indentation (spaces not tab), fix trailing blanks -# - version variable, only one place to change -# - added main routine, now callable as a library/module, -# means tools can add optional support for ereader2html -# - outdir is no longer a mandatory parameter (defaults based on input name if missing) -# - time taken output to stdout -# - Psyco support - reduces runtime by a factor of (over) 3! -# E.g. (~600Kb file) 90 secs down to 24 secs -# - newstyle classes -# - changed map call to list comprehension -# may not work with python 2.3 -# without Psyco this reduces runtime to 90% -# E.g. 90 secs down to 77 secs -# Psyco with map calls takes longer, do not run with map in Psyco JIT! -# - izip calls used instead of zip (if available), further reduction -# in run time (factor of 4.5). -# E.g. (~600Kb file) 90 secs down to 20 secs -# - Python 2.6+ support, avoid DeprecationWarning with sha/sha1 -# 0.04 - Footnote support, PML output, correct charset in html, support more PML tags -# - Feature change, dump out PML file -# - Added supprt for footnote tags. NOTE footnote ids appear to be bad (not usable) -# in some pdb files :-( due to the same id being used multiple times -# - Added correct charset encoding (pml is based on cp1252) -# - Added logging support. -# 0.05 - Improved type 272 support for sidebars, links, chapters, metainfo, etc -# 0.06 - Merge of 0.04 and 0.05. Improved HTML output -# Placed images in subfolder, so that it's possible to just -# drop the book.pml file onto DropBook to make an unencrypted -# copy of the eReader file. -# Using that with Calibre works a lot better than the HTML -# conversion in this code. -# 0.07 - Further Improved type 272 support for sidebars with all earlier fixes -# 0.08 - fixed typos, removed extraneous things -# 0.09 - fixed typos in first_pages to first_page to again support older formats -# 0.10 - minor cleanups -# 0.11 - fixups for using correct xml for footnotes and sidebars for use with Dropbook -# 0.12 - Fix added to prevent lowercasing of image names when the pml code itself uses a different case in the link name. -# 0.13 - change to unbuffered stdout for use with gui front ends -# 0.14 - contributed enhancement to support --make-pmlz switch -# 0.15 - enabled high-ascii to pml character encoding. DropBook now works on Mac. -# 0.16 - convert to use openssl DES (very very fast) or pure python DES if openssl's libcrypto is not available -# 0.17 - added support for pycrypto's DES as well -# 0.18 - on Windows try PyCrypto first and OpenSSL next -# 0.19 - Modify the interface to allow use of import -# 0.20 - modify to allow use inside new interface for calibre plugins -# 0.21 - Support eReader (drm) version 11. -# - Don't reject dictionary format. -# - Ignore sidebars for dictionaries (different format?) -# 0.22 - Unicode and plugin support, different image folders for PMLZ and source -# 0.23 - moved unicode_argv call inside main for Windows DeDRM compatibility -__version__='0.23' +from __future__ import with_statement -import sys, re -import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile, traceback +__version__ = '1.01' -if 'calibre' in sys.modules: - inCalibre = True -else: - inCalibre = False +import sys, struct, os +import zlib +import zipfile +import xml.etree.ElementTree as etree + +NSMAP = {'adept': 'http://ns.adobe.com/adept', + 'enc': 'http://www.w3.org/2001/04/xmlenc#'} # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get @@ -95,8 +73,11 @@ class SafeUnbuffered: def __getattr__(self, attr): return getattr(self.stream, attr) -iswindows = sys.platform.startswith('win') -isosx = sys.platform.startswith('darwin') +try: + from calibre.constants import iswindows, isosx +except: + iswindows = sys.platform.startswith('win') + isosx = sys.platform.startswith('darwin') def unicode_argv(): if iswindows: @@ -105,8 +86,8 @@ def unicode_argv(): # Versions 2.x of Python don't support Unicode in sys.argv on # Windows, with the underlying Windows API instead replacing multi-byte - # characters with '?'. - + # characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv + # as a list of Unicode strings and encode them as utf-8 from ctypes import POINTER, byref, cdll, c_int, windll from ctypes.wintypes import LPCWSTR, LPWSTR @@ -129,469 +110,99 @@ def unicode_argv(): xrange(start, argc.value)] # if we don't have any arguments at all, just pass back script name # this should never happen - return [u"mobidedrm.py"] + return [u"epubtest.py"] else: argvencoding = sys.stdin.encoding if argvencoding == None: argvencoding = "utf-8" return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv] -Des = None -if iswindows: - # first try with pycrypto - if inCalibre: - from calibre_plugins.dedrm import pycrypto_des - else: - import pycrypto_des - Des = pycrypto_des.load_pycrypto() - if Des == None: - # they try with openssl - if inCalibre: - from calibre_plugins.dedrm import openssl_des - else: - import openssl_des - Des = openssl_des.load_libcrypto() -else: - # first try with openssl - if inCalibre: - from calibre_plugins.dedrm import openssl_des - else: - import openssl_des - Des = openssl_des.load_libcrypto() - if Des == None: - # then try with pycrypto - if inCalibre: - from calibre_plugins.dedrm import pycrypto_des - else: - import pycrypto_des - Des = pycrypto_des.load_pycrypto() - -# if that did not work then use pure python implementation -# of DES and try to speed it up with Psycho -if Des == None: - if inCalibre: - from calibre_plugins.dedrm import python_des - else: - import python_des - Des = python_des.Des - # Import Psyco if available - try: - # http://psyco.sourceforge.net - import psyco - psyco.full() - except ImportError: - pass - -try: - from hashlib import sha1 -except ImportError: - # older Python release - import sha - sha1 = lambda s: sha.new(s) - -import cgi -import logging - -logging.basicConfig() -#logging.basicConfig(level=logging.DEBUG) - - -class Sectionizer(object): - bkType = "Book" - - def __init__(self, filename, ident): - self.contents = file(filename, 'rb').read() - self.header = self.contents[0:72] - self.num_sections, = struct.unpack('>H', self.contents[76:78]) - # Dictionary or normal content (TODO: Not hard-coded) - if self.header[0x3C:0x3C+8] != ident: - if self.header[0x3C:0x3C+8] == "PDctPPrs": - self.bkType = "Dict" - else: - raise ValueError('Invalid file format') - self.sections = [] - for i in xrange(self.num_sections): - offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', self.contents[78+i*8:78+i*8+8]) - flags, val = a1, a2<<16|a3<<8|a4 - self.sections.append( (offset, flags, val) ) - def loadSection(self, section): - if section + 1 == self.num_sections: - end_off = len(self.contents) - else: - end_off = self.sections[section + 1][0] - off = self.sections[section][0] - return self.contents[off:end_off] - -# cleanup unicode filenames -# borrowed from calibre from calibre/src/calibre/__init__.py -# added in removal of control (<32) chars -# and removal of . at start and end -# and with some (heavily edited) code from Paul Durrant's kindlenamer.py -def sanitizeFileName(name): - # substitute filename unfriendly characters - name = name.replace(u"<",u"[").replace(u">",u"]").replace(u" : ",u" – ").replace(u": ",u" – ").replace(u":",u"—").replace(u"/",u"_").replace(u"\\",u"_").replace(u"|",u"_").replace(u"\"",u"\'") - # delete control characters - name = u"".join(char for char in name if ord(char)>=32) - # white space to single space, delete leading and trailing while space - name = re.sub(ur"\s", u" ", name).strip() - # remove leading dots - while len(name)>0 and name[0] == u".": - name = name[1:] - # remove trailing dots (Windows doesn't like them) - if name.endswith(u'.'): - name = name[:-1] - return name - -def fixKey(key): - def fixByte(b): - return b ^ ((b ^ (b<<1) ^ (b<<2) ^ (b<<3) ^ (b<<4) ^ (b<<5) ^ (b<<6) ^ (b<<7) ^ 0x80) & 0x80) - return "".join([chr(fixByte(ord(a))) for a in key]) - -def deXOR(text, sp, table): - r='' - j = sp - for i in xrange(len(text)): - r += chr(ord(table[j]) ^ ord(text[i])) - j = j + 1 - if j == len(table): - j = 0 - return r - -class EreaderProcessor(object): - def __init__(self, sect, user_key): - self.section_reader = sect.loadSection - data = self.section_reader(0) - version, = struct.unpack('>H', data[0:2]) - self.version = version - logging.info('eReader file format version %s', version) - if version != 272 and version != 260 and version != 259: - raise ValueError('incorrect eReader version %d (error 1)' % version) - data = self.section_reader(1) - self.data = data - des = Des(fixKey(data[0:8])) - cookie_shuf, cookie_size = struct.unpack('>LL', des.decrypt(data[-8:])) - if cookie_shuf < 3 or cookie_shuf > 0x14 or cookie_size < 0xf0 or cookie_size > 0x200: - raise ValueError('incorrect eReader version (error 2)') - input = des.decrypt(data[-cookie_size:]) - def unshuff(data, shuf): - r = [''] * len(data) - j = 0 - for i in xrange(len(data)): - j = (j + shuf) % len(data) - r[j] = data[i] - assert len("".join(r)) == len(data) - return "".join(r) - r = unshuff(input[0:-8], cookie_shuf) +_FILENAME_LEN_OFFSET = 26 +_EXTRA_LEN_OFFSET = 28 +_FILENAME_OFFSET = 30 +_MAX_SIZE = 64 * 1024 - drm_sub_version = struct.unpack('>H', r[0:2])[0] - self.num_text_pages = struct.unpack('>H', r[2:4])[0] - 1 - self.num_image_pages = struct.unpack('>H', r[26:26+2])[0] - self.first_image_page = struct.unpack('>H', r[24:24+2])[0] - # Default values - self.num_footnote_pages = 0 - self.num_sidebar_pages = 0 - self.first_footnote_page = -1 - self.first_sidebar_page = -1 - if self.version == 272: - self.num_footnote_pages = struct.unpack('>H', r[46:46+2])[0] - self.first_footnote_page = struct.unpack('>H', r[44:44+2])[0] - if (sect.bkType == "Book"): - self.num_sidebar_pages = struct.unpack('>H', r[38:38+2])[0] - self.first_sidebar_page = struct.unpack('>H', r[36:36+2])[0] - # self.num_bookinfo_pages = struct.unpack('>H', r[34:34+2])[0] - # self.first_bookinfo_page = struct.unpack('>H', r[32:32+2])[0] - # self.num_chapter_pages = struct.unpack('>H', r[22:22+2])[0] - # self.first_chapter_page = struct.unpack('>H', r[20:20+2])[0] - # self.num_link_pages = struct.unpack('>H', r[30:30+2])[0] - # self.first_link_page = struct.unpack('>H', r[28:28+2])[0] - # self.num_xtextsize_pages = struct.unpack('>H', r[54:54+2])[0] - # self.first_xtextsize_page = struct.unpack('>H', r[52:52+2])[0] - # **before** data record 1 was decrypted and unshuffled, it contained data - # to create an XOR table and which is used to fix footnote record 0, link records, chapter records, etc - self.xortable_offset = struct.unpack('>H', r[40:40+2])[0] - self.xortable_size = struct.unpack('>H', r[42:42+2])[0] - self.xortable = self.data[self.xortable_offset:self.xortable_offset + self.xortable_size] +def uncompress(cmpdata): + dc = zlib.decompressobj(-15) + data = '' + while len(cmpdata) > 0: + if len(cmpdata) > _MAX_SIZE : + newdata = cmpdata[0:_MAX_SIZE] + cmpdata = cmpdata[_MAX_SIZE:] else: - # Nothing needs to be done - pass - # self.num_bookinfo_pages = 0 - # self.num_chapter_pages = 0 - # self.num_link_pages = 0 - # self.num_xtextsize_pages = 0 - # self.first_bookinfo_page = -1 - # self.first_chapter_page = -1 - # self.first_link_page = -1 - # self.first_xtextsize_page = -1 - - logging.debug('self.num_text_pages %d', self.num_text_pages) - logging.debug('self.num_footnote_pages %d, self.first_footnote_page %d', self.num_footnote_pages , self.first_footnote_page) - logging.debug('self.num_sidebar_pages %d, self.first_sidebar_page %d', self.num_sidebar_pages , self.first_sidebar_page) - self.flags = struct.unpack('>L', r[4:8])[0] - reqd_flags = (1<<9) | (1<<7) | (1<<10) - if (self.flags & reqd_flags) != reqd_flags: - print "Flags: 0x%X" % self.flags - raise ValueError('incompatible eReader file') - des = Des(fixKey(user_key)) - if version == 259: - if drm_sub_version != 7: - raise ValueError('incorrect eReader version %d (error 3)' % drm_sub_version) - encrypted_key_sha = r[44:44+20] - encrypted_key = r[64:64+8] - elif version == 260: - if drm_sub_version != 13 and drm_sub_version != 11: - raise ValueError('incorrect eReader version %d (error 3)' % drm_sub_version) - if drm_sub_version == 13: - encrypted_key = r[44:44+8] - encrypted_key_sha = r[52:52+20] - else: - encrypted_key = r[64:64+8] - encrypted_key_sha = r[44:44+20] - elif version == 272: - encrypted_key = r[172:172+8] - encrypted_key_sha = r[56:56+20] - self.content_key = des.decrypt(encrypted_key) - if sha1(self.content_key).digest() != encrypted_key_sha: - raise ValueError('Incorrect Name and/or Credit Card') - - def getNumImages(self): - return self.num_image_pages - - def getImage(self, i): - sect = self.section_reader(self.first_image_page + i) - name = sect[4:4+32].strip('\0') - data = sect[62:] - return sanitizeFileName(unicode(name,'windows-1252')), data - - - # def getChapterNamePMLOffsetData(self): - # cv = '' - # if self.num_chapter_pages > 0: - # for i in xrange(self.num_chapter_pages): - # chaps = self.section_reader(self.first_chapter_page + i) - # j = i % self.xortable_size - # offname = deXOR(chaps, j, self.xortable) - # offset = struct.unpack('>L', offname[0:4])[0] - # name = offname[4:].strip('\0') - # cv += '%d|%s\n' % (offset, name) - # return cv - - # def getLinkNamePMLOffsetData(self): - # lv = '' - # if self.num_link_pages > 0: - # for i in xrange(self.num_link_pages): - # links = self.section_reader(self.first_link_page + i) - # j = i % self.xortable_size - # offname = deXOR(links, j, self.xortable) - # offset = struct.unpack('>L', offname[0:4])[0] - # name = offname[4:].strip('\0') - # lv += '%d|%s\n' % (offset, name) - # return lv - - # def getExpandedTextSizesData(self): - # ts = '' - # if self.num_xtextsize_pages > 0: - # tsize = deXOR(self.section_reader(self.first_xtextsize_page), 0, self.xortable) - # for i in xrange(self.num_text_pages): - # xsize = struct.unpack('>H', tsize[0:2])[0] - # ts += "%d\n" % xsize - # tsize = tsize[2:] - # return ts - - # def getBookInfo(self): - # bkinfo = '' - # if self.num_bookinfo_pages > 0: - # info = self.section_reader(self.first_bookinfo_page) - # bkinfo = deXOR(info, 0, self.xortable) - # bkinfo = bkinfo.replace('\0','|') - # bkinfo += '\n' - # return bkinfo - - def getText(self): - des = Des(fixKey(self.content_key)) - r = '' - for i in xrange(self.num_text_pages): - logging.debug('get page %d', i) - r += zlib.decompress(des.decrypt(self.section_reader(1 + i))) - - # now handle footnotes pages - if self.num_footnote_pages > 0: - r += '\n' - # the record 0 of the footnote section must pass through the Xor Table to make it useful - sect = self.section_reader(self.first_footnote_page) - fnote_ids = deXOR(sect, 0, self.xortable) - # the remaining records of the footnote sections need to be decoded with the content_key and zlib inflated - des = Des(fixKey(self.content_key)) - for i in xrange(1,self.num_footnote_pages): - logging.debug('get footnotepage %d', i) - id_len = ord(fnote_ids[2]) - id = fnote_ids[3:3+id_len] - fmarker = '\n' % id - fmarker += zlib.decompress(des.decrypt(self.section_reader(self.first_footnote_page + i))) - fmarker += '\n\n' - r += fmarker - fnote_ids = fnote_ids[id_len+4:] - - # TODO: Handle dictionary index (?) pages - which are also marked as - # sidebar_pages (?). For now dictionary sidebars are ignored - # For dictionaries - record 0 is null terminated strings, followed by - # blocks of around 62000 bytes and a final block. Not sure of the - # encoding - - # now handle sidebar pages - if self.num_sidebar_pages > 0: - r += '\n' - # the record 0 of the sidebar section must pass through the Xor Table to make it useful - sect = self.section_reader(self.first_sidebar_page) - sbar_ids = deXOR(sect, 0, self.xortable) - # the remaining records of the sidebar sections need to be decoded with the content_key and zlib inflated - des = Des(fixKey(self.content_key)) - for i in xrange(1,self.num_sidebar_pages): - id_len = ord(sbar_ids[2]) - id = sbar_ids[3:3+id_len] - smarker = '\n' % id - smarker += zlib.decompress(des.decrypt(self.section_reader(self.first_sidebar_page + i))) - smarker += '\n\n' - r += smarker - sbar_ids = sbar_ids[id_len+4:] - - return r - -def cleanPML(pml): - # Convert special characters to proper PML code. High ASCII start at (\x80, \a128) and go up to (\xff, \a255) - pml2 = pml - for k in xrange(128,256): - badChar = chr(k) - pml2 = pml2.replace(badChar, '\\a%03d' % k) - return pml2 - -def decryptBook(infile, outpath, make_pmlz, user_key): - bookname = os.path.splitext(os.path.basename(infile))[0] - if make_pmlz: - # outpath is actually pmlz name - pmlzname = outpath - outdir = tempfile.mkdtemp() - imagedirpath = os.path.join(outdir,u"images") - else: - pmlzname = None - outdir = outpath - imagedirpath = os.path.join(outdir,bookname + u"_img") - + newdata = cmpdata + cmpdata = '' + newdata = dc.decompress(newdata) + unprocessed = dc.unconsumed_tail + if len(unprocessed) == 0: + newdata += dc.flush() + data += newdata + cmpdata += unprocessed + unprocessed = '' + return data + +def getfiledata(file, zi): + # get file name length and exta data length to find start of file data + local_header_offset = zi.header_offset + + file.seek(local_header_offset + _FILENAME_LEN_OFFSET) + leninfo = file.read(2) + local_name_length, = struct.unpack(' 0: - print u"Extracting images" - if not os.path.exists(imagedirpath): - os.makedirs(imagedirpath) - for i in xrange(er.getNumImages()): - name, contents = er.getImage(i) - file(os.path.join(imagedirpath, name), 'wb').write(contents) - - print u"Extracting pml" - pml_string = er.getText() - pmlfilename = bookname + ".pml" - file(os.path.join(outdir, pmlfilename),'wb').write(cleanPML(pml_string)) - if pmlzname is not None: - import zipfile - import shutil - print u"Creating PMLZ file {0}".format(os.path.basename(pmlzname)) - myZipFile = zipfile.ZipFile(pmlzname,'w',zipfile.ZIP_STORED, False) - list = os.listdir(outdir) - for filename in list: - localname = filename - filePath = os.path.join(outdir,filename) - if os.path.isfile(filePath): - myZipFile.write(filePath, localname) - elif os.path.isdir(filePath): - imageList = os.listdir(filePath) - localimgdir = os.path.basename(filePath) - for image in imageList: - localname = os.path.join(localimgdir,image) - imagePath = os.path.join(filePath,image) - if os.path.isfile(imagePath): - myZipFile.write(imagePath, localname) - myZipFile.close() - # remove temporary directory - shutil.rmtree(outdir, True) - print u"Output is {0}".format(pmlzname) - else : - print u"Output is in {0}".format(outdir) - print "done" - except ValueError, e: - print u"Error: {0}".format(e) + with open(infile,'rb') as infileobject: + bookdata = infileobject.read(58) + # Check for Zip + if bookdata[0:0+2] == "PK": + foundrights = False + foundencryption = False + inzip = zipfile.ZipFile(infile,'r') + namelist = set(inzip.namelist()) + if 'META-INF/rights.xml' not in namelist or 'META-INF/encryption.xml' not in namelist: + encryption = "Unencrypted" + else: + rights = etree.fromstring(inzip.read('META-INF/rights.xml')) + adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) + expr = './/%s' % (adept('encryptedKey'),) + bookkey = ''.join(rights.findtext(expr)) + if len(bookkey) == 172: + encryption = "Adobe" + elif len(bookkey) == 64: + encryption = "B&N" + else: + encryption = "Unknown" + except: traceback.print_exc() - return 1 - return 0 - - -def usage(): - print u"Converts DRMed eReader books to PML Source" - print u"Usage:" - print u" erdr2pml [options] infile.pdb [outpath] \"your name\" credit_card_number" - print u" " - print u"Options: " - print u" -h prints this message" - print u" -p create PMLZ instead of source folder" - print u" --make-pmlz create PMLZ instead of source folder" - print u" " - print u"Note:" - print u" if outpath is ommitted, creates source in 'infile_Source' folder" - print u" if outpath is ommitted and pmlz option, creates PMLZ 'infile.pmlz'" - print u" if source folder created, images are in infile_img folder" - print u" if pmlz file created, images are in images folder" - print u" It's enough to enter the last 8 digits of the credit card number" - return - -def getuser_key(name,cc): - newname = "".join(c for c in name.lower() if c >= 'a' and c <= 'z' or c >= '0' and c <= '9') - cc = cc.replace(" ","") - return struct.pack('>LL', binascii.crc32(newname) & 0xffffffff,binascii.crc32(cc[-8:])& 0xffffffff) - -def cli_main(): - print u"eRdr2Pml v{0}. Copyright © 2009–2012 The Dark Reverser et al.".format(__version__) + return encryption +def main(): argv=unicode_argv() - try: - opts, args = getopt.getopt(argv[1:], "hp", ["make-pmlz"]) - except getopt.GetoptError, err: - print err.args[0] - usage() - return 1 - make_pmlz = False - for o, a in opts: - if o == "-h": - usage() - return 0 - elif o == "-p": - make_pmlz = True - elif o == "--make-pmlz": - make_pmlz = True - - if len(args)!=3 and len(args)!=4: - usage() - return 1 - - if len(args)==3: - infile, name, cc = args - if make_pmlz: - outpath = os.path.splitext(infile)[0] + u".pmlz" - else: - outpath = os.path.splitext(infile)[0] + u"_Source" - elif len(args)==4: - infile, outpath, name, cc = args - - print getuser_key(name,cc).encode('hex') - - return decryptBook(infile, outpath, make_pmlz, getuser_key(name,cc)) - + print encryption(argv[1]) + return 0 if __name__ == "__main__": sys.stdout=SafeUnbuffered(sys.stdout) sys.stderr=SafeUnbuffered(sys.stderr) - sys.exit(cli_main()) - + sys.exit(main()) diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/erdr2pml.py b/DeDRM_calibre_plugin/DeDRM_plugin/erdr2pml.py index 4d83368..1dfef42 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/erdr2pml.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/erdr2pml.py @@ -1,797 +1,597 @@ -#! /usr/bin/python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab -# For use with Topaz Scripts Version 2.6 - -import sys -import csv -import os -import math -import getopt -from struct import pack -from struct import unpack - - -class DocParser(object): - 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.gdict = gdict - tmpList = classlst.split('\n') - for pclass in tmpList: - if pclass != '': - # remove the leading period from the css name - cname = pclass[1:] - self.classList[cname] = True - self.fixedimage = fixedimage - self.ocrtext = [] - self.link_id = [] - self.link_title = [] - self.link_page = [] - self.link_href = [] - self.link_type = [] - self.dehyphen_rootid = [] - self.paracont_stemid = [] - self.parastems_stemid = [] - - - def getGlyph(self, gid): - result = '' - id='id="gl%d"' % gid - return self.gdict.lookup(id) - - def glyphs_to_image(self, glyphList): - - def extract(path, key): - b = path.find(key) + len(key) - e = path.find(' ',b) - return int(path[b:e]) - - svgDir = os.path.join(self.bookDir,'svg') - - imgDir = os.path.join(self.bookDir,'img') - imgname = self.id + '_%04d.svg' % self.svgcount - imgfile = os.path.join(imgDir,imgname) - - # get glyph information - gxList = self.getData('info.glyph.x',0,-1) - gyList = self.getData('info.glyph.y',0,-1) - gidList = self.getData('info.glyph.glyphID',0,-1) - - gids = [] - maxws = [] - maxhs = [] - xs = [] - ys = [] - gdefs = [] - - # get path defintions, positions, dimensions for each glyph - # that makes up the image, and find min x and min y to reposition origin - minx = -1 - miny = -1 - for j in glyphList: - gid = gidList[j] - gids.append(gid) - - xs.append(gxList[j]) - if minx == -1: minx = gxList[j] - else : minx = min(minx, gxList[j]) - - ys.append(gyList[j]) - if miny == -1: miny = gyList[j] - else : miny = min(miny, gyList[j]) - - path = self.getGlyph(gid) - gdefs.append(path) - - maxws.append(extract(path,'width=')) - maxhs.append(extract(path,'height=')) - - - # change the origin to minx, miny and calc max height and width - maxw = maxws[0] + xs[0] - minx - maxh = maxhs[0] + ys[0] - miny - for j in xrange(0, len(xs)): - xs[j] = xs[j] - minx - ys[j] = ys[j] - miny - maxw = max( maxw, (maxws[j] + xs[j]) ) - maxh = max( maxh, (maxhs[j] + ys[j]) ) - - # open the image file for output - ifile = open(imgfile,'w') - ifile.write('\n') - ifile.write('\n') - ifile.write('\n' % (math.floor(maxw/10), math.floor(maxh/10), maxw, maxh)) - ifile.write('\n') - for j in xrange(0,len(gdefs)): - ifile.write(gdefs[j]) - ifile.write('\n') - for j in xrange(0,len(gids)): - ifile.write('\n' % (gids[j], xs[j], ys[j])) - ifile.write('') - ifile.close() - - return 0 - - - - # return tag at line pos in document - def lineinDoc(self, pos) : - if (pos >= 0) and (pos < self.docSize) : - item = self.docList[pos] - if item.find('=') >= 0: - (name, argres) = item.split('=',1) - else : - name = item - argres = '' - return name, argres - - - # find tag in doc if within pos to end inclusive - def findinDoc(self, tagpath, pos, end) : - result = None - if end == -1 : - end = self.docSize +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# erdr2pml.py +# Copyright © 2008 The Dark Reverser +# +# Modified 2008–2012 by some_updates, DiapDealer and Apprentice Alf + +# This is a python script. You need a Python interpreter to run it. +# For example, ActiveState Python, which exists for windows. +# Changelog +# +# Based on ereader2html version 0.08 plus some later small fixes +# +# 0.01 - Initial version +# 0.02 - Support more eReader files. Support bold text and links. Fix PML decoder parsing bug. +# 0.03 - Fix incorrect variable usage at one place. +# 0.03b - enhancement by DeBockle (version 259 support) +# Custom version 0.03 - no change to eReader support, only usability changes +# - start of pep-8 indentation (spaces not tab), fix trailing blanks +# - version variable, only one place to change +# - added main routine, now callable as a library/module, +# means tools can add optional support for ereader2html +# - outdir is no longer a mandatory parameter (defaults based on input name if missing) +# - time taken output to stdout +# - Psyco support - reduces runtime by a factor of (over) 3! +# E.g. (~600Kb file) 90 secs down to 24 secs +# - newstyle classes +# - changed map call to list comprehension +# may not work with python 2.3 +# without Psyco this reduces runtime to 90% +# E.g. 90 secs down to 77 secs +# Psyco with map calls takes longer, do not run with map in Psyco JIT! +# - izip calls used instead of zip (if available), further reduction +# in run time (factor of 4.5). +# E.g. (~600Kb file) 90 secs down to 20 secs +# - Python 2.6+ support, avoid DeprecationWarning with sha/sha1 +# 0.04 - Footnote support, PML output, correct charset in html, support more PML tags +# - Feature change, dump out PML file +# - Added supprt for footnote tags. NOTE footnote ids appear to be bad (not usable) +# in some pdb files :-( due to the same id being used multiple times +# - Added correct charset encoding (pml is based on cp1252) +# - Added logging support. +# 0.05 - Improved type 272 support for sidebars, links, chapters, metainfo, etc +# 0.06 - Merge of 0.04 and 0.05. Improved HTML output +# Placed images in subfolder, so that it's possible to just +# drop the book.pml file onto DropBook to make an unencrypted +# copy of the eReader file. +# Using that with Calibre works a lot better than the HTML +# conversion in this code. +# 0.07 - Further Improved type 272 support for sidebars with all earlier fixes +# 0.08 - fixed typos, removed extraneous things +# 0.09 - fixed typos in first_pages to first_page to again support older formats +# 0.10 - minor cleanups +# 0.11 - fixups for using correct xml for footnotes and sidebars for use with Dropbook +# 0.12 - Fix added to prevent lowercasing of image names when the pml code itself uses a different case in the link name. +# 0.13 - change to unbuffered stdout for use with gui front ends +# 0.14 - contributed enhancement to support --make-pmlz switch +# 0.15 - enabled high-ascii to pml character encoding. DropBook now works on Mac. +# 0.16 - convert to use openssl DES (very very fast) or pure python DES if openssl's libcrypto is not available +# 0.17 - added support for pycrypto's DES as well +# 0.18 - on Windows try PyCrypto first and OpenSSL next +# 0.19 - Modify the interface to allow use of import +# 0.20 - modify to allow use inside new interface for calibre plugins +# 0.21 - Support eReader (drm) version 11. +# - Don't reject dictionary format. +# - Ignore sidebars for dictionaries (different format?) +# 0.22 - Unicode and plugin support, different image folders for PMLZ and source +# 0.23 - moved unicode_argv call inside main for Windows DeDRM compatibility + +__version__='0.23' + +import sys, re +import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile, traceback + +if 'calibre' in sys.modules: + inCalibre = True +else: + inCalibre = False + +# Wrap a stream so that output gets flushed immediately +# and also make sure that any unicode strings get +# encoded using "replace" before writing them. +class SafeUnbuffered: + def __init__(self, stream): + self.stream = stream + self.encoding = stream.encoding + if self.encoding == None: + self.encoding = "utf-8" + def write(self, data): + if isinstance(data,unicode): + data = data.encode(self.encoding,"replace") + self.stream.write(data) + self.stream.flush() + def __getattr__(self, attr): + return getattr(self.stream, attr) + +iswindows = sys.platform.startswith('win') +isosx = sys.platform.startswith('darwin') + +def unicode_argv(): + if iswindows: + # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode + # strings. + + # Versions 2.x of Python don't support Unicode in sys.argv on + # Windows, with the underlying Windows API instead replacing multi-byte + # characters with '?'. + + + from ctypes import POINTER, byref, cdll, c_int, windll + from ctypes.wintypes import LPCWSTR, LPWSTR + + GetCommandLineW = cdll.kernel32.GetCommandLineW + GetCommandLineW.argtypes = [] + GetCommandLineW.restype = LPCWSTR + + CommandLineToArgvW = windll.shell32.CommandLineToArgvW + CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] + CommandLineToArgvW.restype = POINTER(LPWSTR) + + cmd = GetCommandLineW() + argc = c_int(0) + argv = CommandLineToArgvW(cmd, byref(argc)) + if argc.value > 0: + # Remove Python executable and commands if present + start = argc.value - len(sys.argv) + return [argv[i] for i in + xrange(start, argc.value)] + # if we don't have any arguments at all, just pass back script name + # this should never happen + return [u"mobidedrm.py"] + else: + argvencoding = sys.stdin.encoding + if argvencoding == None: + argvencoding = "utf-8" + return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv] + +Des = None +if iswindows: + # first try with pycrypto + if inCalibre: + from calibre_plugins.dedrm import pycrypto_des + else: + import pycrypto_des + Des = pycrypto_des.load_pycrypto() + if Des == None: + # they try with openssl + if inCalibre: + from calibre_plugins.dedrm import openssl_des else: - end = min(self.docSize, end) - foundat = -1 - for j in xrange(pos, end): - item = self.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 - - - # returns a vector of integers for the tagpath - def getData(self, tagpath, pos, end): - argres=[] - (foundat, argt) = self.findinDoc(tagpath, pos, end) - if (argt != None) and (len(argt) > 0) : - argList = argt.split('|') - argres = [ int(strval) for strval in argList] - return argres - - - # get the class - def getClass(self, pclass): - nclass = pclass - - # class names are an issue given topaz may start them with numerals (not allowed), - # use a mix of cases (which cause some browsers problems), and actually - # attach numbers after "_reclustered*" to the end to deal classeses that inherit - # from a base class (but then not actually provide all of these _reclustereed - # classes in the stylesheet! - - # so we clean this up by lowercasing, prepend 'cl-', and getting any baseclass - # that exists in the stylesheet first, and then adding this specific class - # after - - # also some class names have spaces in them so need to convert to dashes - if nclass != None : - nclass = nclass.replace(' ','-') - classres = '' - nclass = nclass.lower() - nclass = 'cl-' + nclass - baseclass = '' - # graphic is the base class for captions - if nclass.find('cl-cap-') >=0 : - classres = 'graphic' + ' ' - else : - # strip to find baseclass - p = nclass.find('_') - if p > 0 : - baseclass = nclass[0:p] - if baseclass in self.classList: - classres += baseclass + ' ' - classres += nclass - nclass = classres - return nclass - - - # develop a sorted description of the starting positions of - # groups and regions on the page, as well as the page type - def PageDescription(self): - - def compare(x, y): - (xtype, xval) = x - (ytype, yval) = y - if xval > yval: - return 1 - if xval == yval: - return 0 - return -1 - - result = [] - (pos, pagetype) = self.findinDoc('page.type',0,-1) - - groupList = self.posinDoc('page.group') - groupregionList = self.posinDoc('page.group.region') - pageregionList = self.posinDoc('page.region') - # integrate into one list - for j in groupList: - result.append(('grpbeg',j)) - for j in groupregionList: - result.append(('gregion',j)) - for j in pageregionList: - result.append(('pregion',j)) - result.sort(compare) - - # insert group end and page end indicators - inGroup = False - j = 0 - while True: - if j == len(result): break - rtype = result[j][0] - rval = result[j][1] - if not inGroup and (rtype == 'grpbeg') : - inGroup = True - j = j + 1 - elif inGroup and (rtype in ('grpbeg', 'pregion')): - result.insert(j,('grpend',rval)) - inGroup = False + import openssl_des + Des = openssl_des.load_libcrypto() +else: + # first try with openssl + if inCalibre: + from calibre_plugins.dedrm import openssl_des + else: + import openssl_des + Des = openssl_des.load_libcrypto() + if Des == None: + # then try with pycrypto + if inCalibre: + from calibre_plugins.dedrm import pycrypto_des + else: + import pycrypto_des + Des = pycrypto_des.load_pycrypto() + +# if that did not work then use pure python implementation +# of DES and try to speed it up with Psycho +if Des == None: + if inCalibre: + from calibre_plugins.dedrm import python_des + else: + import python_des + Des = python_des.Des + # Import Psyco if available + try: + # http://psyco.sourceforge.net + import psyco + psyco.full() + except ImportError: + pass + +try: + from hashlib import sha1 +except ImportError: + # older Python release + import sha + sha1 = lambda s: sha.new(s) + +import cgi +import logging + +logging.basicConfig() +#logging.basicConfig(level=logging.DEBUG) + + +class Sectionizer(object): + bkType = "Book" + + def __init__(self, filename, ident): + self.contents = file(filename, 'rb').read() + self.header = self.contents[0:72] + self.num_sections, = struct.unpack('>H', self.contents[76:78]) + # Dictionary or normal content (TODO: Not hard-coded) + if self.header[0x3C:0x3C+8] != ident: + if self.header[0x3C:0x3C+8] == "PDctPPrs": + self.bkType = "Dict" else: - j = j + 1 - if inGroup: - result.append(('grpend',-1)) - result.append(('pageend', -1)) - return pagetype, result - - - - # build a description of the paragraph - def getParaDescription(self, start, end, regtype): - - result = [] - - # paragraph - (pos, pclass) = self.findinDoc('paragraph.class',start,end) - - pclass = self.getClass(pclass) - - # if paragraph uses extratokens (extra glyphs) then make it fixed - (pos, extraglyphs) = self.findinDoc('paragraph.extratokens',start,end) - - # build up a description of the paragraph in result and return it - # first check for the basic - all words paragraph - (pos, sfirst) = self.findinDoc('paragraph.firstWord',start,end) - (pos, slast) = self.findinDoc('paragraph.lastWord',start,end) - if (sfirst != None) and (slast != None) : - first = int(sfirst) - last = int(slast) - - makeImage = (regtype == 'vertical') or (regtype == 'table') - makeImage = makeImage or (extraglyphs != None) - if self.fixedimage: - makeImage = makeImage or (regtype == 'fixed') - - if (pclass != None): - makeImage = makeImage or (pclass.find('.inverted') >= 0) - if self.fixedimage : - makeImage = makeImage or (pclass.find('cl-f-') >= 0) - - # before creating an image make sure glyph info exists - gidList = self.getData('info.glyph.glyphID',0,-1) - - makeImage = makeImage & (len(gidList) > 0) - - if not makeImage : - # standard all word paragraph - for wordnum in xrange(first, last): - result.append(('ocr', wordnum)) - return pclass, result - - # convert paragraph to svg image - # translate first and last word into first and last glyphs - # and generate inline image and include it - glyphList = [] - firstglyphList = self.getData('word.firstGlyph',0,-1) - gidList = self.getData('info.glyph.glyphID',0,-1) - firstGlyph = firstglyphList[first] - if last < len(firstglyphList): - lastGlyph = firstglyphList[last] - else : - lastGlyph = len(gidList) - - # handle case of white sapce paragraphs with no actual glyphs in them - # by reverting to text based paragraph - if firstGlyph >= lastGlyph: - # revert to standard text based paragraph - for wordnum in xrange(first, last): - result.append(('ocr', wordnum)) - return pclass, result - - for glyphnum in xrange(firstGlyph, lastGlyph): - glyphList.append(glyphnum) - # include any extratokens if they exist - (pos, sfg) = self.findinDoc('extratokens.firstGlyph',start,end) - (pos, slg) = self.findinDoc('extratokens.lastGlyph',start,end) - if (sfg != None) and (slg != None): - for glyphnum in xrange(int(sfg), int(slg)): - glyphList.append(glyphnum) - num = self.svgcount - self.glyphs_to_image(glyphList) - self.svgcount += 1 - result.append(('svg', num)) - return pclass, result - - # this type of paragraph may be made up of multiple spans, inline - # word monograms (images), and words with semantic meaning, - # plus glyphs used to form starting letter of first word - - # need to parse this type line by line - line = start + 1 - word_class = '' - - # if end is -1 then we must search to end of document - if end == -1 : - end = self.docSize - - # seems some xml has last* coming before first* so we have to - # handle any order - sp_first = -1 - sp_last = -1 - - gl_first = -1 - gl_last = -1 - - ws_first = -1 - ws_last = -1 - - word_class = '' - - word_semantic_type = '' - - while (line < end) : - - (name, argres) = self.lineinDoc(line) - - if name.endswith('span.firstWord') : - sp_first = int(argres) - - elif name.endswith('span.lastWord') : - sp_last = int(argres) - - elif name.endswith('word.firstGlyph') : - gl_first = int(argres) - - elif name.endswith('word.lastGlyph') : - gl_last = int(argres) - - elif name.endswith('word_semantic.firstWord'): - ws_first = int(argres) - - elif name.endswith('word_semantic.lastWord'): - ws_last = int(argres) - - elif name.endswith('word.class'): - # we only handle spaceafter word class - try: - (cname, space) = argres.split('-',1) - if space == '' : space = '0' - if (cname == 'spaceafter') and (int(space) > 0) : - word_class = 'sa' - except: - pass - - elif name.endswith('word.img.src'): - result.append(('img' + word_class, int(argres))) - word_class = '' - - elif name.endswith('region.img.src'): - result.append(('img' + word_class, int(argres))) - - if (sp_first != -1) and (sp_last != -1): - for wordnum in xrange(sp_first, sp_last): - result.append(('ocr', wordnum)) - sp_first = -1 - sp_last = -1 - - if (gl_first != -1) and (gl_last != -1): - glyphList = [] - for glyphnum in xrange(gl_first, gl_last): - glyphList.append(glyphnum) - num = self.svgcount - self.glyphs_to_image(glyphList) - self.svgcount += 1 - result.append(('svg', num)) - gl_first = -1 - gl_last = -1 - - if (ws_first != -1) and (ws_last != -1): - for wordnum in xrange(ws_first, ws_last): - result.append(('ocr', wordnum)) - ws_first = -1 - ws_last = -1 - - line += 1 - - return pclass, result - - - def buildParagraph(self, pclass, pdesc, type, regtype) : - parares = '' - sep ='' - - classres = '' - if pclass : - classres = ' class="' + pclass + '"' - - br_lb = (regtype == 'fixed') or (regtype == 'chapterheading') or (regtype == 'vertical') - - handle_links = len(self.link_id) > 0 - - if (type == 'full') or (type == 'begin') : - parares += '' - - if (type == 'end'): - parares += ' ' - - lstart = len(parares) - - cnt = len(pdesc) - - for j in xrange( 0, cnt) : - - (wtype, num) = pdesc[j] - - if wtype == 'ocr' : - word = self.ocrtext[num] - sep = ' ' - - if handle_links: - link = self.link_id[num] - if (link > 0): - linktype = self.link_type[link-1] - title = self.link_title[link-1] - if (title == "") or (parares.rfind(title) < 0): - title=parares[lstart:] - if linktype == 'external' : - linkhref = self.link_href[link-1] - linkhtml = '' % linkhref - else : - if len(self.link_page) >= link : - ptarget = self.link_page[link-1] - 1 - linkhtml = '' % ptarget - else : - # just link to the current page - linkhtml = '' - linkhtml += title + '' - pos = parares.rfind(title) - if pos >= 0: - parares = parares[0:pos] + linkhtml + parares[pos+len(title):] - else : - parares += linkhtml - lstart = len(parares) - if word == '_link_' : word = '' - elif (link < 0) : - if word == '_link_' : word = '' - - if word == '_lb_': - if ((num-1) in self.dehyphen_rootid ) or handle_links: - word = '' - sep = '' - elif br_lb : - word = '
\n' - sep = '' - else : - word = '\n' - sep = '' - - if num in self.dehyphen_rootid : - word = word[0:-1] - sep = '' - - parares += word + sep - - elif wtype == 'img' : - sep = '' - parares += '' % num - parares += sep - - elif wtype == 'imgsa' : - sep = ' ' - parares += '' % num - parares += sep - - elif wtype == 'svg' : - sep = '' - parares += '' % num - parares += sep - - if len(sep) > 0 : parares = parares[0:-1] - if (type == 'full') or (type == 'end') : - parares += '

' - return parares - - - def buildTOCEntry(self, pdesc) : - parares = '' - sep ='' - tocentry = '' - handle_links = len(self.link_id) > 0 - - lstart = 0 - - cnt = len(pdesc) - for j in xrange( 0, cnt) : - - (wtype, num) = pdesc[j] - - if wtype == 'ocr' : - word = self.ocrtext[num] - sep = ' ' - - if handle_links: - link = self.link_id[num] - if (link > 0): - linktype = self.link_type[link-1] - title = self.link_title[link-1] - title = title.rstrip('. ') - alt_title = parares[lstart:] - alt_title = alt_title.strip() - # now strip off the actual printed page number - alt_title = alt_title.rstrip('01234567890ivxldIVXLD-.') - alt_title = alt_title.rstrip('. ') - # skip over any external links - can't have them in a books toc - if linktype == 'external' : - title = '' - alt_title = '' - linkpage = '' - else : - if len(self.link_page) >= link : - ptarget = self.link_page[link-1] - 1 - linkpage = '%04d' % ptarget - else : - # just link to the current page - linkpage = self.id[4:] - if len(alt_title) >= len(title): - title = alt_title - if title != '' and linkpage != '': - tocentry += title + '|' + linkpage + '\n' - lstart = len(parares) - if word == '_link_' : word = '' - elif (link < 0) : - if word == '_link_' : word = '' - - if word == '_lb_': - word = '' - sep = '' - - if num in self.dehyphen_rootid : - word = word[0:-1] - sep = '' - - parares += word + sep - - else : - continue - - return tocentry - - - - - # walk the document tree collecting the information needed - # to build an html page using the ocrText - - def process(self): - - tocinfo = '' - hlst = [] - - # get the ocr text - (pos, argres) = self.findinDoc('info.word.ocrText',0,-1) - if argres : self.ocrtext = argres.split('|') - - # get information to dehyphenate the text - self.dehyphen_rootid = self.getData('info.dehyphen.rootID',0,-1) - - # determine if first paragraph is continued from previous page - (pos, self.parastems_stemid) = self.findinDoc('info.paraStems.stemID',0,-1) - first_para_continued = (self.parastems_stemid != None) - - # determine if last paragraph is continued onto the next page - (pos, self.paracont_stemid) = self.findinDoc('info.paraCont.stemID',0,-1) - last_para_continued = (self.paracont_stemid != None) - - # collect link ids - self.link_id = self.getData('info.word.link_id',0,-1) - - # collect link destination page numbers - self.link_page = self.getData('info.links.page',0,-1) - - # collect link types (container versus external) - (pos, argres) = self.findinDoc('info.links.type',0,-1) - if argres : self.link_type = argres.split('|') - - # collect link destinations - (pos, argres) = self.findinDoc('info.links.href',0,-1) - if argres : self.link_href = argres.split('|') - - # collect link titles - (pos, argres) = self.findinDoc('info.links.title',0,-1) - if argres : - self.link_title = argres.split('|') + raise ValueError('Invalid file format') + self.sections = [] + for i in xrange(self.num_sections): + offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', self.contents[78+i*8:78+i*8+8]) + flags, val = a1, a2<<16|a3<<8|a4 + self.sections.append( (offset, flags, val) ) + def loadSection(self, section): + if section + 1 == self.num_sections: + end_off = len(self.contents) else: - self.link_title.append('') - - # get a descriptions of the starting points of the regions - # and groups on the page - (pagetype, pageDesc) = self.PageDescription() - regcnt = len(pageDesc) - 1 - - anchorSet = False - breakSet = False - inGroup = False - - # process each region on the page and convert what you can to html - - for j in xrange(regcnt): - - (etype, start) = pageDesc[j] - (ntype, end) = pageDesc[j+1] + end_off = self.sections[section + 1][0] + off = self.sections[section][0] + return self.contents[off:end_off] + +# cleanup unicode filenames +# borrowed from calibre from calibre/src/calibre/__init__.py +# added in removal of control (<32) chars +# and removal of . at start and end +# and with some (heavily edited) code from Paul Durrant's kindlenamer.py +def sanitizeFileName(name): + # substitute filename unfriendly characters + name = name.replace(u"<",u"[").replace(u">",u"]").replace(u" : ",u" – ").replace(u": ",u" – ").replace(u":",u"—").replace(u"/",u"_").replace(u"\\",u"_").replace(u"|",u"_").replace(u"\"",u"\'") + # delete control characters + name = u"".join(char for char in name if ord(char)>=32) + # white space to single space, delete leading and trailing while space + name = re.sub(ur"\s", u" ", name).strip() + # remove leading dots + while len(name)>0 and name[0] == u".": + name = name[1:] + # remove trailing dots (Windows doesn't like them) + if name.endswith(u'.'): + name = name[:-1] + return name + +def fixKey(key): + def fixByte(b): + return b ^ ((b ^ (b<<1) ^ (b<<2) ^ (b<<3) ^ (b<<4) ^ (b<<5) ^ (b<<6) ^ (b<<7) ^ 0x80) & 0x80) + return "".join([chr(fixByte(ord(a))) for a in key]) + +def deXOR(text, sp, table): + r='' + j = sp + for i in xrange(len(text)): + r += chr(ord(table[j]) ^ ord(text[i])) + j = j + 1 + if j == len(table): + j = 0 + return r + +class EreaderProcessor(object): + def __init__(self, sect, user_key): + self.section_reader = sect.loadSection + data = self.section_reader(0) + version, = struct.unpack('>H', data[0:2]) + self.version = version + logging.info('eReader file format version %s', version) + if version != 272 and version != 260 and version != 259: + raise ValueError('incorrect eReader version %d (error 1)' % version) + data = self.section_reader(1) + self.data = data + des = Des(fixKey(data[0:8])) + cookie_shuf, cookie_size = struct.unpack('>LL', des.decrypt(data[-8:])) + if cookie_shuf < 3 or cookie_shuf > 0x14 or cookie_size < 0xf0 or cookie_size > 0x200: + raise ValueError('incorrect eReader version (error 2)') + input = des.decrypt(data[-cookie_size:]) + def unshuff(data, shuf): + r = [''] * len(data) + j = 0 + for i in xrange(len(data)): + j = (j + shuf) % len(data) + r[j] = data[i] + assert len("".join(r)) == len(data) + return "".join(r) + r = unshuff(input[0:-8], cookie_shuf) + + drm_sub_version = struct.unpack('>H', r[0:2])[0] + self.num_text_pages = struct.unpack('>H', r[2:4])[0] - 1 + self.num_image_pages = struct.unpack('>H', r[26:26+2])[0] + self.first_image_page = struct.unpack('>H', r[24:24+2])[0] + # Default values + self.num_footnote_pages = 0 + self.num_sidebar_pages = 0 + self.first_footnote_page = -1 + self.first_sidebar_page = -1 + if self.version == 272: + self.num_footnote_pages = struct.unpack('>H', r[46:46+2])[0] + self.first_footnote_page = struct.unpack('>H', r[44:44+2])[0] + if (sect.bkType == "Book"): + self.num_sidebar_pages = struct.unpack('>H', r[38:38+2])[0] + self.first_sidebar_page = struct.unpack('>H', r[36:36+2])[0] + # self.num_bookinfo_pages = struct.unpack('>H', r[34:34+2])[0] + # self.first_bookinfo_page = struct.unpack('>H', r[32:32+2])[0] + # self.num_chapter_pages = struct.unpack('>H', r[22:22+2])[0] + # self.first_chapter_page = struct.unpack('>H', r[20:20+2])[0] + # self.num_link_pages = struct.unpack('>H', r[30:30+2])[0] + # self.first_link_page = struct.unpack('>H', r[28:28+2])[0] + # self.num_xtextsize_pages = struct.unpack('>H', r[54:54+2])[0] + # self.first_xtextsize_page = struct.unpack('>H', r[52:52+2])[0] + + # **before** data record 1 was decrypted and unshuffled, it contained data + # to create an XOR table and which is used to fix footnote record 0, link records, chapter records, etc + self.xortable_offset = struct.unpack('>H', r[40:40+2])[0] + self.xortable_size = struct.unpack('>H', r[42:42+2])[0] + self.xortable = self.data[self.xortable_offset:self.xortable_offset + self.xortable_size] + else: + # Nothing needs to be done + pass + # self.num_bookinfo_pages = 0 + # self.num_chapter_pages = 0 + # self.num_link_pages = 0 + # self.num_xtextsize_pages = 0 + # self.first_bookinfo_page = -1 + # self.first_chapter_page = -1 + # self.first_link_page = -1 + # self.first_xtextsize_page = -1 + + logging.debug('self.num_text_pages %d', self.num_text_pages) + logging.debug('self.num_footnote_pages %d, self.first_footnote_page %d', self.num_footnote_pages , self.first_footnote_page) + logging.debug('self.num_sidebar_pages %d, self.first_sidebar_page %d', self.num_sidebar_pages , self.first_sidebar_page) + self.flags = struct.unpack('>L', r[4:8])[0] + reqd_flags = (1<<9) | (1<<7) | (1<<10) + if (self.flags & reqd_flags) != reqd_flags: + print "Flags: 0x%X" % self.flags + raise ValueError('incompatible eReader file') + des = Des(fixKey(user_key)) + if version == 259: + if drm_sub_version != 7: + raise ValueError('incorrect eReader version %d (error 3)' % drm_sub_version) + encrypted_key_sha = r[44:44+20] + encrypted_key = r[64:64+8] + elif version == 260: + if drm_sub_version != 13 and drm_sub_version != 11: + raise ValueError('incorrect eReader version %d (error 3)' % drm_sub_version) + if drm_sub_version == 13: + encrypted_key = r[44:44+8] + encrypted_key_sha = r[52:52+20] + else: + encrypted_key = r[64:64+8] + encrypted_key_sha = r[44:44+20] + elif version == 272: + encrypted_key = r[172:172+8] + encrypted_key_sha = r[56:56+20] + self.content_key = des.decrypt(encrypted_key) + if sha1(self.content_key).digest() != encrypted_key_sha: + raise ValueError('Incorrect Name and/or Credit Card') + + def getNumImages(self): + return self.num_image_pages + + def getImage(self, i): + sect = self.section_reader(self.first_image_page + i) + name = sect[4:4+32].strip('\0') + data = sect[62:] + return sanitizeFileName(unicode(name,'windows-1252')), data + + + # def getChapterNamePMLOffsetData(self): + # cv = '' + # if self.num_chapter_pages > 0: + # for i in xrange(self.num_chapter_pages): + # chaps = self.section_reader(self.first_chapter_page + i) + # j = i % self.xortable_size + # offname = deXOR(chaps, j, self.xortable) + # offset = struct.unpack('>L', offname[0:4])[0] + # name = offname[4:].strip('\0') + # cv += '%d|%s\n' % (offset, name) + # return cv + + # def getLinkNamePMLOffsetData(self): + # lv = '' + # if self.num_link_pages > 0: + # for i in xrange(self.num_link_pages): + # links = self.section_reader(self.first_link_page + i) + # j = i % self.xortable_size + # offname = deXOR(links, j, self.xortable) + # offset = struct.unpack('>L', offname[0:4])[0] + # name = offname[4:].strip('\0') + # lv += '%d|%s\n' % (offset, name) + # return lv + + # def getExpandedTextSizesData(self): + # ts = '' + # if self.num_xtextsize_pages > 0: + # tsize = deXOR(self.section_reader(self.first_xtextsize_page), 0, self.xortable) + # for i in xrange(self.num_text_pages): + # xsize = struct.unpack('>H', tsize[0:2])[0] + # ts += "%d\n" % xsize + # tsize = tsize[2:] + # return ts + + # def getBookInfo(self): + # bkinfo = '' + # if self.num_bookinfo_pages > 0: + # info = self.section_reader(self.first_bookinfo_page) + # bkinfo = deXOR(info, 0, self.xortable) + # bkinfo = bkinfo.replace('\0','|') + # bkinfo += '\n' + # return bkinfo + + def getText(self): + des = Des(fixKey(self.content_key)) + r = '' + for i in xrange(self.num_text_pages): + logging.debug('get page %d', i) + r += zlib.decompress(des.decrypt(self.section_reader(1 + i))) + + # now handle footnotes pages + if self.num_footnote_pages > 0: + r += '\n' + # the record 0 of the footnote section must pass through the Xor Table to make it useful + sect = self.section_reader(self.first_footnote_page) + fnote_ids = deXOR(sect, 0, self.xortable) + # the remaining records of the footnote sections need to be decoded with the content_key and zlib inflated + des = Des(fixKey(self.content_key)) + for i in xrange(1,self.num_footnote_pages): + logging.debug('get footnotepage %d', i) + id_len = ord(fnote_ids[2]) + id = fnote_ids[3:3+id_len] + fmarker = '\n' % id + fmarker += zlib.decompress(des.decrypt(self.section_reader(self.first_footnote_page + i))) + fmarker += '\n\n' + r += fmarker + fnote_ids = fnote_ids[id_len+4:] + + # TODO: Handle dictionary index (?) pages - which are also marked as + # sidebar_pages (?). For now dictionary sidebars are ignored + # For dictionaries - record 0 is null terminated strings, followed by + # blocks of around 62000 bytes and a final block. Not sure of the + # encoding + + # now handle sidebar pages + if self.num_sidebar_pages > 0: + r += '\n' + # the record 0 of the sidebar section must pass through the Xor Table to make it useful + sect = self.section_reader(self.first_sidebar_page) + sbar_ids = deXOR(sect, 0, self.xortable) + # the remaining records of the sidebar sections need to be decoded with the content_key and zlib inflated + des = Des(fixKey(self.content_key)) + for i in xrange(1,self.num_sidebar_pages): + id_len = ord(sbar_ids[2]) + id = sbar_ids[3:3+id_len] + smarker = '\n' % id + smarker += zlib.decompress(des.decrypt(self.section_reader(self.first_sidebar_page + i))) + smarker += '\n\n' + r += smarker + sbar_ids = sbar_ids[id_len+4:] + + return r + +def cleanPML(pml): + # Convert special characters to proper PML code. High ASCII start at (\x80, \a128) and go up to (\xff, \a255) + pml2 = pml + for k in xrange(128,256): + badChar = chr(k) + pml2 = pml2.replace(badChar, '\\a%03d' % k) + return pml2 + +def decryptBook(infile, outpath, make_pmlz, user_key): + bookname = os.path.splitext(os.path.basename(infile))[0] + if make_pmlz: + # outpath is actually pmlz name + pmlzname = outpath + outdir = tempfile.mkdtemp() + imagedirpath = os.path.join(outdir,u"images") + else: + pmlzname = None + outdir = outpath + imagedirpath = os.path.join(outdir,bookname + u"_img") + + try: + if not os.path.exists(outdir): + os.makedirs(outdir) + print u"Decoding File" + sect = Sectionizer(infile, 'PNRdPPrs') + er = EreaderProcessor(sect, user_key) + + if er.getNumImages() > 0: + print u"Extracting images" + if not os.path.exists(imagedirpath): + os.makedirs(imagedirpath) + for i in xrange(er.getNumImages()): + name, contents = er.getImage(i) + file(os.path.join(imagedirpath, name), 'wb').write(contents) + + print u"Extracting pml" + pml_string = er.getText() + pmlfilename = bookname + ".pml" + file(os.path.join(outdir, pmlfilename),'wb').write(cleanPML(pml_string)) + if pmlzname is not None: + import zipfile + import shutil + print u"Creating PMLZ file {0}".format(os.path.basename(pmlzname)) + myZipFile = zipfile.ZipFile(pmlzname,'w',zipfile.ZIP_STORED, False) + list = os.listdir(outdir) + for filename in list: + localname = filename + filePath = os.path.join(outdir,filename) + if os.path.isfile(filePath): + myZipFile.write(filePath, localname) + elif os.path.isdir(filePath): + imageList = os.listdir(filePath) + localimgdir = os.path.basename(filePath) + for image in imageList: + localname = os.path.join(localimgdir,image) + imagePath = os.path.join(filePath,image) + if os.path.isfile(imagePath): + myZipFile.write(imagePath, localname) + myZipFile.close() + # remove temporary directory + shutil.rmtree(outdir, True) + print u"Output is {0}".format(pmlzname) + else : + print u"Output is in {0}".format(outdir) + print "done" + except ValueError, e: + print u"Error: {0}".format(e) + traceback.print_exc() + return 1 + return 0 + + +def usage(): + print u"Converts DRMed eReader books to PML Source" + print u"Usage:" + print u" erdr2pml [options] infile.pdb [outpath] \"your name\" credit_card_number" + print u" " + print u"Options: " + print u" -h prints this message" + print u" -p create PMLZ instead of source folder" + print u" --make-pmlz create PMLZ instead of source folder" + print u" " + print u"Note:" + print u" if outpath is ommitted, creates source in 'infile_Source' folder" + print u" if outpath is ommitted and pmlz option, creates PMLZ 'infile.pmlz'" + print u" if source folder created, images are in infile_img folder" + print u" if pmlz file created, images are in images folder" + print u" It's enough to enter the last 8 digits of the credit card number" + return + +def getuser_key(name,cc): + newname = "".join(c for c in name.lower() if c >= 'a' and c <= 'z' or c >= '0' and c <= '9') + cc = cc.replace(" ","") + return struct.pack('>LL', binascii.crc32(newname) & 0xffffffff,binascii.crc32(cc[-8:])& 0xffffffff) + +def cli_main(): + print u"eRdr2Pml v{0}. Copyright © 2009–2012 The Dark Reverser et al.".format(__version__) + + argv=unicode_argv() + try: + opts, args = getopt.getopt(argv[1:], "hp", ["make-pmlz"]) + except getopt.GetoptError, err: + print err.args[0] + usage() + return 1 + make_pmlz = False + for o, a in opts: + if o == "-h": + usage() + return 0 + elif o == "-p": + make_pmlz = True + elif o == "--make-pmlz": + make_pmlz = True + + if len(args)!=3 and len(args)!=4: + usage() + return 1 + + if len(args)==3: + infile, name, cc = args + if make_pmlz: + outpath = os.path.splitext(infile)[0] + u".pmlz" + else: + outpath = os.path.splitext(infile)[0] + u"_Source" + elif len(args)==4: + infile, outpath, name, cc = args + print getuser_key(name,cc).encode('hex') - # set anchor for link target on this page - if not anchorSet and not first_para_continued: - hlst.append('\n') - anchorSet = True + return decryptBook(infile, outpath, make_pmlz, getuser_key(name,cc)) - # handle groups of graphics with text captions - if (etype == 'grpbeg'): - (pos, grptype) = self.findinDoc('group.type', start, end) - if grptype != None: - if grptype == 'graphic': - gcstr = ' class="' + grptype + '"' - hlst.append('') - inGroup = True - elif (etype == 'grpend'): - if inGroup: - hlst.append('\n') - inGroup = False +if __name__ == "__main__": + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) + sys.exit(cli_main()) - else: - (pos, regtype) = self.findinDoc('region.type',start,end) - - if regtype == 'graphic' : - (pos, simgsrc) = self.findinDoc('img.src',start,end) - if simgsrc: - if inGroup: - hlst.append('' % int(simgsrc)) - else: - hlst.append('
' % int(simgsrc)) - - elif regtype == 'chapterheading' : - (pclass, pdesc) = self.getParaDescription(start,end, regtype) - if not breakSet: - hlst.append('
 
\n') - breakSet = True - tag = 'h1' - if pclass and (len(pclass) >= 7): - if pclass[3:7] == 'ch1-' : tag = 'h1' - if pclass[3:7] == 'ch2-' : tag = 'h2' - if pclass[3:7] == 'ch3-' : tag = 'h3' - hlst.append('<' + tag + ' class="' + pclass + '">') - else: - hlst.append('<' + tag + '>') - hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype)) - hlst.append('') - - elif (regtype == 'text') or (regtype == 'fixed') or (regtype == 'insert') or (regtype == 'listitem'): - ptype = 'full' - # check to see if this is a continution from the previous page - if first_para_continued : - ptype = 'end' - first_para_continued = False - (pclass, pdesc) = self.getParaDescription(start,end, regtype) - if pclass and (len(pclass) >= 6) and (ptype == 'full'): - tag = 'p' - if pclass[3:6] == 'h1-' : tag = 'h4' - if pclass[3:6] == 'h2-' : tag = 'h5' - if pclass[3:6] == 'h3-' : tag = 'h6' - hlst.append('<' + tag + ' class="' + pclass + '">') - hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype)) - hlst.append('') - else : - hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype)) - - elif (regtype == 'tocentry') : - ptype = 'full' - if first_para_continued : - ptype = 'end' - first_para_continued = False - (pclass, pdesc) = self.getParaDescription(start,end, regtype) - tocinfo += self.buildTOCEntry(pdesc) - hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype)) - - elif (regtype == 'vertical') or (regtype == 'table') : - ptype = 'full' - if inGroup: - ptype = 'middle' - if first_para_continued : - ptype = 'end' - first_para_continued = False - (pclass, pdesc) = self.getParaDescription(start, end, regtype) - hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype)) - - - elif (regtype == 'synth_fcvr.center'): - (pos, simgsrc) = self.findinDoc('img.src',start,end) - if simgsrc: - hlst.append('
' % int(simgsrc)) - - else : - print ' Making region type', regtype, - (pos, temp) = self.findinDoc('paragraph',start,end) - (pos2, temp) = self.findinDoc('span',start,end) - if pos != -1 or pos2 != -1: - print ' a "text" region' - orig_regtype = regtype - regtype = 'fixed' - ptype = 'full' - # check to see if this is a continution from the previous page - if first_para_continued : - ptype = 'end' - first_para_continued = False - (pclass, pdesc) = self.getParaDescription(start,end, regtype) - if not pclass: - if orig_regtype.endswith('.right') : pclass = 'cl-right' - elif orig_regtype.endswith('.center') : pclass = 'cl-center' - elif orig_regtype.endswith('.left') : pclass = 'cl-left' - elif orig_regtype.endswith('.justify') : pclass = 'cl-justify' - if pclass and (ptype == 'full') and (len(pclass) >= 6): - tag = 'p' - if pclass[3:6] == 'h1-' : tag = 'h4' - if pclass[3:6] == 'h2-' : tag = 'h5' - if pclass[3:6] == 'h3-' : tag = 'h6' - hlst.append('<' + tag + ' class="' + pclass + '">') - hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype)) - hlst.append('') - else : - hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype)) - else : - print ' a "graphic" region' - (pos, simgsrc) = self.findinDoc('img.src',start,end) - if simgsrc: - hlst.append('
' % int(simgsrc)) - - - htmlpage = "".join(hlst) - if last_para_continued : - if htmlpage[-4:] == '

': - htmlpage = htmlpage[0:-4] - last_para_continued = False - - return htmlpage, tocinfo - - -def convert2HTML(flatxml, classlst, fileid, bookDir, gdict, fixedimage): - # create a document parser - dp = DocParser(flatxml, classlst, fileid, bookDir, gdict, fixedimage) - htmlpage, tocinfo = dp.process() - return htmlpage, tocinfo diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/flatxml2html.py b/DeDRM_calibre_plugin/DeDRM_plugin/flatxml2html.py index 4dfd6c7..991591b 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/flatxml2html.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/flatxml2html.py @@ -1,63 +1,127 @@ #! /usr/bin/python # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab +# For use with Topaz Scripts Version 2.6 import sys import csv import os +import math import getopt from struct import pack from struct import unpack -class PParser(object): - def __init__(self, gd, flatxml, meta_array): - self.gd = gd - self.flatdoc = flatxml.split('\n') - self.docSize = len(self.flatdoc) - self.temp = [] - - self.ph = -1 - self.pw = -1 - startpos = self.posinDoc('page.h') or self.posinDoc('book.h') - for p in startpos: - (name, argres) = self.lineinDoc(p) - self.ph = max(self.ph, int(argres)) - startpos = self.posinDoc('page.w') or self.posinDoc('book.w') - for p in startpos: - (name, argres) = self.lineinDoc(p) - self.pw = max(self.pw, int(argres)) - - if self.ph <= 0: - self.ph = int(meta_array.get('pageHeight', '11000')) - if self.pw <= 0: - self.pw = int(meta_array.get('pageWidth', '8500')) - - res = [] - startpos = self.posinDoc('info.glyph.x') - for p in startpos: - argres = self.getDataatPos('info.glyph.x', p) - res.extend(argres) - self.gx = res - - res = [] - startpos = self.posinDoc('info.glyph.y') - for p in startpos: - argres = self.getDataatPos('info.glyph.y', p) - res.extend(argres) - self.gy = res - - res = [] - startpos = self.posinDoc('info.glyph.glyphID') - for p in startpos: - argres = self.getDataatPos('info.glyph.glyphID', p) - res.extend(argres) - self.gid = res +class DocParser(object): + 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.gdict = gdict + tmpList = classlst.split('\n') + for pclass in tmpList: + if pclass != '': + # remove the leading period from the css name + cname = pclass[1:] + self.classList[cname] = True + self.fixedimage = fixedimage + self.ocrtext = [] + self.link_id = [] + self.link_title = [] + self.link_page = [] + self.link_href = [] + self.link_type = [] + self.dehyphen_rootid = [] + self.paracont_stemid = [] + self.parastems_stemid = [] + + + def getGlyph(self, gid): + result = '' + id='id="gl%d"' % gid + return self.gdict.lookup(id) + + def glyphs_to_image(self, glyphList): + + def extract(path, key): + b = path.find(key) + len(key) + e = path.find(' ',b) + return int(path[b:e]) + + svgDir = os.path.join(self.bookDir,'svg') + + imgDir = os.path.join(self.bookDir,'img') + imgname = self.id + '_%04d.svg' % self.svgcount + imgfile = os.path.join(imgDir,imgname) + + # get glyph information + gxList = self.getData('info.glyph.x',0,-1) + gyList = self.getData('info.glyph.y',0,-1) + gidList = self.getData('info.glyph.glyphID',0,-1) + + gids = [] + maxws = [] + maxhs = [] + xs = [] + ys = [] + gdefs = [] + + # get path defintions, positions, dimensions for each glyph + # that makes up the image, and find min x and min y to reposition origin + minx = -1 + miny = -1 + for j in glyphList: + gid = gidList[j] + gids.append(gid) + + xs.append(gxList[j]) + if minx == -1: minx = gxList[j] + else : minx = min(minx, gxList[j]) + + ys.append(gyList[j]) + if miny == -1: miny = gyList[j] + else : miny = min(miny, gyList[j]) + + path = self.getGlyph(gid) + gdefs.append(path) + + maxws.append(extract(path,'width=')) + maxhs.append(extract(path,'height=')) + + + # change the origin to minx, miny and calc max height and width + maxw = maxws[0] + xs[0] - minx + maxh = maxhs[0] + ys[0] - miny + for j in xrange(0, len(xs)): + xs[j] = xs[j] - minx + ys[j] = ys[j] - miny + maxw = max( maxw, (maxws[j] + xs[j]) ) + maxh = max( maxh, (maxhs[j] + ys[j]) ) + + # open the image file for output + ifile = open(imgfile,'w') + ifile.write('\n') + ifile.write('\n') + ifile.write('\n' % (math.floor(maxw/10), math.floor(maxh/10), maxw, maxh)) + ifile.write('\n') + for j in xrange(0,len(gdefs)): + ifile.write(gdefs[j]) + ifile.write('\n') + for j in xrange(0,len(gids)): + ifile.write('\n' % (gids[j], xs[j], ys[j])) + ifile.write('') + ifile.close() + + return 0 + # return tag at line pos in document def lineinDoc(self, pos) : if (pos >= 0) and (pos < self.docSize) : - item = self.flatdoc[pos] + item = self.docList[pos] if item.find('=') >= 0: (name, argres) = item.split('=',1) else : @@ -65,6 +129,7 @@ class PParser(object): argres = '' return name, argres + # find tag in doc if within pos to end inclusive def findinDoc(self, tagpath, pos, end) : result = None @@ -74,7 +139,7 @@ class PParser(object): end = min(self.docSize, end) foundat = -1 for j in xrange(pos, end): - item = self.flatdoc[j] + item = self.docList[j] if item.find('=') >= 0: (name, argres) = item.split('=',1) else : @@ -86,6 +151,7 @@ class PParser(object): break return foundat, result + # return list of start positions for the tagpath def posinDoc(self, tagpath): startpos = [] @@ -98,152 +164,638 @@ class PParser(object): pos = foundpos + 1 return startpos - def getData(self, path): - result = None - cnt = len(self.flatdoc) - for j in xrange(cnt): - item = self.flatdoc[j] - if item.find('=') >= 0: - (name, argt) = item.split('=') - argres = argt.split('|') - else: - name = item - argres = [] - if (name.endswith(path)): - result = argres - break - if (len(argres) > 0) : - for j in xrange(0,len(argres)): - argres[j] = int(argres[j]) - return result - def getDataatPos(self, path, pos): - result = None - item = self.flatdoc[pos] - if item.find('=') >= 0: - (name, argt) = item.split('=') - argres = argt.split('|') - else: - name = item - argres = [] - if (len(argres) > 0) : - for j in xrange(0,len(argres)): - argres[j] = int(argres[j]) - if (name.endswith(path)): - result = argres - return result - - def getDataTemp(self, path): - result = None - cnt = len(self.temp) - for j in xrange(cnt): - item = self.temp[j] - if item.find('=') >= 0: - (name, argt) = item.split('=') - argres = argt.split('|') - else: - name = item - argres = [] - if (name.endswith(path)): - result = argres - self.temp.pop(j) - break - if (len(argres) > 0) : - for j in xrange(0,len(argres)): - argres[j] = int(argres[j]) - return result + # returns a vector of integers for the tagpath + def getData(self, tagpath, pos, end): + argres=[] + (foundat, argt) = self.findinDoc(tagpath, pos, end) + if (argt != None) and (len(argt) > 0) : + argList = argt.split('|') + argres = [ int(strval) for strval in argList] + return argres + + + # get the class + def getClass(self, pclass): + nclass = pclass + + # class names are an issue given topaz may start them with numerals (not allowed), + # use a mix of cases (which cause some browsers problems), and actually + # attach numbers after "_reclustered*" to the end to deal classeses that inherit + # from a base class (but then not actually provide all of these _reclustereed + # classes in the stylesheet! + + # so we clean this up by lowercasing, prepend 'cl-', and getting any baseclass + # that exists in the stylesheet first, and then adding this specific class + # after + + # also some class names have spaces in them so need to convert to dashes + if nclass != None : + nclass = nclass.replace(' ','-') + classres = '' + nclass = nclass.lower() + nclass = 'cl-' + nclass + baseclass = '' + # graphic is the base class for captions + if nclass.find('cl-cap-') >=0 : + classres = 'graphic' + ' ' + else : + # strip to find baseclass + p = nclass.find('_') + if p > 0 : + baseclass = nclass[0:p] + if baseclass in self.classList: + classres += baseclass + ' ' + classres += nclass + nclass = classres + return nclass + + + # develop a sorted description of the starting positions of + # groups and regions on the page, as well as the page type + def PageDescription(self): + + def compare(x, y): + (xtype, xval) = x + (ytype, yval) = y + if xval > yval: + return 1 + if xval == yval: + return 0 + return -1 - 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): + (pos, pagetype) = self.findinDoc('page.type',0,-1) + + groupList = self.posinDoc('page.group') + groupregionList = self.posinDoc('page.group.region') + pageregionList = self.posinDoc('page.region') + # integrate into one list + for j in groupList: + result.append(('grpbeg',j)) + for j in groupregionList: + result.append(('gregion',j)) + for j in pageregionList: + result.append(('pregion',j)) + result.sort(compare) + + # insert group end and page end indicators + inGroup = False + j = 0 + while True: + if j == len(result): break + rtype = result[j][0] + rval = result[j][1] + if not inGroup and (rtype == 'grpbeg') : + inGroup = True + j = j + 1 + elif inGroup and (rtype in ('grpbeg', 'pregion')): + result.insert(j,('grpend',rval)) + inGroup = False + else: + j = j + 1 + if inGroup: + result.append(('grpend',-1)) + result.append(('pageend', -1)) + return pagetype, result + + + + # build a description of the paragraph + def getParaDescription(self, start, end, regtype): + result = [] - if (self.gid != None) and (len(self.gid) > 0): - glyphs = [] - for j in set(self.gid): - glyphs.append(j) - glyphs.sort() - for gid in glyphs: - id='id="gl%d"' % gid - path = self.gd.lookup(id) - if path: - result.append(id + ' ' + path) - return result - - -def convert2SVG(gdict, flat_xml, pageid, previd, nextid, svgDir, raw, meta_array, scaledpi): - mlst = [] - pp = PParser(gdict, flat_xml, meta_array) - mlst.append('\n') - if (raw): - mlst.append('\n') - mlst.append('\n' % (pp.pw / scaledpi, pp.ph / scaledpi, pp.pw -1, pp.ph -1)) - mlst.append('Page %d - %s by %s\n' % (pageid, meta_array['Title'],meta_array['Authors'])) - else: - mlst.append('\n') - mlst.append('\n') - mlst.append('Page %d - %s by %s\n' % (pageid, meta_array['Title'],meta_array['Authors'])) - mlst.append('\n') - mlst.append('\n') - mlst.append('\n') - mlst.append('
\n') - if previd == None: - mlst.append('\n') + + # paragraph + (pos, pclass) = self.findinDoc('paragraph.class',start,end) + + pclass = self.getClass(pclass) + + # if paragraph uses extratokens (extra glyphs) then make it fixed + (pos, extraglyphs) = self.findinDoc('paragraph.extratokens',start,end) + + # build up a description of the paragraph in result and return it + # first check for the basic - all words paragraph + (pos, sfirst) = self.findinDoc('paragraph.firstWord',start,end) + (pos, slast) = self.findinDoc('paragraph.lastWord',start,end) + if (sfirst != None) and (slast != None) : + first = int(sfirst) + last = int(slast) + + makeImage = (regtype == 'vertical') or (regtype == 'table') + makeImage = makeImage or (extraglyphs != None) + if self.fixedimage: + makeImage = makeImage or (regtype == 'fixed') + + if (pclass != None): + makeImage = makeImage or (pclass.find('.inverted') >= 0) + if self.fixedimage : + makeImage = makeImage or (pclass.find('cl-f-') >= 0) + + # before creating an image make sure glyph info exists + gidList = self.getData('info.glyph.glyphID',0,-1) + + makeImage = makeImage & (len(gidList) > 0) + + if not makeImage : + # standard all word paragraph + for wordnum in xrange(first, last): + result.append(('ocr', wordnum)) + return pclass, result + + # convert paragraph to svg image + # translate first and last word into first and last glyphs + # and generate inline image and include it + glyphList = [] + firstglyphList = self.getData('word.firstGlyph',0,-1) + gidList = self.getData('info.glyph.glyphID',0,-1) + firstGlyph = firstglyphList[first] + if last < len(firstglyphList): + lastGlyph = firstglyphList[last] + else : + lastGlyph = len(gidList) + + # handle case of white sapce paragraphs with no actual glyphs in them + # by reverting to text based paragraph + if firstGlyph >= lastGlyph: + # revert to standard text based paragraph + for wordnum in xrange(first, last): + result.append(('ocr', wordnum)) + return pclass, result + + for glyphnum in xrange(firstGlyph, lastGlyph): + glyphList.append(glyphnum) + # include any extratokens if they exist + (pos, sfg) = self.findinDoc('extratokens.firstGlyph',start,end) + (pos, slg) = self.findinDoc('extratokens.lastGlyph',start,end) + if (sfg != None) and (slg != None): + for glyphnum in xrange(int(sfg), int(slg)): + glyphList.append(glyphnum) + num = self.svgcount + self.glyphs_to_image(glyphList) + self.svgcount += 1 + result.append(('svg', num)) + return pclass, result + + # this type of paragraph may be made up of multiple spans, inline + # word monograms (images), and words with semantic meaning, + # plus glyphs used to form starting letter of first word + + # need to parse this type line by line + line = start + 1 + word_class = '' + + # if end is -1 then we must search to end of document + if end == -1 : + end = self.docSize + + # seems some xml has last* coming before first* so we have to + # handle any order + sp_first = -1 + sp_last = -1 + + gl_first = -1 + gl_last = -1 + + ws_first = -1 + ws_last = -1 + + word_class = '' + + word_semantic_type = '' + + while (line < end) : + + (name, argres) = self.lineinDoc(line) + + if name.endswith('span.firstWord') : + sp_first = int(argres) + + elif name.endswith('span.lastWord') : + sp_last = int(argres) + + elif name.endswith('word.firstGlyph') : + gl_first = int(argres) + + elif name.endswith('word.lastGlyph') : + gl_last = int(argres) + + elif name.endswith('word_semantic.firstWord'): + ws_first = int(argres) + + elif name.endswith('word_semantic.lastWord'): + ws_last = int(argres) + + elif name.endswith('word.class'): + # we only handle spaceafter word class + try: + (cname, space) = argres.split('-',1) + if space == '' : space = '0' + if (cname == 'spaceafter') and (int(space) > 0) : + word_class = 'sa' + except: + pass + + elif name.endswith('word.img.src'): + result.append(('img' + word_class, int(argres))) + word_class = '' + + elif name.endswith('region.img.src'): + result.append(('img' + word_class, int(argres))) + + if (sp_first != -1) and (sp_last != -1): + for wordnum in xrange(sp_first, sp_last): + result.append(('ocr', wordnum)) + sp_first = -1 + sp_last = -1 + + if (gl_first != -1) and (gl_last != -1): + glyphList = [] + for glyphnum in xrange(gl_first, gl_last): + glyphList.append(glyphnum) + num = self.svgcount + self.glyphs_to_image(glyphList) + self.svgcount += 1 + result.append(('svg', num)) + gl_first = -1 + gl_last = -1 + + if (ws_first != -1) and (ws_last != -1): + for wordnum in xrange(ws_first, ws_last): + result.append(('ocr', wordnum)) + ws_first = -1 + ws_last = -1 + + line += 1 + + return pclass, result + + + def buildParagraph(self, pclass, pdesc, type, regtype) : + parares = '' + sep ='' + + classres = '' + if pclass : + classres = ' class="' + pclass + '"' + + br_lb = (regtype == 'fixed') or (regtype == 'chapterheading') or (regtype == 'vertical') + + handle_links = len(self.link_id) > 0 + + if (type == 'full') or (type == 'begin') : + parares += '' + + if (type == 'end'): + parares += ' ' + + lstart = len(parares) + + cnt = len(pdesc) + + for j in xrange( 0, cnt) : + + (wtype, num) = pdesc[j] + + if wtype == 'ocr' : + try: + word = self.ocrtext[num] + except: + word = "" + + sep = ' ' + + if handle_links: + link = self.link_id[num] + if (link > 0): + linktype = self.link_type[link-1] + title = self.link_title[link-1] + if (title == "") or (parares.rfind(title) < 0): + title=parares[lstart:] + if linktype == 'external' : + linkhref = self.link_href[link-1] + linkhtml = '' % linkhref + else : + if len(self.link_page) >= link : + ptarget = self.link_page[link-1] - 1 + linkhtml = '' % ptarget + else : + # just link to the current page + linkhtml = '' + linkhtml += title + '' + pos = parares.rfind(title) + if pos >= 0: + parares = parares[0:pos] + linkhtml + parares[pos+len(title):] + else : + parares += linkhtml + lstart = len(parares) + if word == '_link_' : word = '' + elif (link < 0) : + if word == '_link_' : word = '' + + if word == '_lb_': + if ((num-1) in self.dehyphen_rootid ) or handle_links: + word = '' + sep = '' + elif br_lb : + word = '
\n' + sep = '' + else : + word = '\n' + sep = '' + + if num in self.dehyphen_rootid : + word = word[0:-1] + sep = '' + + parares += word + sep + + elif wtype == 'img' : + sep = '' + parares += '' % num + parares += sep + + elif wtype == 'imgsa' : + sep = ' ' + parares += '' % num + parares += sep + + elif wtype == 'svg' : + sep = '' + parares += '' % num + parares += sep + + if len(sep) > 0 : parares = parares[0:-1] + if (type == 'full') or (type == 'end') : + parares += '

' + return parares + + + def buildTOCEntry(self, pdesc) : + parares = '' + sep ='' + tocentry = '' + handle_links = len(self.link_id) > 0 + + lstart = 0 + + cnt = len(pdesc) + for j in xrange( 0, cnt) : + + (wtype, num) = pdesc[j] + + if wtype == 'ocr' : + word = self.ocrtext[num] + sep = ' ' + + if handle_links: + link = self.link_id[num] + if (link > 0): + linktype = self.link_type[link-1] + title = self.link_title[link-1] + title = title.rstrip('. ') + alt_title = parares[lstart:] + alt_title = alt_title.strip() + # now strip off the actual printed page number + alt_title = alt_title.rstrip('01234567890ivxldIVXLD-.') + alt_title = alt_title.rstrip('. ') + # skip over any external links - can't have them in a books toc + if linktype == 'external' : + title = '' + alt_title = '' + linkpage = '' + else : + if len(self.link_page) >= link : + ptarget = self.link_page[link-1] - 1 + linkpage = '%04d' % ptarget + else : + # just link to the current page + linkpage = self.id[4:] + if len(alt_title) >= len(title): + title = alt_title + if title != '' and linkpage != '': + tocentry += title + '|' + linkpage + '\n' + lstart = len(parares) + if word == '_link_' : word = '' + elif (link < 0) : + if word == '_link_' : word = '' + + if word == '_lb_': + word = '' + sep = '' + + if num in self.dehyphen_rootid : + word = word[0:-1] + sep = '' + + parares += word + sep + + else : + continue + + return tocentry + + + + + # walk the document tree collecting the information needed + # to build an html page using the ocrText + + def process(self): + + tocinfo = '' + hlst = [] + + # get the ocr text + (pos, argres) = self.findinDoc('info.word.ocrText',0,-1) + if argres : self.ocrtext = argres.split('|') + + # get information to dehyphenate the text + self.dehyphen_rootid = self.getData('info.dehyphen.rootID',0,-1) + + # determine if first paragraph is continued from previous page + (pos, self.parastems_stemid) = self.findinDoc('info.paraStems.stemID',0,-1) + first_para_continued = (self.parastems_stemid != None) + + # determine if last paragraph is continued onto the next page + (pos, self.paracont_stemid) = self.findinDoc('info.paraCont.stemID',0,-1) + last_para_continued = (self.paracont_stemid != None) + + # collect link ids + self.link_id = self.getData('info.word.link_id',0,-1) + + # collect link destination page numbers + self.link_page = self.getData('info.links.page',0,-1) + + # collect link types (container versus external) + (pos, argres) = self.findinDoc('info.links.type',0,-1) + if argres : self.link_type = argres.split('|') + + # collect link destinations + (pos, argres) = self.findinDoc('info.links.href',0,-1) + if argres : self.link_href = argres.split('|') + + # collect link titles + (pos, argres) = self.findinDoc('info.links.title',0,-1) + if argres : + self.link_title = argres.split('|') else: - mlst.append('\n') + self.link_title.append('') - mlst.append('' % (pp.pw, pp.ph)) - if (pp.gid != None): - mlst.append('\n') - gdefs = pp.getGlyphs() - for j in xrange(0,len(gdefs)): - mlst.append(gdefs[j]) - mlst.append('\n') - img = pp.getImages() - if (img != None): - for j in xrange(0,len(img)): - mlst.append(img[j]) - if (pp.gid != None): - for j in xrange(0,len(pp.gid)): - mlst.append('\n' % (pp.gid[j], pp.gx[j], pp.gy[j])) - if (img == None or len(img) == 0) and (pp.gid == None or len(pp.gid) == 0): - xpos = "%d" % (pp.pw // 3) - ypos = "%d" % (pp.ph // 3) - mlst.append('This page intentionally left blank.\n') - if (raw) : - mlst.append('') - else : - mlst.append('\n') - if nextid == None: - mlst.append('\n') - else : - mlst.append('\n') - mlst.append('
\n') - mlst.append('\n') - mlst.append('\n') - mlst.append('\n') - return "".join(mlst) + # get a descriptions of the starting points of the regions + # and groups on the page + (pagetype, pageDesc) = self.PageDescription() + regcnt = len(pageDesc) - 1 + + anchorSet = False + breakSet = False + inGroup = False + + # process each region on the page and convert what you can to html + + for j in xrange(regcnt): + + (etype, start) = pageDesc[j] + (ntype, end) = pageDesc[j+1] + + + # set anchor for link target on this page + if not anchorSet and not first_para_continued: + hlst.append('\n') + anchorSet = True + + # handle groups of graphics with text captions + if (etype == 'grpbeg'): + (pos, grptype) = self.findinDoc('group.type', start, end) + if grptype != None: + if grptype == 'graphic': + gcstr = ' class="' + grptype + '"' + hlst.append('') + inGroup = True + + elif (etype == 'grpend'): + if inGroup: + hlst.append('\n') + inGroup = False + + else: + (pos, regtype) = self.findinDoc('region.type',start,end) + + if regtype == 'graphic' : + (pos, simgsrc) = self.findinDoc('img.src',start,end) + if simgsrc: + if inGroup: + hlst.append('' % int(simgsrc)) + else: + hlst.append('
' % int(simgsrc)) + + elif regtype == 'chapterheading' : + (pclass, pdesc) = self.getParaDescription(start,end, regtype) + if not breakSet: + hlst.append('
 
\n') + breakSet = True + tag = 'h1' + if pclass and (len(pclass) >= 7): + if pclass[3:7] == 'ch1-' : tag = 'h1' + if pclass[3:7] == 'ch2-' : tag = 'h2' + if pclass[3:7] == 'ch3-' : tag = 'h3' + hlst.append('<' + tag + ' class="' + pclass + '">') + else: + hlst.append('<' + tag + '>') + hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype)) + hlst.append('') + + elif (regtype == 'text') or (regtype == 'fixed') or (regtype == 'insert') or (regtype == 'listitem'): + ptype = 'full' + # check to see if this is a continution from the previous page + if first_para_continued : + ptype = 'end' + first_para_continued = False + (pclass, pdesc) = self.getParaDescription(start,end, regtype) + if pclass and (len(pclass) >= 6) and (ptype == 'full'): + tag = 'p' + if pclass[3:6] == 'h1-' : tag = 'h4' + if pclass[3:6] == 'h2-' : tag = 'h5' + if pclass[3:6] == 'h3-' : tag = 'h6' + hlst.append('<' + tag + ' class="' + pclass + '">') + hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype)) + hlst.append('') + else : + hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype)) + + elif (regtype == 'tocentry') : + ptype = 'full' + if first_para_continued : + ptype = 'end' + first_para_continued = False + (pclass, pdesc) = self.getParaDescription(start,end, regtype) + tocinfo += self.buildTOCEntry(pdesc) + hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype)) + + elif (regtype == 'vertical') or (regtype == 'table') : + ptype = 'full' + if inGroup: + ptype = 'middle' + if first_para_continued : + ptype = 'end' + first_para_continued = False + (pclass, pdesc) = self.getParaDescription(start, end, regtype) + hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype)) + + + elif (regtype == 'synth_fcvr.center'): + (pos, simgsrc) = self.findinDoc('img.src',start,end) + if simgsrc: + hlst.append('
' % int(simgsrc)) + + else : + print ' Making region type', regtype, + (pos, temp) = self.findinDoc('paragraph',start,end) + (pos2, temp) = self.findinDoc('span',start,end) + if pos != -1 or pos2 != -1: + print ' a "text" region' + orig_regtype = regtype + regtype = 'fixed' + ptype = 'full' + # check to see if this is a continution from the previous page + if first_para_continued : + ptype = 'end' + first_para_continued = False + (pclass, pdesc) = self.getParaDescription(start,end, regtype) + if not pclass: + if orig_regtype.endswith('.right') : pclass = 'cl-right' + elif orig_regtype.endswith('.center') : pclass = 'cl-center' + elif orig_regtype.endswith('.left') : pclass = 'cl-left' + elif orig_regtype.endswith('.justify') : pclass = 'cl-justify' + if pclass and (ptype == 'full') and (len(pclass) >= 6): + tag = 'p' + if pclass[3:6] == 'h1-' : tag = 'h4' + if pclass[3:6] == 'h2-' : tag = 'h5' + if pclass[3:6] == 'h3-' : tag = 'h6' + hlst.append('<' + tag + ' class="' + pclass + '">') + hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype)) + hlst.append('') + else : + hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype)) + else : + print ' a "graphic" region' + (pos, simgsrc) = self.findinDoc('img.src',start,end) + if simgsrc: + hlst.append('
' % int(simgsrc)) + + + htmlpage = "".join(hlst) + if last_para_continued : + if htmlpage[-4:] == '

': + htmlpage = htmlpage[0:-4] + last_para_continued = False + + return htmlpage, tocinfo + + +def convert2HTML(flatxml, classlst, fileid, bookDir, gdict, fixedimage): + # create a document parser + dp = DocParser(flatxml, classlst, fileid, bookDir, gdict, fixedimage) + htmlpage, tocinfo = dp.process() + return htmlpage, tocinfo diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/flatxml2svg.py b/DeDRM_calibre_plugin/DeDRM_plugin/flatxml2svg.py index 3ed925d..4dfd6c7 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/flatxml2svg.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/flatxml2svg.py @@ -1,148 +1,82 @@ #! /usr/bin/python # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab -class Unbuffered: - def __init__(self, stream): - self.stream = stream - def write(self, data): - self.stream.write(data) - self.stream.flush() - def __getattr__(self, attr): - return getattr(self.stream, attr) - import sys -sys.stdout=Unbuffered(sys.stdout) - import csv import os import getopt from struct import pack from struct import unpack -class TpzDRMError(Exception): - pass - -# local support routines -if 'calibre' in sys.modules: - inCalibre = True -else: - inCalibre = False - -if inCalibre : - from calibre_plugins.dedrm import convert2xml - from calibre_plugins.dedrm import flatxml2html - from calibre_plugins.dedrm import flatxml2svg - from calibre_plugins.dedrm import stylexml2css -else : - import convert2xml - import flatxml2html - import flatxml2svg - import stylexml2css - -# global switch -buildXML = False - -# Get a 7 bit encoded number from a file -def readEncodedNumber(file): - flag = False - c = file.read(1) - if (len(c) == 0): - return None - data = ord(c) - if data == 0xFF: - flag = True - c = file.read(1) - if (len(c) == 0): - return None - data = ord(c) - if data >= 0x80: - datax = (data & 0x7F) - while data >= 0x80 : - c = file.read(1) - if (len(c) == 0): - return None - data = ord(c) - datax = (datax <<7) + (data & 0x7F) - data = datax - if flag: - data = -data - return data - -# Get a length prefixed string from the file -def lengthPrefixString(data): - return encodeNumber(len(data))+data - -def readString(file): - stringLength = readEncodedNumber(file) - if (stringLength == None): - return None - sv = file.read(stringLength) - if (len(sv) != stringLength): - return "" - return unpack(str(stringLength)+"s",sv)[0] - -def getMetaArray(metaFile): - # parse the meta file - result = {} - fo = file(metaFile,'rb') - size = readEncodedNumber(fo) - for i in xrange(size): - tag = readString(fo) - value = readString(fo) - result[tag] = value - # print tag, value - fo.close() - return result - - -# dictionary of all text strings by index value -class Dictionary(object): - def __init__(self, dictFile): - self.filename = dictFile - self.size = 0 - self.fo = file(dictFile,'rb') - self.stable = [] - self.size = readEncodedNumber(self.fo) - for i in xrange(self.size): - self.stable.append(self.escapestr(readString(self.fo))) - self.pos = 0 - def escapestr(self, str): - str = str.replace('&','&') - str = str.replace('<','<') - str = str.replace('>','>') - str = str.replace('=','=') - return str - def lookup(self,val): - if ((val >= 0) and (val < self.size)) : - self.pos = val - return self.stable[self.pos] - else: - print "Error: %d outside of string table limits" % val - raise TpzDRMError('outside or string table limits') - # sys.exit(-1) - def getSize(self): - return self.size - def getPos(self): - return self.pos - -class PageDimParser(object): - def __init__(self, flatxml): +class PParser(object): + def __init__(self, gd, flatxml, meta_array): + self.gd = gd self.flatdoc = flatxml.split('\n') - # find tag if within pos to end inclusive + self.docSize = len(self.flatdoc) + self.temp = [] + + self.ph = -1 + self.pw = -1 + startpos = self.posinDoc('page.h') or self.posinDoc('book.h') + for p in startpos: + (name, argres) = self.lineinDoc(p) + self.ph = max(self.ph, int(argres)) + startpos = self.posinDoc('page.w') or self.posinDoc('book.w') + for p in startpos: + (name, argres) = self.lineinDoc(p) + self.pw = max(self.pw, int(argres)) + + if self.ph <= 0: + self.ph = int(meta_array.get('pageHeight', '11000')) + if self.pw <= 0: + self.pw = int(meta_array.get('pageWidth', '8500')) + + res = [] + startpos = self.posinDoc('info.glyph.x') + for p in startpos: + argres = self.getDataatPos('info.glyph.x', p) + res.extend(argres) + self.gx = res + + res = [] + startpos = self.posinDoc('info.glyph.y') + for p in startpos: + argres = self.getDataatPos('info.glyph.y', p) + res.extend(argres) + self.gy = res + + res = [] + startpos = self.posinDoc('info.glyph.glyphID') + for p in startpos: + argres = self.getDataatPos('info.glyph.glyphID', p) + res.extend(argres) + self.gid = res + + + # return tag at line pos in document + def lineinDoc(self, pos) : + if (pos >= 0) and (pos < self.docSize) : + item = self.flatdoc[pos] + if item.find('=') >= 0: + (name, argres) = item.split('=',1) + else : + name = item + argres = '' + return name, argres + + # find tag in doc if within pos to end inclusive def findinDoc(self, tagpath, pos, end) : result = None - docList = self.flatdoc - cnt = len(docList) if end == -1 : - end = cnt + end = self.docSize else: - end = min(cnt,end) + end = min(self.docSize, end) foundat = -1 for j in xrange(pos, end): - item = docList[j] + item = self.flatdoc[j] if item.find('=') >= 0: - (name, argres) = item.split('=') + (name, argres) = item.split('=',1) else : name = item argres = '' @@ -151,44 +85,19 @@ class PageDimParser(object): 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 + # 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 -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) @@ -200,522 +109,141 @@ class GParser(object): else: name = item argres = [] - if (name == path): + 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 getGlyphDim(self, gly): - if self.gdpi[gly] == 0: - return 0, 0 - maxh = (self.gh[gly] * self.dpi) / self.gdpi[gly] - maxw = (self.gw[gly] * self.dpi) / self.gdpi[gly] - return maxh, maxw - def getPath(self, gly): - path = '' - if (gly < 0) or (gly >= self.count): - return path - tx = self.vx[self.gvtx[gly]:self.gvtx[gly+1]] - ty = self.vy[self.gvtx[gly]:self.gvtx[gly+1]] - p = 0 - for k in xrange(self.glen[gly], self.glen[gly+1]): - if (p == 0): - zx = tx[0:self.vlen[k]+1] - zy = ty[0:self.vlen[k]+1] - else: - zx = tx[self.vlen[k-1]+1:self.vlen[k]+1] - zy = ty[self.vlen[k-1]+1:self.vlen[k]+1] - p += 1 - j = 0 - while ( j < len(zx) ): - if (j == 0): - # Start Position. - path += 'M %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly]) - elif (j <= len(zx)-3): - # Cubic Bezier Curve - path += 'C %d %d %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[j+1] * self.dpi / self.gdpi[gly], zy[j+1] * self.dpi / self.gdpi[gly], zx[j+2] * self.dpi / self.gdpi[gly], zy[j+2] * self.dpi / self.gdpi[gly]) - j += 2 - elif (j == len(zx)-2): - # Cubic Bezier Curve to Start Position - path += 'C %d %d %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[j+1] * self.dpi / self.gdpi[gly], zy[j+1] * self.dpi / self.gdpi[gly], zx[0] * self.dpi / self.gdpi[gly], zy[0] * self.dpi / self.gdpi[gly]) - j += 1 - elif (j == len(zx)-1): - # Quadratic Bezier Curve to Start Position - path += 'Q %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[0] * self.dpi / self.gdpi[gly], zy[0] * self.dpi / self.gdpi[gly]) - - j += 1 - path += 'z' - return path - - - -# dictionary of all text strings by index value -class GlyphDict(object): - def __init__(self): - self.gdict = {} - def lookup(self, id): - # id='id="gl%d"' % val - if id in self.gdict: - return self.gdict[id] - return None - def addGlyph(self, val, path): - id='id="gl%d"' % val - self.gdict[id] = path - -def generateBook(bookDir, raw, fixedimage): - # sanity check Topaz file extraction - if not os.path.exists(bookDir) : - print "Can not find directory with unencrypted book" - return 1 - - dictFile = os.path.join(bookDir,'dict0000.dat') - if not os.path.exists(dictFile) : - print "Can not find dict0000.dat file" - return 1 - - pageDir = os.path.join(bookDir,'page') - if not os.path.exists(pageDir) : - print "Can not find page directory in unencrypted book" - return 1 - - imgDir = os.path.join(bookDir,'img') - if not os.path.exists(imgDir) : - print "Can not find image directory in unencrypted book" - return 1 - - glyphsDir = os.path.join(bookDir,'glyphs') - if not os.path.exists(glyphsDir) : - print "Can not find glyphs directory in unencrypted book" - return 1 - - metaFile = os.path.join(bookDir,'metadata0000.dat') - if not os.path.exists(metaFile) : - print "Can not find metadata0000.dat in unencrypted book" - return 1 - - svgDir = os.path.join(bookDir,'svg') - if not os.path.exists(svgDir) : - os.makedirs(svgDir) - - if buildXML: - xmlDir = os.path.join(bookDir,'xml') - if not os.path.exists(xmlDir) : - os.makedirs(xmlDir) - - otherFile = os.path.join(bookDir,'other0000.dat') - if not os.path.exists(otherFile) : - print "Can not find other0000.dat in unencrypted book" - return 1 - - print "Updating to color images if available" - spath = os.path.join(bookDir,'color_img') - dpath = os.path.join(bookDir,'img') - filenames = os.listdir(spath) - filenames = sorted(filenames) - for filename in filenames: - imgname = filename.replace('color','img') - sfile = os.path.join(spath,filename) - dfile = os.path.join(dpath,imgname) - imgdata = file(sfile,'rb').read() - file(dfile,'wb').write(imgdata) - - print "Creating cover.jpg" - isCover = False - cpath = os.path.join(bookDir,'img') - cpath = os.path.join(cpath,'img0000.jpg') - if os.path.isfile(cpath): - cover = file(cpath, 'rb').read() - cpath = os.path.join(bookDir,'cover.jpg') - file(cpath, 'wb').write(cover) - isCover = True - - - print 'Processing Dictionary' - dict = Dictionary(dictFile) - - print 'Processing Meta Data and creating OPF' - meta_array = getMetaArray(metaFile) - - # replace special chars in title and authors like & < > - title = meta_array.get('Title','No Title Provided') - title = title.replace('&','&') - title = title.replace('<','<') - title = title.replace('>','>') - meta_array['Title'] = title - authors = meta_array.get('Authors','No Authors Provided') - authors = authors.replace('&','&') - authors = authors.replace('<','<') - authors = authors.replace('>','>') - meta_array['Authors'] = authors - - if buildXML: - xname = os.path.join(xmlDir, 'metadata.xml') - mlst = [] - for key in meta_array: - mlst.append('\n') - metastr = "".join(mlst) - mlst = None - file(xname, 'wb').write(metastr) - - print 'Processing StyleSheet' - - # get some scaling info from metadata to use while processing styles - # and first page info - - fontsize = '135' - if 'fontSize' in meta_array: - fontsize = meta_array['fontSize'] - - # also get the size of a normal text page - # get the total number of pages unpacked as a safety check - filenames = os.listdir(pageDir) - numfiles = len(filenames) - - spage = '1' - if 'firstTextPage' in meta_array: - spage = meta_array['firstTextPage'] - pnum = int(spage) - if pnum >= numfiles or pnum < 0: - # metadata is wrong so just select a page near the front - # 10% of the book to get a normal text page - pnum = int(0.10 * numfiles) - # print "first normal text page is", spage - - # get page height and width from first text page for use in stylesheet scaling - pname = 'page%04d.dat' % (pnum + 1) - fname = os.path.join(pageDir,pname) - flat_xml = convert2xml.fromData(dict, fname) - - (ph, pw) = getPageDim(flat_xml) - if (ph == '-1') or (ph == '0') : ph = '11000' - if (pw == '-1') or (pw == '0') : pw = '8500' - meta_array['pageHeight'] = ph - meta_array['pageWidth'] = pw - if 'fontSize' not in meta_array.keys(): - meta_array['fontSize'] = fontsize - - # process other.dat for css info and for map of page files to svg images - # this map is needed because some pages actually are made up of multiple - # pageXXXX.xml files - xname = os.path.join(bookDir, 'style.css') - flat_xml = convert2xml.fromData(dict, otherFile) - - # extract info.original.pid to get original page information - pageIDMap = {} - pageidnums = stylexml2css.getpageIDMap(flat_xml) - if len(pageidnums) == 0: - filenames = os.listdir(pageDir) - numfiles = len(filenames) - for k in range(numfiles): - pageidnums.append(k) - # create a map from page ids to list of page file nums to process for that page - for i in range(len(pageidnums)): - id = pageidnums[i] - if id in pageIDMap.keys(): - pageIDMap[id].append(i) + def getDataatPos(self, path, pos): + result = None + item = self.flatdoc[pos] + if item.find('=') >= 0: + (name, argt) = item.split('=') + argres = argt.split('|') else: - pageIDMap[id] = [i] - - # now get the css info - cssstr , classlst = stylexml2css.convert2CSS(flat_xml, fontsize, ph, pw) - file(xname, 'wb').write(cssstr) - if buildXML: - xname = os.path.join(xmlDir, 'other0000.xml') - file(xname, 'wb').write(convert2xml.getXML(dict, otherFile)) - - print 'Processing Glyphs' - gd = GlyphDict() - filenames = os.listdir(glyphsDir) - filenames = sorted(filenames) - glyfname = os.path.join(svgDir,'glyphs.svg') - glyfile = open(glyfname, 'w') - glyfile.write('\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) - - if buildXML: - xname = os.path.join(xmlDir, filename.replace('.dat','.xml')) - file(xname, 'wb').write(convert2xml.getXML(dict, fname)) - - gp = GParser(flat_xml) - for i in xrange(0, gp.count): - path = gp.getPath(i) - maxh, maxw = gp.getGlyphDim(i) - fullpath = '\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 - # also build up tocentries while processing html - htmlFileName = "book.html" - hlst = [] - hlst.append('\n') - hlst.append('\n') - hlst.append('\n') - hlst.append('\n') - hlst.append('\n') - hlst.append('' + meta_array['Title'] + ' by ' + meta_array['Authors'] + '\n') - hlst.append('\n') - hlst.append('\n') - if 'ASIN' in meta_array: - hlst.append('\n') - if 'GUID' in meta_array: - hlst.append('\n') - hlst.append('\n') - hlst.append('\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 - - filenames = os.listdir(pageDir) - filenames = sorted(filenames) - numfiles = len(filenames) - - xmllst = [] - elst = [] - - for filename in filenames: - # print ' ', filename - print ".", - fname = os.path.join(pageDir,filename) - flat_xml = convert2xml.fromData(dict, fname) - - # keep flat_xml for later svg processing - xmllst.append(flat_xml) - - if buildXML: - xname = os.path.join(xmlDir, filename.replace('.dat','.xml')) - file(xname, 'wb').write(convert2xml.getXML(dict, fname)) + name = item + argres = [] + if (len(argres) > 0) : + for j in xrange(0,len(argres)): + argres[j] = int(argres[j]) + if (name.endswith(path)): + result = argres + return result - # first get the html - pagehtml, tocinfo = flatxml2html.convert2HTML(flat_xml, classlst, fname, bookDir, gd, fixedimage) - elst.append(tocinfo) - hlst.append(pagehtml) + 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 - # finish up the html string and output it - hlst.append('\n\n') - htmlstr = "".join(hlst) - hlst = None - file(os.path.join(bookDir, htmlFileName), 'wb').write(htmlstr) + 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 - print " " - print 'Extracting Table of Contents from Amazon OCR' + 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 - # first create a table of contents file for the svg images - tlst = [] - tlst.append('\n') - tlst.append('\n') - tlst.append('') - tlst.append('\n') - tlst.append('' + meta_array['Title'] + '\n') - tlst.append('\n') - tlst.append('\n') - if 'ASIN' in meta_array: - tlst.append('\n') - if 'GUID' in meta_array: - tlst.append('\n') - tlst.append('\n') - tlst.append('\n') - tlst.append('

Table of Contents

\n') - start = pageidnums[0] +def convert2SVG(gdict, flat_xml, pageid, previd, nextid, svgDir, raw, meta_array, scaledpi): + mlst = [] + pp = PParser(gdict, flat_xml, meta_array) + mlst.append('\n') if (raw): - startname = 'page%04d.svg' % start + mlst.append('\n') + mlst.append('\n' % (pp.pw / scaledpi, pp.ph / scaledpi, pp.pw -1, pp.ph -1)) + mlst.append('Page %d - %s by %s\n' % (pageid, meta_array['Title'],meta_array['Authors'])) else: - startname = 'page%04d.xhtml' % start - - tlst.append('

Start of Book

\n') - # build up a table of contents for the svg xhtml output - tocentries = "".join(elst) - elst = None - toclst = tocentries.split('\n') - toclst.pop() - for entry in toclst: - print entry - title, pagenum = entry.split('|') - id = pageidnums[int(pagenum)] - if (raw): - fname = 'page%04d.svg' % id + mlst.append('\n') + mlst.append('\n') + mlst.append('Page %d - %s by %s\n' % (pageid, meta_array['Title'],meta_array['Authors'])) + mlst.append('\n') + mlst.append('\n') + mlst.append('\n') + mlst.append('
\n') + if previd == None: + mlst.append('\n') else: - fname = 'page%04d.xhtml' % id - tlst.append('

' + title + '

\n') - tlst.append('\n') - tlst.append('\n') - tochtml = "".join(tlst) - file(os.path.join(svgDir, 'toc.xhtml'), 'wb').write(tochtml) - - - # now create index_svg.xhtml that points to all required files - slst = [] - slst.append('\n') - slst.append('\n') - slst.append('') - slst.append('\n') - slst.append('' + meta_array['Title'] + '\n') - slst.append('\n') - slst.append('\n') - if 'ASIN' in meta_array: - slst.append('\n') - if 'GUID' in meta_array: - slst.append('\n') - slst.append('\n') - slst.append('\n') - - print "Building svg images of each book page" - slst.append('

List of Pages

\n') - slst.append('
\n') - idlst = sorted(pageIDMap.keys()) - numids = len(idlst) - cnt = len(idlst) - previd = None - for j in range(cnt): - pageid = idlst[j] - if j < cnt - 1: - nextid = idlst[j+1] - else: - nextid = None - print '.', - pagelst = pageIDMap[pageid] - flst = [] - for page in pagelst: - flst.append(xmllst[page]) - flat_svg = "".join(flst) - flst=None - svgxml = flatxml2svg.convert2SVG(gd, flat_svg, pageid, previd, nextid, svgDir, raw, meta_array, scaledpi) - if (raw) : - pfile = open(os.path.join(svgDir,'page%04d.svg' % pageid),'w') - slst.append('Page %d\n' % (pageid, pageid)) + mlst.append('\n') + + mlst.append('' % (pp.pw, pp.ph)) + if (pp.gid != None): + mlst.append('\n') + gdefs = pp.getGlyphs() + for j in xrange(0,len(gdefs)): + mlst.append(gdefs[j]) + mlst.append('\n') + img = pp.getImages() + if (img != None): + for j in xrange(0,len(img)): + mlst.append(img[j]) + if (pp.gid != None): + for j in xrange(0,len(pp.gid)): + mlst.append('\n' % (pp.gid[j], pp.gx[j], pp.gy[j])) + if (img == None or len(img) == 0) and (pp.gid == None or len(pp.gid) == 0): + xpos = "%d" % (pp.pw // 3) + ypos = "%d" % (pp.ph // 3) + mlst.append('This page intentionally left blank.\n') + if (raw) : + mlst.append('') + else : + mlst.append('\n') + if nextid == None: + mlst.append('\n') else : - pfile = open(os.path.join(svgDir,'page%04d.xhtml' % pageid), 'w') - slst.append('Page %d\n' % (pageid, pageid)) - previd = pageid - pfile.write(svgxml) - pfile.close() - counter += 1 - slst.append('
\n') - slst.append('

Table of Contents

\n') - slst.append('\n\n') - svgindex = "".join(slst) - slst = None - file(os.path.join(bookDir, 'index_svg.xhtml'), 'wb').write(svgindex) - - print " " - - # build the opf file - opfname = os.path.join(bookDir, 'book.opf') - olst = [] - olst.append('\n') - olst.append('\n') - # adding metadata - olst.append(' \n') - if 'GUID' in meta_array: - olst.append(' ' + meta_array['GUID'] + '\n') - if 'ASIN' in meta_array: - olst.append(' ' + meta_array['ASIN'] + '\n') - if 'oASIN' in meta_array: - olst.append(' ' + meta_array['oASIN'] + '\n') - olst.append(' ' + meta_array['Title'] + '\n') - olst.append(' ' + meta_array['Authors'] + '\n') - olst.append(' en\n') - olst.append(' ' + meta_array['UpdateTime'] + '\n') - if isCover: - olst.append(' \n') - olst.append(' \n') - olst.append('\n') - olst.append(' \n') - olst.append(' \n') - # adding image files to manifest - filenames = os.listdir(imgDir) - filenames = sorted(filenames) - for filename in filenames: - imgname, imgext = os.path.splitext(filename) - if imgext == '.jpg': - imgext = 'jpeg' - if imgext == '.svg': - imgext = 'svg+xml' - olst.append(' \n') - if isCover: - olst.append(' \n') - olst.append('\n') - # adding spine - olst.append('\n \n\n') - if isCover: - olst.append(' \n') - olst.append(' \n') - olst.append(' \n') - olst.append('\n') - opfstr = "".join(olst) - olst = None - file(opfname, 'wb').write(opfstr) - - print 'Processing Complete' - - return 0 - -def usage(): - print "genbook.py generates a book from the extract Topaz Files" - print "Usage:" - print " genbook.py [-r] [-h [--fixed-image] " - print " " - print "Options:" - print " -h : help - print this usage message" - print " -r : generate raw svg files (not wrapped in xhtml)" - print " --fixed-image : genearate any Fixed Area as an svg image in the html" - print " " - - -def main(argv): - bookDir = '' - if len(argv) == 0: - argv = sys.argv - - try: - opts, args = getopt.getopt(argv[1:], "rh:",["fixed-image"]) - - except getopt.GetoptError, err: - print str(err) - usage() - return 1 - - if len(opts) == 0 and len(args) == 0 : - usage() - return 1 - - raw = 0 - fixedimage = True - for o, a in opts: - if o =="-h": - usage() - return 0 - if o =="-r": - raw = 1 - if o =="--fixed-image": - fixedimage = True - - bookDir = args[0] - - rv = generateBook(bookDir, raw, fixedimage) - return rv - - -if __name__ == '__main__': - sys.exit(main('')) + mlst.append('\n') + mlst.append('
\n') + mlst.append('\n') + mlst.append('\n') + mlst.append('\n') + return "".join(mlst) diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/genbook.py b/DeDRM_calibre_plugin/DeDRM_plugin/genbook.py index ac73d1e..3ed925d 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/genbook.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/genbook.py @@ -1,452 +1,721 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -# ignobleepub.pyw, version 3.8 -# Copyright © 2009-2010 by i♥cabbages - -# Released under the terms of the GNU General Public Licence, version 3 -# - -# Modified 2010–2013 by some_updates, DiapDealer and Apprentice Alf - -# Windows users: Before running this program, you must first install Python 2.6 -# from and PyCrypto from -# (make sure to -# install the version for Python 2.6). Save this script file as -# ineptepub.pyw and double-click on it to run it. -# -# Mac OS X users: Save this script file as ineptepub.pyw. You can run this -# program from the command line (pythonw ineptepub.pyw) or by double-clicking -# it when it has been associated with PythonLauncher. - -# Revision history: -# 1 - Initial release -# 2 - Added OS X support by using OpenSSL when available -# 3 - screen out improper key lengths to prevent segfaults on Linux -# 3.1 - Allow Windows versions of libcrypto to be found -# 3.2 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml -# 3.3 - On Windows try PyCrypto first, OpenSSL next -# 3.4 - Modify interface to allow use with import -# 3.5 - Fix for potential problem with PyCrypto -# 3.6 - Revised to allow use in calibre plugins to eliminate need for duplicate code -# 3.7 - Tweaked to match ineptepub more closely -# 3.8 - Fixed to retain zip file metadata (e.g. file modification date) -# 3.9 - moved unicode_argv call inside main for Windows DeDRM compatibility -# 4.0 - Work if TkInter is missing - -""" -Decrypt Barnes & Noble encrypted ePub books. -""" - -__license__ = 'GPL v3' -__version__ = "4.0" +#! /usr/bin/python +# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab -import sys -import os -import traceback -import zlib -import zipfile -from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED -from contextlib import closing -import xml.etree.ElementTree as etree - -# Wrap a stream so that output gets flushed immediately -# and also make sure that any unicode strings get -# encoded using "replace" before writing them. -class SafeUnbuffered: +class Unbuffered: def __init__(self, stream): self.stream = stream - self.encoding = stream.encoding - if self.encoding == None: - self.encoding = "utf-8" def write(self, data): - if isinstance(data,unicode): - data = data.encode(self.encoding,"replace") self.stream.write(data) self.stream.flush() def __getattr__(self, attr): return getattr(self.stream, attr) -try: - from calibre.constants import iswindows, isosx -except: - iswindows = sys.platform.startswith('win') - isosx = sys.platform.startswith('darwin') - -def unicode_argv(): - if iswindows: - # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode - # strings. - - # Versions 2.x of Python don't support Unicode in sys.argv on - # Windows, with the underlying Windows API instead replacing multi-byte - # characters with '?'. - - - from ctypes import POINTER, byref, cdll, c_int, windll - from ctypes.wintypes import LPCWSTR, LPWSTR - - GetCommandLineW = cdll.kernel32.GetCommandLineW - GetCommandLineW.argtypes = [] - GetCommandLineW.restype = LPCWSTR - - CommandLineToArgvW = windll.shell32.CommandLineToArgvW - CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] - CommandLineToArgvW.restype = POINTER(LPWSTR) - - cmd = GetCommandLineW() - argc = c_int(0) - argv = CommandLineToArgvW(cmd, byref(argc)) - if argc.value > 0: - # Remove Python executable and commands if present - start = argc.value - len(sys.argv) - return [argv[i] for i in - xrange(start, argc.value)] - return [u"ineptepub.py"] - else: - argvencoding = sys.stdin.encoding - if argvencoding == None: - argvencoding = "utf-8" - return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv] +import sys +sys.stdout=Unbuffered(sys.stdout) +import csv +import os +import getopt +from struct import pack +from struct import unpack -class IGNOBLEError(Exception): +class TpzDRMError(Exception): pass -def _load_crypto_libcrypto(): - from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \ - Structure, c_ulong, create_string_buffer, cast - from ctypes.util import find_library +# local support routines +if 'calibre' in sys.modules: + inCalibre = True +else: + inCalibre = False + +if inCalibre : + from calibre_plugins.dedrm import convert2xml + from calibre_plugins.dedrm import flatxml2html + from calibre_plugins.dedrm import flatxml2svg + from calibre_plugins.dedrm import stylexml2css +else : + import convert2xml + import flatxml2html + import flatxml2svg + import stylexml2css + +# global switch +buildXML = False + +# Get a 7 bit encoded number from a file +def readEncodedNumber(file): + flag = False + c = file.read(1) + if (len(c) == 0): + return None + data = ord(c) + if data == 0xFF: + flag = True + c = file.read(1) + if (len(c) == 0): + return None + data = ord(c) + if data >= 0x80: + datax = (data & 0x7F) + while data >= 0x80 : + c = file.read(1) + if (len(c) == 0): + return None + data = ord(c) + datax = (datax <<7) + (data & 0x7F) + data = datax + if flag: + data = -data + return data + +# Get a length prefixed string from the file +def lengthPrefixString(data): + return encodeNumber(len(data))+data + +def readString(file): + stringLength = readEncodedNumber(file) + if (stringLength == None): + return None + sv = file.read(stringLength) + if (len(sv) != stringLength): + return "" + return unpack(str(stringLength)+"s",sv)[0] + +def getMetaArray(metaFile): + # parse the meta file + result = {} + fo = file(metaFile,'rb') + size = readEncodedNumber(fo) + for i in xrange(size): + tag = readString(fo) + value = readString(fo) + result[tag] = value + # print tag, value + fo.close() + return result + + +# dictionary of all text strings by index value +class Dictionary(object): + def __init__(self, dictFile): + self.filename = dictFile + self.size = 0 + self.fo = file(dictFile,'rb') + self.stable = [] + self.size = readEncodedNumber(self.fo) + for i in xrange(self.size): + self.stable.append(self.escapestr(readString(self.fo))) + self.pos = 0 + def escapestr(self, str): + str = str.replace('&','&') + str = str.replace('<','<') + str = str.replace('>','>') + str = str.replace('=','=') + return str + def lookup(self,val): + if ((val >= 0) and (val < self.size)) : + self.pos = val + return self.stable[self.pos] + else: + print "Error: %d outside of string table limits" % val + raise TpzDRMError('outside or string table limits') + # sys.exit(-1) + def getSize(self): + return self.size + def getPos(self): + return self.pos + + +class PageDimParser(object): + def __init__(self, flatxml): + self.flatdoc = flatxml.split('\n') + # find tag if within pos to end inclusive + def findinDoc(self, tagpath, pos, end) : + result = None + docList = self.flatdoc + cnt = len(docList) + if end == -1 : + end = cnt + else: + end = min(cnt,end) + foundat = -1 + for j in xrange(pos, end): + item = docList[j] + if item.find('=') >= 0: + (name, argres) = item.split('=') + else : + name = item + argres = '' + if name.endswith(tagpath) : + result = argres + foundat = j + break + return foundat, result + def process(self): + (pos, sph) = self.findinDoc('page.h',0,-1) + (pos, spw) = self.findinDoc('page.w',0,-1) + if (sph == None): sph = '-1' + if (spw == None): spw = '-1' + return sph, spw + +def getPageDim(flatxml): + # create a document parser + dp = PageDimParser(flatxml) + (ph, pw) = dp.process() + return ph, pw + +class GParser(object): + def __init__(self, flatxml): + self.flatdoc = flatxml.split('\n') + self.dpi = 1440 + self.gh = self.getData('info.glyph.h') + self.gw = self.getData('info.glyph.w') + self.guse = self.getData('info.glyph.use') + if self.guse : + self.count = len(self.guse) + else : + self.count = 0 + self.gvtx = self.getData('info.glyph.vtx') + self.glen = self.getData('info.glyph.len') + self.gdpi = self.getData('info.glyph.dpi') + self.vx = self.getData('info.vtx.x') + self.vy = self.getData('info.vtx.y') + self.vlen = self.getData('info.len.n') + if self.vlen : + self.glen.append(len(self.vlen)) + elif self.glen: + self.glen.append(0) + if self.vx : + self.gvtx.append(len(self.vx)) + elif self.gvtx : + self.gvtx.append(0) + def getData(self, path): + result = None + cnt = len(self.flatdoc) + for j in xrange(cnt): + item = self.flatdoc[j] + if item.find('=') >= 0: + (name, argt) = item.split('=') + argres = argt.split('|') + else: + name = item + argres = [] + if (name == path): + result = argres + break + if (len(argres) > 0) : + for j in xrange(0,len(argres)): + argres[j] = int(argres[j]) + return result + def getGlyphDim(self, gly): + if self.gdpi[gly] == 0: + return 0, 0 + maxh = (self.gh[gly] * self.dpi) / self.gdpi[gly] + maxw = (self.gw[gly] * self.dpi) / self.gdpi[gly] + return maxh, maxw + def getPath(self, gly): + path = '' + if (gly < 0) or (gly >= self.count): + return path + tx = self.vx[self.gvtx[gly]:self.gvtx[gly+1]] + ty = self.vy[self.gvtx[gly]:self.gvtx[gly+1]] + p = 0 + for k in xrange(self.glen[gly], self.glen[gly+1]): + if (p == 0): + zx = tx[0:self.vlen[k]+1] + zy = ty[0:self.vlen[k]+1] + else: + zx = tx[self.vlen[k-1]+1:self.vlen[k]+1] + zy = ty[self.vlen[k-1]+1:self.vlen[k]+1] + p += 1 + j = 0 + while ( j < len(zx) ): + if (j == 0): + # Start Position. + path += 'M %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly]) + elif (j <= len(zx)-3): + # Cubic Bezier Curve + path += 'C %d %d %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[j+1] * self.dpi / self.gdpi[gly], zy[j+1] * self.dpi / self.gdpi[gly], zx[j+2] * self.dpi / self.gdpi[gly], zy[j+2] * self.dpi / self.gdpi[gly]) + j += 2 + elif (j == len(zx)-2): + # Cubic Bezier Curve to Start Position + path += 'C %d %d %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[j+1] * self.dpi / self.gdpi[gly], zy[j+1] * self.dpi / self.gdpi[gly], zx[0] * self.dpi / self.gdpi[gly], zy[0] * self.dpi / self.gdpi[gly]) + j += 1 + elif (j == len(zx)-1): + # Quadratic Bezier Curve to Start Position + path += 'Q %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[0] * self.dpi / self.gdpi[gly], zy[0] * self.dpi / self.gdpi[gly]) + + j += 1 + path += 'z' + return path + + + +# dictionary of all text strings by index value +class GlyphDict(object): + def __init__(self): + self.gdict = {} + def lookup(self, id): + # id='id="gl%d"' % val + if id in self.gdict: + return self.gdict[id] + return None + def addGlyph(self, val, path): + id='id="gl%d"' % val + self.gdict[id] = path + + +def generateBook(bookDir, raw, fixedimage): + # sanity check Topaz file extraction + if not os.path.exists(bookDir) : + print "Can not find directory with unencrypted book" + return 1 + + dictFile = os.path.join(bookDir,'dict0000.dat') + if not os.path.exists(dictFile) : + print "Can not find dict0000.dat file" + return 1 + + pageDir = os.path.join(bookDir,'page') + if not os.path.exists(pageDir) : + print "Can not find page directory in unencrypted book" + return 1 + + imgDir = os.path.join(bookDir,'img') + if not os.path.exists(imgDir) : + print "Can not find image directory in unencrypted book" + return 1 - if iswindows: - libcrypto = find_library('libeay32') + glyphsDir = os.path.join(bookDir,'glyphs') + if not os.path.exists(glyphsDir) : + print "Can not find glyphs directory in unencrypted book" + return 1 + + metaFile = os.path.join(bookDir,'metadata0000.dat') + if not os.path.exists(metaFile) : + print "Can not find metadata0000.dat in unencrypted book" + return 1 + + svgDir = os.path.join(bookDir,'svg') + if not os.path.exists(svgDir) : + os.makedirs(svgDir) + + if buildXML: + xmlDir = os.path.join(bookDir,'xml') + if not os.path.exists(xmlDir) : + os.makedirs(xmlDir) + + otherFile = os.path.join(bookDir,'other0000.dat') + if not os.path.exists(otherFile) : + print "Can not find other0000.dat in unencrypted book" + return 1 + + print "Updating to color images if available" + spath = os.path.join(bookDir,'color_img') + dpath = os.path.join(bookDir,'img') + filenames = os.listdir(spath) + filenames = sorted(filenames) + for filename in filenames: + imgname = filename.replace('color','img') + sfile = os.path.join(spath,filename) + dfile = os.path.join(dpath,imgname) + imgdata = file(sfile,'rb').read() + file(dfile,'wb').write(imgdata) + + print "Creating cover.jpg" + isCover = False + cpath = os.path.join(bookDir,'img') + cpath = os.path.join(cpath,'img0000.jpg') + if os.path.isfile(cpath): + cover = file(cpath, 'rb').read() + cpath = os.path.join(bookDir,'cover.jpg') + file(cpath, 'wb').write(cover) + isCover = True + + + print 'Processing Dictionary' + dict = Dictionary(dictFile) + + print 'Processing Meta Data and creating OPF' + meta_array = getMetaArray(metaFile) + + # replace special chars in title and authors like & < > + title = meta_array.get('Title','No Title Provided') + title = title.replace('&','&') + title = title.replace('<','<') + title = title.replace('>','>') + meta_array['Title'] = title + authors = meta_array.get('Authors','No Authors Provided') + authors = authors.replace('&','&') + authors = authors.replace('<','<') + authors = authors.replace('>','>') + meta_array['Authors'] = authors + + if buildXML: + xname = os.path.join(xmlDir, 'metadata.xml') + mlst = [] + for key in meta_array: + mlst.append('\n') + metastr = "".join(mlst) + mlst = None + file(xname, 'wb').write(metastr) + + print 'Processing StyleSheet' + + # get some scaling info from metadata to use while processing styles + # and first page info + + fontsize = '135' + if 'fontSize' in meta_array: + fontsize = meta_array['fontSize'] + + # also get the size of a normal text page + # get the total number of pages unpacked as a safety check + filenames = os.listdir(pageDir) + numfiles = len(filenames) + + spage = '1' + if 'firstTextPage' in meta_array: + spage = meta_array['firstTextPage'] + pnum = int(spage) + if pnum >= numfiles or pnum < 0: + # metadata is wrong so just select a page near the front + # 10% of the book to get a normal text page + pnum = int(0.10 * numfiles) + # print "first normal text page is", spage + + # get page height and width from first text page for use in stylesheet scaling + pname = 'page%04d.dat' % (pnum + 1) + fname = os.path.join(pageDir,pname) + flat_xml = convert2xml.fromData(dict, fname) + + (ph, pw) = getPageDim(flat_xml) + if (ph == '-1') or (ph == '0') : ph = '11000' + if (pw == '-1') or (pw == '0') : pw = '8500' + meta_array['pageHeight'] = ph + meta_array['pageWidth'] = pw + if 'fontSize' not in meta_array.keys(): + meta_array['fontSize'] = fontsize + + # process other.dat for css info and for map of page files to svg images + # this map is needed because some pages actually are made up of multiple + # pageXXXX.xml files + xname = os.path.join(bookDir, 'style.css') + flat_xml = convert2xml.fromData(dict, otherFile) + + # extract info.original.pid to get original page information + pageIDMap = {} + pageidnums = stylexml2css.getpageIDMap(flat_xml) + if len(pageidnums) == 0: + filenames = os.listdir(pageDir) + numfiles = len(filenames) + for k in range(numfiles): + pageidnums.append(k) + # create a map from page ids to list of page file nums to process for that page + for i in range(len(pageidnums)): + id = pageidnums[i] + if id in pageIDMap.keys(): + pageIDMap[id].append(i) + else: + pageIDMap[id] = [i] + + # now get the css info + cssstr , classlst = stylexml2css.convert2CSS(flat_xml, fontsize, ph, pw) + file(xname, 'wb').write(cssstr) + if buildXML: + xname = os.path.join(xmlDir, 'other0000.xml') + file(xname, 'wb').write(convert2xml.getXML(dict, otherFile)) + + print 'Processing Glyphs' + gd = GlyphDict() + filenames = os.listdir(glyphsDir) + filenames = sorted(filenames) + glyfname = os.path.join(svgDir,'glyphs.svg') + glyfile = open(glyfname, 'w') + glyfile.write('\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) + + if buildXML: + xname = os.path.join(xmlDir, filename.replace('.dat','.xml')) + file(xname, 'wb').write(convert2xml.getXML(dict, fname)) + + gp = GParser(flat_xml) + for i in xrange(0, gp.count): + path = gp.getPath(i) + maxh, maxw = gp.getGlyphDim(i) + fullpath = '\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 + # also build up tocentries while processing html + htmlFileName = "book.html" + hlst = [] + hlst.append('\n') + hlst.append('\n') + hlst.append('\n') + hlst.append('\n') + hlst.append('\n') + hlst.append('' + meta_array['Title'] + ' by ' + meta_array['Authors'] + '\n') + hlst.append('\n') + hlst.append('\n') + if 'ASIN' in meta_array: + hlst.append('\n') + if 'GUID' in meta_array: + hlst.append('\n') + hlst.append('\n') + hlst.append('\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 + + filenames = os.listdir(pageDir) + filenames = sorted(filenames) + numfiles = len(filenames) + + xmllst = [] + elst = [] + + for filename in filenames: + # print ' ', filename + print ".", + fname = os.path.join(pageDir,filename) + flat_xml = convert2xml.fromData(dict, fname) + + # keep flat_xml for later svg processing + xmllst.append(flat_xml) + + if buildXML: + xname = os.path.join(xmlDir, filename.replace('.dat','.xml')) + file(xname, 'wb').write(convert2xml.getXML(dict, fname)) + + # first get the html + pagehtml, tocinfo = flatxml2html.convert2HTML(flat_xml, classlst, fname, bookDir, gd, fixedimage) + elst.append(tocinfo) + hlst.append(pagehtml) + + # finish up the html string and output it + hlst.append('\n\n') + htmlstr = "".join(hlst) + hlst = None + file(os.path.join(bookDir, htmlFileName), 'wb').write(htmlstr) + + print " " + print 'Extracting Table of Contents from Amazon OCR' + + # first create a table of contents file for the svg images + tlst = [] + tlst.append('\n') + tlst.append('\n') + tlst.append('') + tlst.append('\n') + tlst.append('' + meta_array['Title'] + '\n') + tlst.append('\n') + tlst.append('\n') + if 'ASIN' in meta_array: + tlst.append('\n') + if 'GUID' in meta_array: + tlst.append('\n') + tlst.append('\n') + tlst.append('\n') + + tlst.append('

Table of Contents

\n') + start = pageidnums[0] + if (raw): + startname = 'page%04d.svg' % start else: - libcrypto = find_library('crypto') - - if libcrypto is None: - raise IGNOBLEError('libcrypto not found') - libcrypto = CDLL(libcrypto) - - AES_MAXNR = 14 - - c_char_pp = POINTER(c_char_p) - c_int_p = POINTER(c_int) - - class AES_KEY(Structure): - _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), - ('rounds', c_int)] - AES_KEY_p = POINTER(AES_KEY) - - def F(restype, name, argtypes): - func = getattr(libcrypto, name) - func.restype = restype - func.argtypes = argtypes - return func - - AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key', - [c_char_p, c_int, AES_KEY_p]) - AES_cbc_encrypt = F(None, 'AES_cbc_encrypt', - [c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p, - c_int]) - - class AES(object): - def __init__(self, userkey): - self._blocksize = len(userkey) - if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : - raise IGNOBLEError('AES improper key used') - return - key = self._key = AES_KEY() - rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key) - if rv < 0: - raise IGNOBLEError('Failed to initialize AES key') - - def decrypt(self, data): - out = create_string_buffer(len(data)) - iv = ("\x00" * self._blocksize) - rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0) - if rv == 0: - raise IGNOBLEError('AES decryption failed') - return out.raw - - return AES - -def _load_crypto_pycrypto(): - from Crypto.Cipher import AES as _AES - - class AES(object): - def __init__(self, key): - self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16) - - def decrypt(self, data): - return self._aes.decrypt(data) - - return AES - -def _load_crypto(): - AES = None - cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto) - if sys.platform.startswith('win'): - cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto) - for loader in cryptolist: - try: - AES = loader() - break - except (ImportError, IGNOBLEError): - pass - return AES - -AES = _load_crypto() - -META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml') -NSMAP = {'adept': 'http://ns.adobe.com/adept', - 'enc': 'http://www.w3.org/2001/04/xmlenc#'} - -class Decryptor(object): - def __init__(self, bookkey, encryption): - enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag) - self._aes = AES(bookkey) - encryption = etree.fromstring(encryption) - self._encrypted = encrypted = set() - expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'), - enc('CipherReference')) - for elem in encryption.findall(expr): - path = elem.get('URI', None) - if path is not None: - path = path.encode('utf-8') - encrypted.add(path) - - def decompress(self, bytes): - dc = zlib.decompressobj(-15) - bytes = dc.decompress(bytes) - ex = dc.decompress('Z') + dc.flush() - if ex: - bytes = bytes + ex - return bytes - - def decrypt(self, path, data): - if path in self._encrypted: - data = self._aes.decrypt(data)[16:] - data = data[:-ord(data[-1])] - data = self.decompress(data) - return data - -# check file to make check whether it's probably an Adobe Adept encrypted ePub -def ignobleBook(inpath): - with closing(ZipFile(open(inpath, 'rb'))) as inf: - namelist = set(inf.namelist()) - if 'META-INF/rights.xml' not in namelist or \ - 'META-INF/encryption.xml' not in namelist: - return False - try: - rights = etree.fromstring(inf.read('META-INF/rights.xml')) - adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) - expr = './/%s' % (adept('encryptedKey'),) - bookkey = ''.join(rights.findtext(expr)) - if len(bookkey) == 64: - return True - except: - # if we couldn't check, assume it is - return True - return False - -def decryptBook(keyb64, inpath, outpath): - if AES is None: - raise IGNOBLEError(u"PyCrypto or OpenSSL must be installed.") - key = keyb64.decode('base64')[:16] - aes = AES(key) - with closing(ZipFile(open(inpath, 'rb'))) as inf: - namelist = set(inf.namelist()) - if 'META-INF/rights.xml' not in namelist or \ - 'META-INF/encryption.xml' not in namelist: - print u"{0:s} is DRM-free.".format(os.path.basename(inpath)) - return 1 - for name in META_NAMES: - namelist.remove(name) - try: - rights = etree.fromstring(inf.read('META-INF/rights.xml')) - adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) - expr = './/%s' % (adept('encryptedKey'),) - bookkey = ''.join(rights.findtext(expr)) - if len(bookkey) != 64: - print u"{0:s} is not a secure Barnes & Noble ePub.".format(os.path.basename(inpath)) - return 1 - bookkey = aes.decrypt(bookkey.decode('base64')) - bookkey = bookkey[:-ord(bookkey[-1])] - encryption = inf.read('META-INF/encryption.xml') - decryptor = Decryptor(bookkey[-16:], encryption) - kwds = dict(compression=ZIP_DEFLATED, allowZip64=False) - with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf: - zi = ZipInfo('mimetype') - zi.compress_type=ZIP_STORED - try: - # if the mimetype is present, get its info, including time-stamp - oldzi = inf.getinfo('mimetype') - # copy across fields to be preserved - zi.date_time = oldzi.date_time - zi.comment = oldzi.comment - zi.extra = oldzi.extra - zi.internal_attr = oldzi.internal_attr - # external attributes are dependent on the create system, so copy both. - zi.external_attr = oldzi.external_attr - zi.create_system = oldzi.create_system - except: - pass - outf.writestr(zi, inf.read('mimetype')) - for path in namelist: - data = inf.read(path) - zi = ZipInfo(path) - zi.compress_type=ZIP_DEFLATED - try: - # get the file info, including time-stamp - oldzi = inf.getinfo(path) - # copy across useful fields - zi.date_time = oldzi.date_time - zi.comment = oldzi.comment - zi.extra = oldzi.extra - zi.internal_attr = oldzi.internal_attr - # external attributes are dependent on the create system, so copy both. - zi.external_attr = oldzi.external_attr - zi.create_system = oldzi.create_system - except: - pass - outf.writestr(zi, decryptor.decrypt(path, data)) - except: - print u"Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc()) - return 2 + startname = 'page%04d.xhtml' % start + + tlst.append('

Start of Book

\n') + # build up a table of contents for the svg xhtml output + tocentries = "".join(elst) + elst = None + toclst = tocentries.split('\n') + toclst.pop() + for entry in toclst: + print entry + title, pagenum = entry.split('|') + id = pageidnums[int(pagenum)] + if (raw): + fname = 'page%04d.svg' % id + else: + fname = 'page%04d.xhtml' % id + tlst.append('

' + title + '

\n') + tlst.append('\n') + tlst.append('\n') + tochtml = "".join(tlst) + file(os.path.join(svgDir, 'toc.xhtml'), 'wb').write(tochtml) + + + # now create index_svg.xhtml that points to all required files + slst = [] + slst.append('\n') + slst.append('\n') + slst.append('') + slst.append('\n') + slst.append('' + meta_array['Title'] + '\n') + slst.append('\n') + slst.append('\n') + if 'ASIN' in meta_array: + slst.append('\n') + if 'GUID' in meta_array: + slst.append('\n') + slst.append('\n') + slst.append('\n') + + print "Building svg images of each book page" + slst.append('

List of Pages

\n') + slst.append('
\n') + idlst = sorted(pageIDMap.keys()) + numids = len(idlst) + cnt = len(idlst) + previd = None + for j in range(cnt): + pageid = idlst[j] + if j < cnt - 1: + nextid = idlst[j+1] + else: + nextid = None + print '.', + pagelst = pageIDMap[pageid] + flst = [] + for page in pagelst: + flst.append(xmllst[page]) + flat_svg = "".join(flst) + flst=None + svgxml = flatxml2svg.convert2SVG(gd, flat_svg, pageid, previd, nextid, svgDir, raw, meta_array, scaledpi) + if (raw) : + pfile = open(os.path.join(svgDir,'page%04d.svg' % pageid),'w') + slst.append('Page %d\n' % (pageid, pageid)) + else : + pfile = open(os.path.join(svgDir,'page%04d.xhtml' % pageid), 'w') + slst.append('Page %d\n' % (pageid, pageid)) + previd = pageid + pfile.write(svgxml) + pfile.close() + counter += 1 + slst.append('
\n') + slst.append('

Table of Contents

\n') + slst.append('\n\n') + svgindex = "".join(slst) + slst = None + file(os.path.join(bookDir, 'index_svg.xhtml'), 'wb').write(svgindex) + + print " " + + # build the opf file + opfname = os.path.join(bookDir, 'book.opf') + olst = [] + olst.append('\n') + olst.append('\n') + # adding metadata + olst.append(' \n') + if 'GUID' in meta_array: + olst.append(' ' + meta_array['GUID'] + '\n') + if 'ASIN' in meta_array: + olst.append(' ' + meta_array['ASIN'] + '\n') + if 'oASIN' in meta_array: + olst.append(' ' + meta_array['oASIN'] + '\n') + olst.append(' ' + meta_array['Title'] + '\n') + olst.append(' ' + meta_array['Authors'] + '\n') + olst.append(' en\n') + olst.append(' ' + meta_array['UpdateTime'] + '\n') + if isCover: + olst.append(' \n') + olst.append(' \n') + olst.append('\n') + olst.append(' \n') + olst.append(' \n') + # adding image files to manifest + filenames = os.listdir(imgDir) + filenames = sorted(filenames) + for filename in filenames: + imgname, imgext = os.path.splitext(filename) + if imgext == '.jpg': + imgext = 'jpeg' + if imgext == '.svg': + imgext = 'svg+xml' + olst.append(' \n') + if isCover: + olst.append(' \n') + olst.append('\n') + # adding spine + olst.append('\n \n\n') + if isCover: + olst.append(' \n') + olst.append(' \n') + olst.append(' \n') + olst.append('\n') + opfstr = "".join(olst) + olst = None + file(opfname, 'wb').write(opfstr) + + print 'Processing Complete' + return 0 +def usage(): + print "genbook.py generates a book from the extract Topaz Files" + print "Usage:" + print " genbook.py [-r] [-h [--fixed-image] " + 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 cli_main(): - sys.stdout=SafeUnbuffered(sys.stdout) - sys.stderr=SafeUnbuffered(sys.stderr) - argv=unicode_argv() - progname = os.path.basename(argv[0]) - if len(argv) != 4: - print u"usage: {0} ".format(progname) - return 1 - keypath, inpath, outpath = argv[1:] - userkey = open(keypath,'rb').read() - result = decryptBook(userkey, inpath, outpath) - if result == 0: - print u"Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath)) - return result -def gui_main(): +def main(argv): + bookDir = '' + if len(argv) == 0: + argv = sys.argv + try: - import Tkinter - import Tkconstants - import tkMessageBox - import traceback - except: - return cli_main() - - class DecryptionDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - self.status = Tkinter.Label(self, text=u"Select files for decryption") - self.status.pack(fill=Tkconstants.X, expand=1) - body = Tkinter.Frame(self) - body.pack(fill=Tkconstants.X, expand=1) - sticky = Tkconstants.E + Tkconstants.W - body.grid_columnconfigure(1, weight=2) - Tkinter.Label(body, text=u"Key file").grid(row=0) - self.keypath = Tkinter.Entry(body, width=30) - self.keypath.grid(row=0, column=1, sticky=sticky) - if os.path.exists(u"bnepubkey.b64"): - self.keypath.insert(0, u"bnepubkey.b64") - button = Tkinter.Button(body, text=u"...", command=self.get_keypath) - button.grid(row=0, column=2) - Tkinter.Label(body, text=u"Input file").grid(row=1) - self.inpath = Tkinter.Entry(body, width=30) - self.inpath.grid(row=1, column=1, sticky=sticky) - button = Tkinter.Button(body, text=u"...", command=self.get_inpath) - button.grid(row=1, column=2) - Tkinter.Label(body, text=u"Output file").grid(row=2) - self.outpath = Tkinter.Entry(body, width=30) - self.outpath.grid(row=2, column=1, sticky=sticky) - button = Tkinter.Button(body, text=u"...", command=self.get_outpath) - button.grid(row=2, column=2) - buttons = Tkinter.Frame(self) - buttons.pack() - botton = Tkinter.Button( - buttons, text=u"Decrypt", width=10, command=self.decrypt) - botton.pack(side=Tkconstants.LEFT) - Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) - button = Tkinter.Button( - buttons, text=u"Quit", width=10, command=self.quit) - button.pack(side=Tkconstants.RIGHT) - - def get_keypath(self): - keypath = tkFileDialog.askopenfilename( - parent=None, title=u"Select Barnes & Noble \'.b64\' key file", - defaultextension=u".b64", - filetypes=[('base64-encoded files', '.b64'), - ('All Files', '.*')]) - if keypath: - keypath = os.path.normpath(keypath) - self.keypath.delete(0, Tkconstants.END) - self.keypath.insert(0, keypath) - return - - def get_inpath(self): - inpath = tkFileDialog.askopenfilename( - parent=None, title=u"Select B&N-encrypted ePub file to decrypt", - defaultextension=u".epub", filetypes=[('ePub files', '.epub')]) - if inpath: - inpath = os.path.normpath(inpath) - self.inpath.delete(0, Tkconstants.END) - self.inpath.insert(0, inpath) - return - - def get_outpath(self): - outpath = tkFileDialog.asksaveasfilename( - parent=None, title=u"Select unencrypted ePub file to produce", - defaultextension=u".epub", filetypes=[('ePub files', '.epub')]) - if outpath: - outpath = os.path.normpath(outpath) - self.outpath.delete(0, Tkconstants.END) - self.outpath.insert(0, outpath) - return - - def decrypt(self): - keypath = self.keypath.get() - inpath = self.inpath.get() - outpath = self.outpath.get() - if not keypath or not os.path.exists(keypath): - self.status['text'] = u"Specified key file does not exist" - return - if not inpath or not os.path.exists(inpath): - self.status['text'] = u"Specified input file does not exist" - return - if not outpath: - self.status['text'] = u"Output file not specified" - return - if inpath == outpath: - self.status['text'] = u"Must have different input and output files" - return - userkey = open(keypath,'rb').read() - self.status['text'] = u"Decrypting..." - try: - decrypt_status = decryptBook(userkey, inpath, outpath) - except Exception, e: - self.status['text'] = u"Error: {0}".format(e.args[0]) - return - if decrypt_status == 0: - self.status['text'] = u"File successfully decrypted" - else: - self.status['text'] = u"The was an error decrypting the file." - - root = Tkinter.Tk() - root.title(u"Barnes & Noble ePub Decrypter v.{0}".format(__version__)) - root.resizable(True, False) - root.minsize(300, 0) - DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1) - root.mainloop() - return 0 + opts, args = getopt.getopt(argv[1:], "rh:",["fixed-image"]) + + except getopt.GetoptError, err: + print str(err) + usage() + return 1 + + if len(opts) == 0 and len(args) == 0 : + usage() + return 1 + + raw = 0 + fixedimage = True + for o, a in opts: + if o =="-h": + usage() + return 0 + if o =="-r": + raw = 1 + if o =="--fixed-image": + fixedimage = True + + bookDir = args[0] + + rv = generateBook(bookDir, raw, fixedimage) + return rv + if __name__ == '__main__': - if len(sys.argv) > 1: - sys.exit(cli_main()) - sys.exit(gui_main()) + sys.exit(main('')) diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/ignobleepub.py b/DeDRM_calibre_plugin/DeDRM_plugin/ignobleepub.py index 5118c87..ac73d1e 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/ignobleepub.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/ignobleepub.py @@ -3,47 +3,54 @@ from __future__ import with_statement -# ignoblekeygen.pyw, version 2.5 -# Copyright © 2009-2010 i♥cabbages +# ignobleepub.pyw, version 3.8 +# Copyright © 2009-2010 by i♥cabbages # Released under the terms of the GNU General Public Licence, version 3 # # Modified 2010–2013 by some_updates, DiapDealer and Apprentice Alf -# Windows users: Before running this program, you must first install Python. -# We recommend ActiveState Python 2.7.X for Windows (x86) from -# http://www.activestate.com/activepython/downloads. -# You must also install PyCrypto from -# http://www.voidspace.org.uk/python/modules.shtml#pycrypto -# (make certain to install the version for Python 2.7). -# Then save this script file as ignoblekeygen.pyw and double-click on it to run it. +# Windows users: Before running this program, you must first install Python 2.6 +# from and PyCrypto from +# (make sure to +# install the version for Python 2.6). Save this script file as +# ineptepub.pyw and double-click on it to run it. # -# Mac OS X users: Save this script file as ignoblekeygen.pyw. You can run this -# program from the command line (python ignoblekeygen.pyw) or by double-clicking +# Mac OS X users: Save this script file as ineptepub.pyw. You can run this +# program from the command line (pythonw ineptepub.pyw) or by double-clicking # it when it has been associated with PythonLauncher. # Revision history: # 1 - Initial release -# 2 - Add OS X support by using OpenSSL when available (taken/modified from ineptepub v5) -# 2.1 - Allow Windows versions of libcrypto to be found -# 2.2 - On Windows try PyCrypto first and then OpenSSL next -# 2.3 - Modify interface to allow use of import -# 2.4 - Improvements to UI and now works in plugins -# 2.5 - Additional improvement for unicode and plugin support -# 2.6 - moved unicode_argv call inside main for Windows DeDRM compatibility -# 2.7 - Work if TkInter is missing +# 2 - Added OS X support by using OpenSSL when available +# 3 - screen out improper key lengths to prevent segfaults on Linux +# 3.1 - Allow Windows versions of libcrypto to be found +# 3.2 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml +# 3.3 - On Windows try PyCrypto first, OpenSSL next +# 3.4 - Modify interface to allow use with import +# 3.5 - Fix for potential problem with PyCrypto +# 3.6 - Revised to allow use in calibre plugins to eliminate need for duplicate code +# 3.7 - Tweaked to match ineptepub more closely +# 3.8 - Fixed to retain zip file metadata (e.g. file modification date) +# 3.9 - moved unicode_argv call inside main for Windows DeDRM compatibility +# 4.0 - Work if TkInter is missing """ -Generate Barnes & Noble EPUB user key from name and credit card number. +Decrypt Barnes & Noble encrypted ePub books. """ __license__ = 'GPL v3' -__version__ = "2.7" +__version__ = "4.0" import sys import os -import hashlib +import traceback +import zlib +import zipfile +from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED +from contextlib import closing +import xml.etree.ElementTree as etree # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get @@ -75,8 +82,8 @@ def unicode_argv(): # Versions 2.x of Python don't support Unicode in sys.argv on # Windows, with the underlying Windows API instead replacing multi-byte - # characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv - # as a list of Unicode strings and encode them as utf-8 + # characters with '?'. + from ctypes import POINTER, byref, cdll, c_int, windll from ctypes.wintypes import LPCWSTR, LPWSTR @@ -97,9 +104,7 @@ def unicode_argv(): start = argc.value - len(sys.argv) return [argv[i] for i in xrange(start, argc.value)] - # if we don't have any arguments at all, just pass back script name - # this should never happen - return [u"ignoblekeygen.py"] + return [u"ineptepub.py"] else: argvencoding = sys.stdin.encoding if argvencoding == None: @@ -140,26 +145,29 @@ def _load_crypto_libcrypto(): func.argtypes = argtypes return func - AES_set_encrypt_key = F(c_int, 'AES_set_encrypt_key', + 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, iv): + def __init__(self, userkey): self._blocksize = len(userkey) - self._iv = iv + if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : + raise IGNOBLEError('AES improper key used') + return key = self._key = AES_KEY() - rv = AES_set_encrypt_key(userkey, len(userkey) * 8, key) + rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key) if rv < 0: - raise IGNOBLEError('Failed to initialize AES Encrypt key') + raise IGNOBLEError('Failed to initialize AES key') - def encrypt(self, data): + def decrypt(self, data): out = create_string_buffer(len(data)) - rv = AES_cbc_encrypt(data, out, len(data), self._key, self._iv, 1) + iv = ("\x00" * self._blocksize) + rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0) if rv == 0: - raise IGNOBLEError('AES encryption failed') + raise IGNOBLEError('AES decryption failed') return out.raw return AES @@ -168,11 +176,11 @@ def _load_crypto_pycrypto(): from Crypto.Cipher import AES as _AES class AES(object): - def __init__(self, key, iv): - self._aes = _AES.new(key, _AES.MODE_CBC, iv) + def __init__(self, key): + self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16) - def encrypt(self, data): - return self._aes.encrypt(data) + def decrypt(self, data): + return self._aes.decrypt(data) return AES @@ -191,29 +199,123 @@ def _load_crypto(): AES = _load_crypto() -def normalize_name(name): - return ''.join(x for x in name.lower() if x != ' ') - - -def generate_key(name, ccn): - # remove spaces and case from name and CC numbers. - if type(name)==unicode: - name = name.encode('utf-8') - if type(ccn)==unicode: - ccn = ccn.encode('utf-8') - - name = normalize_name(name) + '\x00' - ccn = normalize_name(ccn) + '\x00' - - name_sha = hashlib.sha1(name).digest()[:16] - ccn_sha = hashlib.sha1(ccn).digest()[:16] - both_sha = hashlib.sha1(name + ccn).digest() - aes = AES(ccn_sha, name_sha) - crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c)) - userkey = hashlib.sha1(crypt).digest() - return userkey.encode('base64') - - +META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml') +NSMAP = {'adept': 'http://ns.adobe.com/adept', + 'enc': 'http://www.w3.org/2001/04/xmlenc#'} + +class Decryptor(object): + def __init__(self, bookkey, encryption): + enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag) + self._aes = AES(bookkey) + encryption = etree.fromstring(encryption) + self._encrypted = encrypted = set() + expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'), + enc('CipherReference')) + for elem in encryption.findall(expr): + path = elem.get('URI', None) + if path is not None: + path = path.encode('utf-8') + encrypted.add(path) + + def decompress(self, bytes): + dc = zlib.decompressobj(-15) + bytes = dc.decompress(bytes) + ex = dc.decompress('Z') + dc.flush() + if ex: + bytes = bytes + ex + return bytes + + def decrypt(self, path, data): + if path in self._encrypted: + data = self._aes.decrypt(data)[16:] + data = data[:-ord(data[-1])] + data = self.decompress(data) + return data + +# check file to make check whether it's probably an Adobe Adept encrypted ePub +def ignobleBook(inpath): + with closing(ZipFile(open(inpath, 'rb'))) as inf: + namelist = set(inf.namelist()) + if 'META-INF/rights.xml' not in namelist or \ + 'META-INF/encryption.xml' not in namelist: + return False + try: + rights = etree.fromstring(inf.read('META-INF/rights.xml')) + adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) + expr = './/%s' % (adept('encryptedKey'),) + bookkey = ''.join(rights.findtext(expr)) + if len(bookkey) == 64: + return True + except: + # if we couldn't check, assume it is + return True + return False + +def decryptBook(keyb64, inpath, outpath): + if AES is None: + raise IGNOBLEError(u"PyCrypto or OpenSSL must be installed.") + key = keyb64.decode('base64')[:16] + aes = AES(key) + with closing(ZipFile(open(inpath, 'rb'))) as inf: + namelist = set(inf.namelist()) + if 'META-INF/rights.xml' not in namelist or \ + 'META-INF/encryption.xml' not in namelist: + print u"{0:s} is DRM-free.".format(os.path.basename(inpath)) + return 1 + for name in META_NAMES: + namelist.remove(name) + try: + rights = etree.fromstring(inf.read('META-INF/rights.xml')) + adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) + expr = './/%s' % (adept('encryptedKey'),) + bookkey = ''.join(rights.findtext(expr)) + if len(bookkey) != 64: + print u"{0:s} is not a secure Barnes & Noble ePub.".format(os.path.basename(inpath)) + return 1 + bookkey = aes.decrypt(bookkey.decode('base64')) + bookkey = bookkey[:-ord(bookkey[-1])] + encryption = inf.read('META-INF/encryption.xml') + decryptor = Decryptor(bookkey[-16:], encryption) + kwds = dict(compression=ZIP_DEFLATED, allowZip64=False) + with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf: + zi = ZipInfo('mimetype') + zi.compress_type=ZIP_STORED + try: + # if the mimetype is present, get its info, including time-stamp + oldzi = inf.getinfo('mimetype') + # copy across fields to be preserved + zi.date_time = oldzi.date_time + zi.comment = oldzi.comment + zi.extra = oldzi.extra + zi.internal_attr = oldzi.internal_attr + # external attributes are dependent on the create system, so copy both. + zi.external_attr = oldzi.external_attr + zi.create_system = oldzi.create_system + except: + pass + outf.writestr(zi, inf.read('mimetype')) + for path in namelist: + data = inf.read(path) + zi = ZipInfo(path) + zi.compress_type=ZIP_DEFLATED + try: + # get the file info, including time-stamp + oldzi = inf.getinfo(path) + # copy across useful fields + zi.date_time = oldzi.date_time + zi.comment = oldzi.comment + zi.extra = oldzi.extra + zi.internal_attr = oldzi.internal_attr + # external attributes are dependent on the create system, so copy both. + zi.external_attr = oldzi.external_attr + zi.create_system = oldzi.create_system + except: + pass + outf.writestr(zi, decryptor.decrypt(path, data)) + except: + print u"Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc()) + return 2 + return 0 def cli_main(): @@ -221,19 +323,15 @@ def cli_main(): sys.stderr=SafeUnbuffered(sys.stderr) argv=unicode_argv() progname = os.path.basename(argv[0]) - if AES is None: - print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \ - "separately. Read the top-of-script comment for details." % \ - (progname,) - return 1 if len(argv) != 4: - print u"usage: {0} ".format(progname) + print u"usage: {0} ".format(progname) return 1 - name, ccn, keypath = argv[1:] - userkey = generate_key(name, ccn) - open(keypath,'wb').write(userkey) - return 0 - + keypath, inpath, outpath = argv[1:] + userkey = open(keypath,'rb').read() + result = decryptBook(userkey, inpath, outpath) + if result == 0: + print u"Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath)) + return result def gui_main(): try: @@ -247,28 +345,33 @@ def gui_main(): class DecryptionDialog(Tkinter.Frame): def __init__(self, root): Tkinter.Frame.__init__(self, root, border=5) - self.status = Tkinter.Label(self, text=u"Enter parameters") + self.status = Tkinter.Label(self, text=u"Select files for decryption") self.status.pack(fill=Tkconstants.X, expand=1) body = Tkinter.Frame(self) body.pack(fill=Tkconstants.X, expand=1) sticky = Tkconstants.E + Tkconstants.W body.grid_columnconfigure(1, weight=2) - Tkinter.Label(body, text=u"Account Name").grid(row=0) - self.name = Tkinter.Entry(body, width=40) - self.name.grid(row=0, column=1, sticky=sticky) - Tkinter.Label(body, text=u"CC#").grid(row=1) - self.ccn = Tkinter.Entry(body, width=40) - self.ccn.grid(row=1, column=1, sticky=sticky) - Tkinter.Label(body, text=u"Output file").grid(row=2) - self.keypath = Tkinter.Entry(body, width=40) - self.keypath.grid(row=2, column=1, sticky=sticky) - self.keypath.insert(2, u"bnepubkey.b64") + Tkinter.Label(body, text=u"Key file").grid(row=0) + self.keypath = Tkinter.Entry(body, width=30) + self.keypath.grid(row=0, column=1, sticky=sticky) + if os.path.exists(u"bnepubkey.b64"): + self.keypath.insert(0, u"bnepubkey.b64") button = Tkinter.Button(body, text=u"...", command=self.get_keypath) + button.grid(row=0, column=2) + Tkinter.Label(body, text=u"Input file").grid(row=1) + self.inpath = Tkinter.Entry(body, width=30) + self.inpath.grid(row=1, column=1, sticky=sticky) + button = Tkinter.Button(body, text=u"...", command=self.get_inpath) + button.grid(row=1, column=2) + Tkinter.Label(body, text=u"Output file").grid(row=2) + self.outpath = Tkinter.Entry(body, width=30) + self.outpath.grid(row=2, column=1, sticky=sticky) + button = Tkinter.Button(body, text=u"...", command=self.get_outpath) button.grid(row=2, column=2) buttons = Tkinter.Frame(self) buttons.pack() botton = Tkinter.Button( - buttons, text=u"Generate", width=10, command=self.generate) + buttons, text=u"Decrypt", width=10, command=self.decrypt) botton.pack(side=Tkconstants.LEFT) Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) button = Tkinter.Button( @@ -276,8 +379,8 @@ def gui_main(): button.pack(side=Tkconstants.RIGHT) def get_keypath(self): - keypath = tkFileDialog.asksaveasfilename( - parent=None, title=u"Select B&N ePub key file to produce", + keypath = tkFileDialog.askopenfilename( + parent=None, title=u"Select Barnes & Noble \'.b64\' key file", defaultextension=u".b64", filetypes=[('base64-encoded files', '.b64'), ('All Files', '.*')]) @@ -287,37 +390,56 @@ def gui_main(): self.keypath.insert(0, keypath) return - def generate(self): - name = self.name.get() - ccn = self.ccn.get() + def get_inpath(self): + inpath = tkFileDialog.askopenfilename( + parent=None, title=u"Select B&N-encrypted ePub file to decrypt", + defaultextension=u".epub", filetypes=[('ePub files', '.epub')]) + if inpath: + inpath = os.path.normpath(inpath) + self.inpath.delete(0, Tkconstants.END) + self.inpath.insert(0, inpath) + return + + def get_outpath(self): + outpath = tkFileDialog.asksaveasfilename( + parent=None, title=u"Select unencrypted ePub file to produce", + defaultextension=u".epub", filetypes=[('ePub files', '.epub')]) + if outpath: + outpath = os.path.normpath(outpath) + self.outpath.delete(0, Tkconstants.END) + self.outpath.insert(0, outpath) + return + + def decrypt(self): keypath = self.keypath.get() - if not name: - self.status['text'] = u"Name not specified" + inpath = self.inpath.get() + outpath = self.outpath.get() + if not keypath or not os.path.exists(keypath): + self.status['text'] = u"Specified key file does not exist" return - if not ccn: - self.status['text'] = u"Credit card number not specified" + if not inpath or not os.path.exists(inpath): + self.status['text'] = u"Specified input file does not exist" return - if not keypath: - self.status['text'] = u"Output keyfile path not specified" + if not outpath: + self.status['text'] = u"Output file not specified" return - self.status['text'] = u"Generating..." + if inpath == outpath: + self.status['text'] = u"Must have different input and output files" + return + userkey = open(keypath,'rb').read() + self.status['text'] = u"Decrypting..." try: - userkey = generate_key(name, ccn) + decrypt_status = decryptBook(userkey, inpath, outpath) except Exception, e: - self.status['text'] = u"Error: (0}".format(e.args[0]) + self.status['text'] = u"Error: {0}".format(e.args[0]) return - open(keypath,'wb').write(userkey) - self.status['text'] = u"Keyfile successfully generated" + if decrypt_status == 0: + self.status['text'] = u"File successfully decrypted" + else: + self.status['text'] = u"The was an error decrypting the file." root = Tkinter.Tk() - if AES is None: - root.withdraw() - tkMessageBox.showerror( - "Ignoble EPUB Keyfile Generator", - "This script requires OpenSSL or PyCrypto, which must be installed " - "separately. Read the top-of-script comment for details.") - return 1 - root.title(u"Barnes & Noble ePub Keyfile Generator v.{0}".format(__version__)) + root.title(u"Barnes & Noble ePub Decrypter v.{0}".format(__version__)) root.resizable(True, False) root.minsize(300, 0) DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1) diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/ignoblekeygen.py b/DeDRM_calibre_plugin/DeDRM_plugin/ignoblekeygen.py index f8181cb..5118c87 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/ignoblekeygen.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/ignoblekeygen.py @@ -3,56 +3,47 @@ from __future__ import with_statement -# ineptepub.pyw, version 6.1 -# Copyright © 2009-2010 by i♥cabbages +# ignoblekeygen.pyw, version 2.5 +# Copyright © 2009-2010 i♥cabbages # Released under the terms of the GNU General Public Licence, version 3 # # Modified 2010–2013 by some_updates, DiapDealer and Apprentice Alf -# Windows users: Before running this program, you must first install Python 2.6 -# from and PyCrypto from -# (make sure to -# install the version for Python 2.6). Save this script file as -# ineptepub.pyw and double-click on it to run it. +# Windows users: Before running this program, you must first install Python. +# We recommend ActiveState Python 2.7.X for Windows (x86) from +# http://www.activestate.com/activepython/downloads. +# You must also install PyCrypto from +# http://www.voidspace.org.uk/python/modules.shtml#pycrypto +# (make certain to install the version for Python 2.7). +# Then save this script file as ignoblekeygen.pyw and double-click on it to run it. # -# Mac OS X users: Save this script file as ineptepub.pyw. You can run this -# program from the command line (pythonw ineptepub.pyw) or by double-clicking +# Mac OS X users: Save this script file as ignoblekeygen.pyw. You can run this +# program from the command line (python ignoblekeygen.pyw) or by double-clicking # it when it has been associated with PythonLauncher. # Revision history: # 1 - Initial release -# 2 - Rename to INEPT, fix exit code -# 5 - Version bump to avoid (?) confusion; -# Improve OS X support by using OpenSSL when available -# 5.1 - Improve OpenSSL error checking -# 5.2 - Fix ctypes error causing segfaults on some systems -# 5.3 - add support for OpenSSL on Windows, fix bug with some versions of libcrypto 0.9.8 prior to path level o -# 5.4 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml -# 5.5 - On Windows try PyCrypto first, OpenSSL next -# 5.6 - Modify interface to allow use with import -# 5.7 - Fix for potential problem with PyCrypto -# 5.8 - Revised to allow use in calibre plugins to eliminate need for duplicate code -# 5.9 - Fixed to retain zip file metadata (e.g. file modification date) -# 6.0 - moved unicode_argv call inside main for Windows DeDRM compatibility -# 6.1 - Work if TkInter is missing +# 2 - Add OS X support by using OpenSSL when available (taken/modified from ineptepub v5) +# 2.1 - Allow Windows versions of libcrypto to be found +# 2.2 - On Windows try PyCrypto first and then OpenSSL next +# 2.3 - Modify interface to allow use of import +# 2.4 - Improvements to UI and now works in plugins +# 2.5 - Additional improvement for unicode and plugin support +# 2.6 - moved unicode_argv call inside main for Windows DeDRM compatibility +# 2.7 - Work if TkInter is missing """ -Decrypt Adobe Digital Editions encrypted ePub books. +Generate Barnes & Noble EPUB user key from name and credit card number. """ __license__ = 'GPL v3' -__version__ = "6.1" +__version__ = "2.7" import sys import os -import traceback -import zlib -import zipfile -from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED -from contextlib import closing -import xml.etree.ElementTree as etree +import hashlib # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get @@ -84,8 +75,8 @@ def unicode_argv(): # Versions 2.x of Python don't support Unicode in sys.argv on # Windows, with the underlying Windows API instead replacing multi-byte - # characters with '?'. - + # characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv + # as a list of Unicode strings and encode them as utf-8 from ctypes import POINTER, byref, cdll, c_int, windll from ctypes.wintypes import LPCWSTR, LPWSTR @@ -106,7 +97,9 @@ def unicode_argv(): start = argc.value - len(sys.argv) return [argv[i] for i in xrange(start, argc.value)] - return [u"ineptepub.py"] + # if we don't have any arguments at all, just pass back script name + # this should never happen + return [u"ignoblekeygen.py"] else: argvencoding = sys.stdin.encoding if argvencoding == None: @@ -114,7 +107,7 @@ def unicode_argv(): return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv] -class ADEPTError(Exception): +class IGNOBLEError(Exception): pass def _load_crypto_libcrypto(): @@ -128,19 +121,14 @@ def _load_crypto_libcrypto(): libcrypto = find_library('crypto') if libcrypto is None: - raise ADEPTError('libcrypto not found') + raise IGNOBLEError('libcrypto not found') libcrypto = CDLL(libcrypto) - RSA_NO_PADDING = 3 AES_MAXNR = 14 c_char_pp = POINTER(c_char_p) c_int_p = POINTER(c_int) - class RSA(Structure): - pass - RSA_p = POINTER(RSA) - class AES_KEY(Structure): _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)] @@ -152,312 +140,80 @@ def _load_crypto_libcrypto(): func.argtypes = argtypes return func - d2i_RSAPrivateKey = F(RSA_p, 'd2i_RSAPrivateKey', - [RSA_p, c_char_pp, c_long]) - RSA_size = F(c_int, 'RSA_size', [RSA_p]) - RSA_private_decrypt = F(c_int, 'RSA_private_decrypt', - [c_int, c_char_p, c_char_p, RSA_p, c_int]) - RSA_free = F(None, 'RSA_free', [RSA_p]) - AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key', + AES_set_encrypt_key = F(c_int, 'AES_set_encrypt_key', [c_char_p, c_int, AES_KEY_p]) AES_cbc_encrypt = F(None, 'AES_cbc_encrypt', [c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p, c_int]) - class RSA(object): - def __init__(self, der): - buf = create_string_buffer(der) - pp = c_char_pp(cast(buf, c_char_p)) - rsa = self._rsa = d2i_RSAPrivateKey(None, pp, len(der)) - if rsa is None: - raise ADEPTError('Error parsing ADEPT user key DER') - - def decrypt(self, from_): - rsa = self._rsa - to = create_string_buffer(RSA_size(rsa)) - dlen = RSA_private_decrypt(len(from_), from_, to, rsa, - RSA_NO_PADDING) - if dlen < 0: - raise ADEPTError('RSA decryption failed') - return to[:dlen] - - def __del__(self): - if self._rsa is not None: - RSA_free(self._rsa) - self._rsa = None - class AES(object): - def __init__(self, userkey): + def __init__(self, userkey, iv): self._blocksize = len(userkey) - if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : - raise ADEPTError('AES improper key used') - return + self._iv = iv key = self._key = AES_KEY() - rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key) + rv = AES_set_encrypt_key(userkey, len(userkey) * 8, key) if rv < 0: - raise ADEPTError('Failed to initialize AES key') + raise IGNOBLEError('Failed to initialize AES Encrypt key') - def decrypt(self, data): + def encrypt(self, data): out = create_string_buffer(len(data)) - iv = ("\x00" * self._blocksize) - rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0) + rv = AES_cbc_encrypt(data, out, len(data), self._key, self._iv, 1) if rv == 0: - raise ADEPTError('AES decryption failed') + raise IGNOBLEError('AES encryption failed') return out.raw - return (AES, RSA) + return AES def _load_crypto_pycrypto(): from Crypto.Cipher import AES as _AES - from Crypto.PublicKey import RSA as _RSA - - # ASN.1 parsing code from tlslite - class ASN1Error(Exception): - pass - - class ASN1Parser(object): - class Parser(object): - def __init__(self, bytes): - self.bytes = bytes - self.index = 0 - - def get(self, length): - if self.index + length > len(self.bytes): - raise ASN1Error("Error decoding ASN.1") - x = 0 - for count in range(length): - x <<= 8 - x |= self.bytes[self.index] - self.index += 1 - return x - - def getFixBytes(self, lengthBytes): - bytes = self.bytes[self.index : self.index+lengthBytes] - self.index += lengthBytes - return bytes - - def getVarBytes(self, lengthLength): - lengthBytes = self.get(lengthLength) - return self.getFixBytes(lengthBytes) - - def getFixList(self, length, lengthList): - l = [0] * lengthList - for x in range(lengthList): - l[x] = self.get(length) - return l - - def getVarList(self, length, lengthLength): - lengthList = self.get(lengthLength) - if lengthList % length != 0: - raise ASN1Error("Error decoding ASN.1") - lengthList = int(lengthList/length) - l = [0] * lengthList - for x in range(lengthList): - l[x] = self.get(length) - return l - - def startLengthCheck(self, lengthLength): - self.lengthCheck = self.get(lengthLength) - self.indexCheck = self.index - - def setLengthCheck(self, length): - self.lengthCheck = length - self.indexCheck = self.index - - def stopLengthCheck(self): - if (self.index - self.indexCheck) != self.lengthCheck: - raise ASN1Error("Error decoding ASN.1") - - def atLengthCheck(self): - if (self.index - self.indexCheck) < self.lengthCheck: - return False - elif (self.index - self.indexCheck) == self.lengthCheck: - return True - else: - raise ASN1Error("Error decoding ASN.1") - - def __init__(self, bytes): - p = self.Parser(bytes) - p.get(1) - self.length = self._getASN1Length(p) - self.value = p.getFixBytes(self.length) - - def getChild(self, which): - p = self.Parser(self.value) - for x in range(which+1): - markIndex = p.index - p.get(1) - length = self._getASN1Length(p) - p.getFixBytes(length) - return ASN1Parser(p.bytes[markIndex:p.index]) - - def _getASN1Length(self, p): - firstLength = p.get(1) - if firstLength<=127: - return firstLength - else: - lengthLength = firstLength & 0x7F - return p.get(lengthLength) class AES(object): - def __init__(self, key): - self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16) + def __init__(self, key, iv): + self._aes = _AES.new(key, _AES.MODE_CBC, iv) - def decrypt(self, data): - return self._aes.decrypt(data) + def encrypt(self, data): + return self._aes.encrypt(data) - class RSA(object): - def __init__(self, der): - key = ASN1Parser([ord(x) for x in der]) - key = [key.getChild(x).value for x in xrange(1, 4)] - key = [self.bytesToNumber(v) for v in key] - self._rsa = _RSA.construct(key) - - def bytesToNumber(self, bytes): - total = 0L - for byte in bytes: - total = (total << 8) + byte - return total - - def decrypt(self, data): - return self._rsa.decrypt(data) - - return (AES, RSA) + return AES def _load_crypto(): - AES = RSA = None + AES = None cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto) if sys.platform.startswith('win'): cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto) for loader in cryptolist: try: - AES, RSA = loader() + AES = loader() break - except (ImportError, ADEPTError): + except (ImportError, IGNOBLEError): pass - return (AES, RSA) - -AES, RSA = _load_crypto() - -META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml') -NSMAP = {'adept': 'http://ns.adobe.com/adept', - 'enc': 'http://www.w3.org/2001/04/xmlenc#'} - -class Decryptor(object): - def __init__(self, bookkey, encryption): - enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag) - self._aes = AES(bookkey) - encryption = etree.fromstring(encryption) - self._encrypted = encrypted = set() - expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'), - enc('CipherReference')) - for elem in encryption.findall(expr): - path = elem.get('URI', None) - if path is not None: - path = path.encode('utf-8') - encrypted.add(path) - - def decompress(self, bytes): - dc = zlib.decompressobj(-15) - bytes = dc.decompress(bytes) - ex = dc.decompress('Z') + dc.flush() - if ex: - bytes = bytes + ex - return bytes - - def decrypt(self, path, data): - if path in self._encrypted: - data = self._aes.decrypt(data)[16:] - data = data[:-ord(data[-1])] - data = self.decompress(data) - return data - -# check file to make check whether it's probably an Adobe Adept encrypted ePub -def adeptBook(inpath): - with closing(ZipFile(open(inpath, 'rb'))) as inf: - namelist = set(inf.namelist()) - if 'META-INF/rights.xml' not in namelist or \ - 'META-INF/encryption.xml' not in namelist: - return False - try: - rights = etree.fromstring(inf.read('META-INF/rights.xml')) - adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) - expr = './/%s' % (adept('encryptedKey'),) - bookkey = ''.join(rights.findtext(expr)) - if len(bookkey) == 172: - return True - except: - # if we couldn't check, assume it is - return True - return False - -def decryptBook(userkey, inpath, outpath): - if AES is None: - raise ADEPTError(u"PyCrypto or OpenSSL must be installed.") - rsa = RSA(userkey) - with closing(ZipFile(open(inpath, 'rb'))) as inf: - namelist = set(inf.namelist()) - if 'META-INF/rights.xml' not in namelist or \ - 'META-INF/encryption.xml' not in namelist: - print u"{0:s} is DRM-free.".format(os.path.basename(inpath)) - return 1 - for name in META_NAMES: - namelist.remove(name) - try: - rights = etree.fromstring(inf.read('META-INF/rights.xml')) - adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) - expr = './/%s' % (adept('encryptedKey'),) - bookkey = ''.join(rights.findtext(expr)) - if len(bookkey) != 172: - print u"{0:s} is not a secure Adobe Adept ePub.".format(os.path.basename(inpath)) - return 1 - bookkey = rsa.decrypt(bookkey.decode('base64')) - # Padded as per RSAES-PKCS1-v1_5 - if bookkey[-17] != '\x00': - print u"Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath)) - return 2 - encryption = inf.read('META-INF/encryption.xml') - decryptor = Decryptor(bookkey[-16:], encryption) - kwds = dict(compression=ZIP_DEFLATED, allowZip64=False) - with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf: - zi = ZipInfo('mimetype') - zi.compress_type=ZIP_STORED - try: - # if the mimetype is present, get its info, including time-stamp - oldzi = inf.getinfo('mimetype') - # copy across fields to be preserved - zi.date_time = oldzi.date_time - zi.comment = oldzi.comment - zi.extra = oldzi.extra - zi.internal_attr = oldzi.internal_attr - # external attributes are dependent on the create system, so copy both. - zi.external_attr = oldzi.external_attr - zi.create_system = oldzi.create_system - except: - pass - outf.writestr(zi, inf.read('mimetype')) - for path in namelist: - data = inf.read(path) - zi = ZipInfo(path) - zi.compress_type=ZIP_DEFLATED - try: - # get the file info, including time-stamp - oldzi = inf.getinfo(path) - # copy across useful fields - zi.date_time = oldzi.date_time - zi.comment = oldzi.comment - zi.extra = oldzi.extra - zi.internal_attr = oldzi.internal_attr - # external attributes are dependent on the create system, so copy both. - zi.external_attr = oldzi.external_attr - zi.create_system = oldzi.create_system - except: - pass - outf.writestr(zi, decryptor.decrypt(path, data)) - except: - print u"Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc()) - return 2 - return 0 + return AES + +AES = _load_crypto() + +def normalize_name(name): + return ''.join(x for x in name.lower() if x != ' ') + + +def generate_key(name, ccn): + # remove spaces and case from name and CC numbers. + if type(name)==unicode: + name = name.encode('utf-8') + if type(ccn)==unicode: + ccn = ccn.encode('utf-8') + + name = normalize_name(name) + '\x00' + ccn = normalize_name(ccn) + '\x00' + + name_sha = hashlib.sha1(name).digest()[:16] + ccn_sha = hashlib.sha1(ccn).digest()[:16] + both_sha = hashlib.sha1(name + ccn).digest() + aes = AES(ccn_sha, name_sha) + crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c)) + userkey = hashlib.sha1(crypt).digest() + return userkey.encode('base64') + + def cli_main(): @@ -465,15 +221,19 @@ def cli_main(): sys.stderr=SafeUnbuffered(sys.stderr) argv=unicode_argv() progname = os.path.basename(argv[0]) + if AES is None: + print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \ + "separately. Read the top-of-script comment for details." % \ + (progname,) + return 1 if len(argv) != 4: - print u"usage: {0} ".format(progname) + print u"usage: {0} ".format(progname) return 1 - keypath, inpath, outpath = argv[1:] - userkey = open(keypath,'rb').read() - result = decryptBook(userkey, inpath, outpath) - if result == 0: - print u"Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath)) - return result + name, ccn, keypath = argv[1:] + userkey = generate_key(name, ccn) + open(keypath,'wb').write(userkey) + return 0 + def gui_main(): try: @@ -487,33 +247,28 @@ def gui_main(): class DecryptionDialog(Tkinter.Frame): def __init__(self, root): Tkinter.Frame.__init__(self, root, border=5) - self.status = Tkinter.Label(self, text=u"Select files for decryption") + self.status = Tkinter.Label(self, text=u"Enter parameters") self.status.pack(fill=Tkconstants.X, expand=1) body = Tkinter.Frame(self) body.pack(fill=Tkconstants.X, expand=1) sticky = Tkconstants.E + Tkconstants.W body.grid_columnconfigure(1, weight=2) - Tkinter.Label(body, text=u"Key file").grid(row=0) - self.keypath = Tkinter.Entry(body, width=30) - self.keypath.grid(row=0, column=1, sticky=sticky) - if os.path.exists(u"adeptkey.der"): - self.keypath.insert(0, u"adeptkey.der") - button = Tkinter.Button(body, text=u"...", command=self.get_keypath) - button.grid(row=0, column=2) - Tkinter.Label(body, text=u"Input file").grid(row=1) - self.inpath = Tkinter.Entry(body, width=30) - self.inpath.grid(row=1, column=1, sticky=sticky) - button = Tkinter.Button(body, text=u"...", command=self.get_inpath) - button.grid(row=1, column=2) + Tkinter.Label(body, text=u"Account Name").grid(row=0) + self.name = Tkinter.Entry(body, width=40) + self.name.grid(row=0, column=1, sticky=sticky) + Tkinter.Label(body, text=u"CC#").grid(row=1) + self.ccn = Tkinter.Entry(body, width=40) + self.ccn.grid(row=1, column=1, sticky=sticky) Tkinter.Label(body, text=u"Output file").grid(row=2) - self.outpath = Tkinter.Entry(body, width=30) - self.outpath.grid(row=2, column=1, sticky=sticky) - button = Tkinter.Button(body, text=u"...", command=self.get_outpath) + self.keypath = Tkinter.Entry(body, width=40) + self.keypath.grid(row=2, column=1, sticky=sticky) + self.keypath.insert(2, u"bnepubkey.b64") + button = Tkinter.Button(body, text=u"...", command=self.get_keypath) button.grid(row=2, column=2) buttons = Tkinter.Frame(self) buttons.pack() botton = Tkinter.Button( - buttons, text=u"Decrypt", width=10, command=self.decrypt) + buttons, text=u"Generate", width=10, command=self.generate) botton.pack(side=Tkconstants.LEFT) Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) button = Tkinter.Button( @@ -521,10 +276,10 @@ def gui_main(): button.pack(side=Tkconstants.RIGHT) def get_keypath(self): - keypath = tkFileDialog.askopenfilename( - parent=None, title=u"Select Adobe Adept \'.der\' key file", - defaultextension=u".der", - filetypes=[('Adobe Adept DER-encoded files', '.der'), + keypath = tkFileDialog.asksaveasfilename( + parent=None, title=u"Select B&N ePub key file to produce", + defaultextension=u".b64", + filetypes=[('base64-encoded files', '.b64'), ('All Files', '.*')]) if keypath: keypath = os.path.normpath(keypath) @@ -532,56 +287,37 @@ def gui_main(): self.keypath.insert(0, keypath) return - def get_inpath(self): - inpath = tkFileDialog.askopenfilename( - parent=None, title=u"Select ADEPT-encrypted ePub file to decrypt", - defaultextension=u".epub", filetypes=[('ePub files', '.epub')]) - if inpath: - inpath = os.path.normpath(inpath) - self.inpath.delete(0, Tkconstants.END) - self.inpath.insert(0, inpath) - return - - def get_outpath(self): - outpath = tkFileDialog.asksaveasfilename( - parent=None, title=u"Select unencrypted ePub file to produce", - defaultextension=u".epub", filetypes=[('ePub files', '.epub')]) - if outpath: - outpath = os.path.normpath(outpath) - self.outpath.delete(0, Tkconstants.END) - self.outpath.insert(0, outpath) - return - - def decrypt(self): + def generate(self): + name = self.name.get() + ccn = self.ccn.get() keypath = self.keypath.get() - inpath = self.inpath.get() - outpath = self.outpath.get() - if not keypath or not os.path.exists(keypath): - self.status['text'] = u"Specified key file does not exist" - return - if not inpath or not os.path.exists(inpath): - self.status['text'] = u"Specified input file does not exist" + if not name: + self.status['text'] = u"Name not specified" return - if not outpath: - self.status['text'] = u"Output file not specified" + if not ccn: + self.status['text'] = u"Credit card number not specified" return - if inpath == outpath: - self.status['text'] = u"Must have different input and output files" + if not keypath: + self.status['text'] = u"Output keyfile path not specified" return - userkey = open(keypath,'rb').read() - self.status['text'] = u"Decrypting..." + self.status['text'] = u"Generating..." try: - decrypt_status = decryptBook(userkey, inpath, outpath) + userkey = generate_key(name, ccn) except Exception, e: - self.status['text'] = u"Error: {0}".format(e.args[0]) + self.status['text'] = u"Error: (0}".format(e.args[0]) return - if decrypt_status == 0: - self.status['text'] = u"File successfully decrypted" - else: - self.status['text'] = u"The was an error decrypting the file." + open(keypath,'wb').write(userkey) + self.status['text'] = u"Keyfile successfully generated" root = Tkinter.Tk() - root.title(u"Adobe Adept ePub Decrypter v.{0}".format(__version__)) + if AES is None: + root.withdraw() + tkMessageBox.showerror( + "Ignoble EPUB Keyfile Generator", + "This script requires OpenSSL or PyCrypto, which must be installed " + "separately. Read the top-of-script comment for details.") + return 1 + root.title(u"Barnes & Noble ePub Keyfile Generator v.{0}".format(__version__)) root.resizable(True, False) root.minsize(300, 0) DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1) diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/ineptepub.py b/DeDRM_calibre_plugin/DeDRM_plugin/ineptepub.py index 1986e20..f8181cb 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/ineptepub.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/ineptepub.py @@ -1,73 +1,57 @@ -#! /usr/bin/python +#!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import with_statement -# ineptpdf.pyw, version 7.11 +# ineptepub.pyw, version 6.1 # Copyright © 2009-2010 by i♥cabbages # Released under the terms of the GNU General Public Licence, version 3 # -# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf +# Modified 2010–2013 by some_updates, DiapDealer and Apprentice Alf # Windows users: Before running this program, you must first install Python 2.6 # from and PyCrypto from # (make sure to # install the version for Python 2.6). Save this script file as -# ineptpdf.pyw and double-click on it to run it. +# ineptepub.pyw and double-click on it to run it. # -# Mac OS X users: Save this script file as ineptpdf.pyw. You can run this -# program from the command line (pythonw ineptpdf.pyw) or by double-clicking +# Mac OS X users: Save this script file as ineptepub.pyw. You can run this +# program from the command line (pythonw ineptepub.pyw) or by double-clicking # it when it has been associated with PythonLauncher. # Revision history: # 1 - Initial release -# 2 - Improved determination of key-generation algorithm -# 3 - Correctly handle PDF >=1.5 cross-reference streams -# 4 - Removal of ciando's personal ID -# 5 - Automated decryption of a complete directory -# 6.1 - backward compatibility for 1.7.1 and old adeptkey.der -# 7 - Get cross reference streams and object streams working for input. -# Not yet supported on output but this only effects file size, -# not functionality. (anon2) -# 7.1 - Correct a problem when an old trailer is not followed by startxref -# 7.2 - Correct malformed Mac OS resource forks for Stanza (anon2) -# - Support for cross ref streams on output (decreases file size) -# 7.3 - Correct bug in trailer with cross ref stream that caused the error -# "The root object is missing or invalid" in Adobe Reader. (anon2) -# 7.4 - Force all generation numbers in output file to be 0, like in v6. -# Fallback code for wrong xref improved (search till last trailer -# instead of first) (anon2) -# 7.5 - allow support for OpenSSL to replace pycrypto on all platforms -# implemented ARC4 interface to OpenSSL -# fixed minor typos -# 7.6 - backported AES and other fixes from version 8.4.48 -# 7.7 - On Windows try PyCrypto first and OpenSSL next -# 7.8 - Modify interface to allow use of import -# 7.9 - Bug fix for some session key errors when len(bookkey) > length required -# 7.10 - Various tweaks to fix minor problems. -# 7.11 - More tweaks to fix minor problems. -# 7.12 - Revised to allow use in calibre plugins to eliminate need for duplicate code -# 7.13 - Fixed erroneous mentions of ineptepub -# 7.14 - moved unicode_argv call inside main for Windows DeDRM compatibility -# 8.0 - Work if TkInter is missing -# 8.0.1 - Broken Metadata fix. +# 2 - Rename to INEPT, fix exit code +# 5 - Version bump to avoid (?) confusion; +# Improve OS X support by using OpenSSL when available +# 5.1 - Improve OpenSSL error checking +# 5.2 - Fix ctypes error causing segfaults on some systems +# 5.3 - add support for OpenSSL on Windows, fix bug with some versions of libcrypto 0.9.8 prior to path level o +# 5.4 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml +# 5.5 - On Windows try PyCrypto first, OpenSSL next +# 5.6 - Modify interface to allow use with import +# 5.7 - Fix for potential problem with PyCrypto +# 5.8 - Revised to allow use in calibre plugins to eliminate need for duplicate code +# 5.9 - Fixed to retain zip file metadata (e.g. file modification date) +# 6.0 - moved unicode_argv call inside main for Windows DeDRM compatibility +# 6.1 - Work if TkInter is missing """ -Decrypts Adobe ADEPT-encrypted PDF files. +Decrypt Adobe Digital Editions encrypted ePub books. """ __license__ = 'GPL v3' -__version__ = "8.0.1" +__version__ = "6.1" import sys import os -import re +import traceback import zlib -import struct -import hashlib -from itertools import chain, islice +import zipfile +from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED +from contextlib import closing import xml.etree.ElementTree as etree # Wrap a stream so that output gets flushed immediately @@ -87,8 +71,11 @@ class SafeUnbuffered: def __getattr__(self, attr): return getattr(self.stream, attr) -iswindows = sys.platform.startswith('win') -isosx = sys.platform.startswith('darwin') +try: + from calibre.constants import iswindows, isosx +except: + iswindows = sys.platform.startswith('win') + isosx = sys.platform.startswith('darwin') def unicode_argv(): if iswindows: @@ -119,7 +106,7 @@ def unicode_argv(): start = argc.value - len(sys.argv) return [argv[i] for i in xrange(start, argc.value)] - return [u"ineptpdf.py"] + return [u"ineptepub.py"] else: argvencoding = sys.stdin.encoding if argvencoding == None: @@ -130,21 +117,12 @@ def unicode_argv(): class ADEPTError(Exception): pass - -import hashlib - -def SHA256(message): - ctx = hashlib.sha256() - ctx.update(message) - return ctx.digest() - - def _load_crypto_libcrypto(): from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \ Structure, c_ulong, create_string_buffer, cast from ctypes.util import find_library - if sys.platform.startswith('win'): + if iswindows: libcrypto = find_library('libeay32') else: libcrypto = find_library('crypto') @@ -153,43 +131,38 @@ def _load_crypto_libcrypto(): raise ADEPTError('libcrypto not found') libcrypto = CDLL(libcrypto) - AES_MAXNR = 14 - RSA_NO_PADDING = 3 + 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) - - class RC4_KEY(Structure): - _fields_ = [('x', c_int), ('y', c_int), ('box', c_int * 256)] - RC4_KEY_p = POINTER(RC4_KEY) - class RSA(Structure): pass RSA_p = POINTER(RSA) + class AES_KEY(Structure): + _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), + ('rounds', c_int)] + AES_KEY_p = POINTER(AES_KEY) + def F(restype, name, argtypes): func = getattr(libcrypto, name) func.restype = restype func.argtypes = argtypes return func - AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,c_int]) - AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',[c_char_p, c_int, AES_KEY_p]) - - RC4_set_key = F(None,'RC4_set_key',[RC4_KEY_p, c_int, c_char_p]) - RC4_crypt = F(None,'RC4',[RC4_KEY_p, c_int, c_char_p, c_char_p]) - d2i_RSAPrivateKey = F(RSA_p, 'd2i_RSAPrivateKey', [RSA_p, c_char_pp, c_long]) RSA_size = F(c_int, 'RSA_size', [RSA_p]) RSA_private_decrypt = F(c_int, 'RSA_private_decrypt', [c_int, c_char_p, c_char_p, RSA_p, c_int]) RSA_free = F(None, 'RSA_free', [RSA_p]) + AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key', + [c_char_p, c_int, AES_KEY_p]) + AES_cbc_encrypt = F(None, 'AES_cbc_encrypt', + [c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p, + c_int]) class RSA(object): def __init__(self, der): @@ -206,65 +179,37 @@ def _load_crypto_libcrypto(): RSA_NO_PADDING) if dlen < 0: raise ADEPTError('RSA decryption failed') - return to[1:dlen] + return to[:dlen] def __del__(self): if self._rsa is not None: RSA_free(self._rsa) self._rsa = None - class ARC4(object): - @classmethod - def new(cls, userkey): - self = ARC4() - self._blocksize = len(userkey) - key = self._key = RC4_KEY() - RC4_set_key(key, self._blocksize, userkey) - return self - def __init__(self): - self._blocksize = 0 - self._key = None - def decrypt(self, data): - out = create_string_buffer(len(data)) - RC4_crypt(self._key, len(data), data, out) - return out.raw - class AES(object): - MODE_CBC = 0 - @classmethod - def new(cls, userkey, mode, iv): - self = AES() + def __init__(self, userkey): self._blocksize = len(userkey) - # mode is ignored since CBCMODE is only thing supported/used so far - self._mode = mode if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : raise ADEPTError('AES improper key used') return - keyctx = self._keyctx = AES_KEY() - self._iv = iv - rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx) + 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') - return self - def __init__(self): - self._blocksize = 0 - self._keyctx = None - self._iv = 0 - self._mode = 0 + def decrypt(self, data): out = create_string_buffer(len(data)) - rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self._iv, 0) + 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 (ARC4, RSA, AES) - + return (AES, RSA) def _load_crypto_pycrypto(): - from Crypto.PublicKey import RSA as _RSA - from Crypto.Cipher import ARC4 as _ARC4 from Crypto.Cipher import AES as _AES + from Crypto.PublicKey import RSA as _RSA # ASN.1 parsing code from tlslite class ASN1Error(Exception): @@ -354,26 +299,10 @@ def _load_crypto_pycrypto(): lengthLength = firstLength & 0x7F return p.get(lengthLength) - class ARC4(object): - @classmethod - def new(cls, userkey): - self = ARC4() - self._arc4 = _ARC4.new(userkey) - return self - def __init__(self): - self._arc4 = None - def decrypt(self, data): - return self._arc4.decrypt(data) - class AES(object): - MODE_CBC = _AES.MODE_CBC - @classmethod - def new(cls, userkey, mode, iv): - self = AES() - self._aes = _AES.new(userkey, mode, iv) - return self - def __init__(self): - self._aes = None + def __init__(self, key): + self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16) + def decrypt(self, data): return self._aes.decrypt(data) @@ -393,1801 +322,141 @@ def _load_crypto_pycrypto(): def decrypt(self, data): return self._rsa.decrypt(data) - return (ARC4, RSA, AES) + return (AES, RSA) def _load_crypto(): - ARC4 = RSA = AES = None + AES = RSA = None cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto) if sys.platform.startswith('win'): cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto) for loader in cryptolist: try: - ARC4, RSA, AES = loader() + AES, RSA = loader() break except (ImportError, ADEPTError): pass - return (ARC4, RSA, AES) -ARC4, RSA, AES = _load_crypto() - - -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO - - -# Do we generate cross reference streams on output? -# 0 = never -# 1 = only if present in input -# 2 = always - -GEN_XREF_STM = 1 - -# This is the value for the current document -gen_xref_stm = False # will be set in PDFSerializer - -# PDF parsing routines from pdfminer, with changes for EBX_HANDLER - -# Utilities - -def choplist(n, seq): - '''Groups every n elements of the list.''' - r = [] - for x in seq: - r.append(x) - if len(r) == n: - yield tuple(r) - r = [] - return - -def nunpack(s, default=0): - '''Unpacks up to 4 bytes big endian.''' - l = len(s) - if not l: - return default - elif l == 1: - return ord(s) - elif l == 2: - return struct.unpack('>H', s)[0] - elif l == 3: - return struct.unpack('>L', '\x00'+s)[0] - elif l == 4: - return struct.unpack('>L', s)[0] - else: - return TypeError('invalid length: %d' % l) - - -STRICT = 0 - - -# PS Exceptions - -class PSException(Exception): pass -class PSEOF(PSException): pass -class PSSyntaxError(PSException): pass -class PSTypeError(PSException): pass -class PSValueError(PSException): pass - - -# Basic PostScript Types - - -# PSLiteral -class PSObject(object): pass - -class PSLiteral(PSObject): - ''' - PS literals (e.g. "/Name"). - Caution: Never create these objects directly. - Use PSLiteralTable.intern() instead. - ''' - def __init__(self, name): - self.name = name - return - - def __repr__(self): - name = [] - for char in self.name: - if not char.isalnum(): - char = '#%02x' % ord(char) - name.append(char) - return '/%s' % ''.join(name) - -# PSKeyword -class PSKeyword(PSObject): - ''' - PS keywords (e.g. "showpage"). - Caution: Never create these objects directly. - Use PSKeywordTable.intern() instead. - ''' - def __init__(self, name): - self.name = name - return - - def __repr__(self): - return self.name - -# PSSymbolTable -class PSSymbolTable(object): - - ''' - Symbol table that stores PSLiteral or PSKeyword. - ''' - - def __init__(self, classe): - self.dic = {} - self.classe = classe - return - - def intern(self, name): - if name in self.dic: - lit = self.dic[name] - else: - lit = self.classe(name) - self.dic[name] = lit - return lit - -PSLiteralTable = PSSymbolTable(PSLiteral) -PSKeywordTable = PSSymbolTable(PSKeyword) -LIT = PSLiteralTable.intern -KWD = PSKeywordTable.intern -KEYWORD_BRACE_BEGIN = KWD('{') -KEYWORD_BRACE_END = KWD('}') -KEYWORD_ARRAY_BEGIN = KWD('[') -KEYWORD_ARRAY_END = KWD(']') -KEYWORD_DICT_BEGIN = KWD('<<') -KEYWORD_DICT_END = KWD('>>') - - -def literal_name(x): - if not isinstance(x, PSLiteral): - if STRICT: - raise PSTypeError('Literal required: %r' % x) - else: - return str(x) - return x.name - -def keyword_name(x): - if not isinstance(x, PSKeyword): - if STRICT: - raise PSTypeError('Keyword required: %r' % x) - else: - return str(x) - return x.name - - -## PSBaseParser -## -EOL = re.compile(r'[\r\n]') -SPC = re.compile(r'\s') -NONSPC = re.compile(r'\S') -HEX = re.compile(r'[0-9a-fA-F]') -END_LITERAL = re.compile(r'[#/%\[\]()<>{}\s]') -END_HEX_STRING = re.compile(r'[^\s0-9a-fA-F]') -HEX_PAIR = re.compile(r'[0-9a-fA-F]{2}|.') -END_NUMBER = re.compile(r'[^0-9]') -END_KEYWORD = re.compile(r'[#/%\[\]()<>{}\s]') -END_STRING = re.compile(r'[()\134]') -OCT_STRING = re.compile(r'[0-7]') -ESC_STRING = { 'b':8, 't':9, 'n':10, 'f':12, 'r':13, '(':40, ')':41, '\\':92 } - -class PSBaseParser(object): - - ''' - Most basic PostScript parser that performs only basic tokenization. - ''' - BUFSIZ = 4096 - - def __init__(self, fp): - self.fp = fp - self.seek(0) - return - - def __repr__(self): - return '' % (self.fp, self.bufpos) - - def flush(self): - return - - def close(self): - self.flush() - return - - def tell(self): - return self.bufpos+self.charpos - - def poll(self, pos=None, n=80): - pos0 = self.fp.tell() - if not pos: - pos = self.bufpos+self.charpos - self.fp.seek(pos) - ##print >>sys.stderr, 'poll(%d): %r' % (pos, self.fp.read(n)) - self.fp.seek(pos0) - return - - def seek(self, pos): - ''' - Seeks the parser to the given position. - ''' - self.fp.seek(pos) - # reset the status for nextline() - self.bufpos = pos - self.buf = '' - self.charpos = 0 - # reset the status for nexttoken() - self.parse1 = self.parse_main - self.tokens = [] - return - - def fillbuf(self): - if self.charpos < len(self.buf): return - # fetch next chunk. - self.bufpos = self.fp.tell() - self.buf = self.fp.read(self.BUFSIZ) - if not self.buf: - raise PSEOF('Unexpected EOF') - self.charpos = 0 - return - - def parse_main(self, s, i): - m = NONSPC.search(s, i) - if not m: - return (self.parse_main, len(s)) - j = m.start(0) - c = s[j] - self.tokenstart = self.bufpos+j - if c == '%': - self.token = '%' - return (self.parse_comment, j+1) - if c == '/': - self.token = '' - return (self.parse_literal, j+1) - if c in '-+' or c.isdigit(): - self.token = c - return (self.parse_number, j+1) - if c == '.': - self.token = c - return (self.parse_float, j+1) - if c.isalpha(): - self.token = c - return (self.parse_keyword, j+1) - if c == '(': - self.token = '' - self.paren = 1 - return (self.parse_string, j+1) - if c == '<': - self.token = '' - return (self.parse_wopen, j+1) - if c == '>': - self.token = '' - return (self.parse_wclose, j+1) - self.add_token(KWD(c)) - return (self.parse_main, j+1) - - def add_token(self, obj): - self.tokens.append((self.tokenstart, obj)) - return - - def parse_comment(self, s, i): - m = EOL.search(s, i) - if not m: - self.token += s[i:] - return (self.parse_comment, len(s)) - j = m.start(0) - self.token += s[i:j] - # We ignore comments. - #self.tokens.append(self.token) - return (self.parse_main, j) - - def parse_literal(self, s, i): - m = END_LITERAL.search(s, i) - if not m: - self.token += s[i:] - return (self.parse_literal, len(s)) - j = m.start(0) - self.token += s[i:j] - c = s[j] - if c == '#': - self.hex = '' - return (self.parse_literal_hex, j+1) - self.add_token(LIT(self.token)) - return (self.parse_main, j) - - def parse_literal_hex(self, s, i): - c = s[i] - if HEX.match(c) and len(self.hex) < 2: - self.hex += c - return (self.parse_literal_hex, i+1) - if self.hex: - self.token += chr(int(self.hex, 16)) - return (self.parse_literal, i) - - def parse_number(self, s, i): - m = END_NUMBER.search(s, i) - if not m: - self.token += s[i:] - return (self.parse_number, len(s)) - j = m.start(0) - self.token += s[i:j] - c = s[j] - if c == '.': - self.token += c - return (self.parse_float, j+1) - try: - self.add_token(int(self.token)) - except ValueError: - pass - return (self.parse_main, j) - def parse_float(self, s, i): - m = END_NUMBER.search(s, i) - if not m: - self.token += s[i:] - return (self.parse_float, len(s)) - j = m.start(0) - self.token += s[i:j] - self.add_token(float(self.token)) - return (self.parse_main, j) - - def parse_keyword(self, s, i): - m = END_KEYWORD.search(s, i) - if not m: - self.token += s[i:] - return (self.parse_keyword, len(s)) - j = m.start(0) - self.token += s[i:j] - if self.token == 'true': - token = True - elif self.token == 'false': - token = False - else: - token = KWD(self.token) - self.add_token(token) - return (self.parse_main, j) - - def parse_string(self, s, i): - m = END_STRING.search(s, i) - if not m: - self.token += s[i:] - return (self.parse_string, len(s)) - j = m.start(0) - self.token += s[i:j] - c = s[j] - if c == '\\': - self.oct = '' - return (self.parse_string_1, j+1) - if c == '(': - self.paren += 1 - self.token += c - return (self.parse_string, j+1) - if c == ')': - self.paren -= 1 - if self.paren: - self.token += c - return (self.parse_string, j+1) - self.add_token(self.token) - return (self.parse_main, j+1) - def parse_string_1(self, s, i): - c = s[i] - if OCT_STRING.match(c) and len(self.oct) < 3: - self.oct += c - return (self.parse_string_1, i+1) - if self.oct: - self.token += chr(int(self.oct, 8)) - return (self.parse_string, i) - if c in ESC_STRING: - self.token += chr(ESC_STRING[c]) - return (self.parse_string, i+1) - - def parse_wopen(self, s, i): - c = s[i] - if c.isspace() or HEX.match(c): - return (self.parse_hexstring, i) - if c == '<': - self.add_token(KEYWORD_DICT_BEGIN) - i += 1 - return (self.parse_main, i) - - def parse_wclose(self, s, i): - c = s[i] - if c == '>': - self.add_token(KEYWORD_DICT_END) - i += 1 - return (self.parse_main, i) - - def parse_hexstring(self, s, i): - m = END_HEX_STRING.search(s, i) - if not m: - self.token += s[i:] - return (self.parse_hexstring, len(s)) - j = m.start(0) - self.token += s[i:j] - token = HEX_PAIR.sub(lambda m: chr(int(m.group(0), 16)), - SPC.sub('', self.token)) - self.add_token(token) - return (self.parse_main, j) - - def nexttoken(self): - while not self.tokens: - self.fillbuf() - (self.parse1, self.charpos) = self.parse1(self.buf, self.charpos) - token = self.tokens.pop(0) - return token - - def nextline(self): - ''' - Fetches a next line that ends either with \\r or \\n. - ''' - linebuf = '' - linepos = self.bufpos + self.charpos - eol = False - while 1: - self.fillbuf() - if eol: - c = self.buf[self.charpos] - # handle '\r\n' - if c == '\n': - linebuf += c - self.charpos += 1 - break - m = EOL.search(self.buf, self.charpos) - if m: - linebuf += self.buf[self.charpos:m.end(0)] - self.charpos = m.end(0) - if linebuf[-1] == '\r': - eol = True - else: - break - else: - linebuf += self.buf[self.charpos:] - self.charpos = len(self.buf) - return (linepos, linebuf) - - def revreadlines(self): - ''' - Fetches a next line backword. This is used to locate - the trailers at the end of a file. - ''' - self.fp.seek(0, 2) - pos = self.fp.tell() - buf = '' - while 0 < pos: - prevpos = pos - pos = max(0, pos-self.BUFSIZ) - self.fp.seek(pos) - s = self.fp.read(prevpos-pos) - if not s: break - while 1: - n = max(s.rfind('\r'), s.rfind('\n')) - if n == -1: - buf = s + buf - break - yield s[n:]+buf - s = s[:n] - buf = '' - return - - -## PSStackParser -## -class PSStackParser(PSBaseParser): - - def __init__(self, fp): - PSBaseParser.__init__(self, fp) - self.reset() - return - - def reset(self): - self.context = [] - self.curtype = None - self.curstack = [] - self.results = [] - return - - def seek(self, pos): - PSBaseParser.seek(self, pos) - self.reset() - return - - def push(self, *objs): - self.curstack.extend(objs) - return - def pop(self, n): - objs = self.curstack[-n:] - self.curstack[-n:] = [] - return objs - def popall(self): - objs = self.curstack - self.curstack = [] - return objs - def add_results(self, *objs): - self.results.extend(objs) - return - - def start_type(self, pos, type): - self.context.append((pos, self.curtype, self.curstack)) - (self.curtype, self.curstack) = (type, []) - return - def end_type(self, type): - if self.curtype != type: - raise PSTypeError('Type mismatch: %r != %r' % (self.curtype, type)) - objs = [ obj for (_,obj) in self.curstack ] - (pos, self.curtype, self.curstack) = self.context.pop() - return (pos, objs) - - def do_keyword(self, pos, token): - return - - def nextobject(self, direct=False): - ''' - Yields a list of objects: keywords, literals, strings, - numbers, arrays and dictionaries. Arrays and dictionaries - are represented as Python sequence and dictionaries. - ''' - while not self.results: - (pos, token) = self.nexttoken() - ##print (pos,token), (self.curtype, self.curstack) - if (isinstance(token, int) or - isinstance(token, float) or - isinstance(token, bool) or - isinstance(token, str) or - isinstance(token, PSLiteral)): - # normal token - self.push((pos, token)) - elif token == KEYWORD_ARRAY_BEGIN: - # begin array - self.start_type(pos, 'a') - elif token == KEYWORD_ARRAY_END: - # end array - try: - self.push(self.end_type('a')) - except PSTypeError: - if STRICT: raise - elif token == KEYWORD_DICT_BEGIN: - # begin dictionary - self.start_type(pos, 'd') - elif token == KEYWORD_DICT_END: - # end dictionary - try: - (pos, objs) = self.end_type('d') - if len(objs) % 2 != 0: - print "Incomplete dictionary construct" - objs.append("") # this isn't necessary. - # temporary fix. is this due to rental books? - # raise PSSyntaxError( - # 'Invalid dictionary construct: %r' % objs) - d = dict((literal_name(k), v) \ - for (k,v) in choplist(2, objs)) - self.push((pos, d)) - except PSTypeError: - if STRICT: raise - else: - self.do_keyword(pos, token) - if self.context: - continue - else: - if direct: - return self.pop(1)[0] - self.flush() - obj = self.results.pop(0) - return obj - - -LITERAL_CRYPT = PSLiteralTable.intern('Crypt') -LITERALS_FLATE_DECODE = (PSLiteralTable.intern('FlateDecode'), PSLiteralTable.intern('Fl')) -LITERALS_LZW_DECODE = (PSLiteralTable.intern('LZWDecode'), PSLiteralTable.intern('LZW')) -LITERALS_ASCII85_DECODE = (PSLiteralTable.intern('ASCII85Decode'), PSLiteralTable.intern('A85')) - - -## PDF Objects -## -class PDFObject(PSObject): pass - -class PDFException(PSException): pass -class PDFTypeError(PDFException): pass -class PDFValueError(PDFException): pass -class PDFNotImplementedError(PSException): pass - - -## PDFObjRef -## -class PDFObjRef(PDFObject): - - def __init__(self, doc, objid, genno): - if objid == 0: - if STRICT: - raise PDFValueError('PDF object id cannot be 0.') - self.doc = doc - self.objid = objid - self.genno = genno - return - - def __repr__(self): - return '' % (self.objid, self.genno) - - def resolve(self): - return self.doc.getobj(self.objid) - - -# resolve -def resolve1(x): - ''' - Resolve an object. If this is an array or dictionary, - it may still contains some indirect objects inside. - ''' - while isinstance(x, PDFObjRef): - x = x.resolve() - return x - -def resolve_all(x): - ''' - Recursively resolve X and all the internals. - Make sure there is no indirect reference within the nested object. - This procedure might be slow. - ''' - while isinstance(x, PDFObjRef): - x = x.resolve() - if isinstance(x, list): - x = [ resolve_all(v) for v in x ] - elif isinstance(x, dict): - for (k,v) in x.iteritems(): - x[k] = resolve_all(v) - return x - -def decipher_all(decipher, objid, genno, x): - ''' - Recursively decipher X. - ''' - if isinstance(x, str): - return decipher(objid, genno, x) - decf = lambda v: decipher_all(decipher, objid, genno, v) - if isinstance(x, list): - x = [decf(v) for v in x] - elif isinstance(x, dict): - x = dict((k, decf(v)) for (k, v) in x.iteritems()) - return x - - -# Type cheking -def int_value(x): - x = resolve1(x) - if not isinstance(x, int): - if STRICT: - raise PDFTypeError('Integer required: %r' % x) - return 0 - return x - -def float_value(x): - x = resolve1(x) - if not isinstance(x, float): - if STRICT: - raise PDFTypeError('Float required: %r' % x) - return 0.0 - return x - -def num_value(x): - x = resolve1(x) - if not (isinstance(x, int) or isinstance(x, float)): - if STRICT: - raise PDFTypeError('Int or Float required: %r' % x) - return 0 - return x - -def str_value(x): - x = resolve1(x) - if not isinstance(x, str): - if STRICT: - raise PDFTypeError('String required: %r' % x) - return '' - return x - -def list_value(x): - x = resolve1(x) - if not (isinstance(x, list) or isinstance(x, tuple)): - if STRICT: - raise PDFTypeError('List required: %r' % x) - return [] - return x - -def dict_value(x): - x = resolve1(x) - if not isinstance(x, dict): - if STRICT: - raise PDFTypeError('Dict required: %r' % x) - return {} - return x - -def stream_value(x): - x = resolve1(x) - if not isinstance(x, PDFStream): - if STRICT: - raise PDFTypeError('PDFStream required: %r' % x) - return PDFStream({}, '') - return x - -# ascii85decode(data) -def ascii85decode(data): - n = b = 0 - out = '' - for c in data: - if '!' <= c and c <= 'u': - n += 1 - b = b*85+(ord(c)-33) - if n == 5: - out += struct.pack('>L',b) - n = b = 0 - elif c == 'z': - assert n == 0 - out += '\0\0\0\0' - elif c == '~': - if n: - for _ in range(5-n): - b = b*85+84 - out += struct.pack('>L',b)[:n-1] - break - return out - - -## PDFStream type -class PDFStream(PDFObject): - def __init__(self, dic, rawdata, decipher=None): - length = int_value(dic.get('Length', 0)) - eol = rawdata[length:] - # quick and dirty fix for false length attribute, - # might not work if the pdf stream parser has a problem - if decipher != None and decipher.__name__ == 'decrypt_aes': - if (len(rawdata) % 16) != 0: - cutdiv = len(rawdata) // 16 - rawdata = rawdata[:16*cutdiv] - else: - if eol in ('\r', '\n', '\r\n'): - rawdata = rawdata[:length] - - self.dic = dic - self.rawdata = rawdata - self.decipher = decipher - self.data = None - self.decdata = None - self.objid = None - self.genno = None - return - - def set_objid(self, objid, genno): - self.objid = objid - self.genno = genno - return - - def __repr__(self): - if self.rawdata: - return '' % \ - (self.objid, len(self.rawdata), self.dic) - else: - return '' % \ - (self.objid, len(self.data), self.dic) - - def decode(self): - assert self.data is None and self.rawdata is not None - data = self.rawdata - if self.decipher: - # Handle encryption - data = self.decipher(self.objid, self.genno, data) - if gen_xref_stm: - self.decdata = data # keep decrypted data - if 'Filter' not in self.dic: - self.data = data - self.rawdata = None - ##print self.dict - return - filters = self.dic['Filter'] - if not isinstance(filters, list): - filters = [ filters ] - for f in filters: - if f in LITERALS_FLATE_DECODE: - # will get errors if the document is encrypted. - data = zlib.decompress(data) - elif f in LITERALS_LZW_DECODE: - data = ''.join(LZWDecoder(StringIO(data)).run()) - elif f in LITERALS_ASCII85_DECODE: - data = ascii85decode(data) - elif f == LITERAL_CRYPT: - raise PDFNotImplementedError('/Crypt filter is unsupported') - else: - raise PDFNotImplementedError('Unsupported filter: %r' % f) - # apply predictors - if 'DP' in self.dic: - params = self.dic['DP'] - else: - params = self.dic.get('DecodeParms', {}) - if 'Predictor' in params: - pred = int_value(params['Predictor']) - if pred: - if pred != 12: - raise PDFNotImplementedError( - 'Unsupported predictor: %r' % pred) - if 'Columns' not in params: - raise PDFValueError( - 'Columns undefined for predictor=12') - columns = int_value(params['Columns']) - buf = '' - ent0 = '\x00' * columns - for i in xrange(0, len(data), columns+1): - pred = data[i] - ent1 = data[i+1:i+1+columns] - if pred == '\x02': - ent1 = ''.join(chr((ord(a)+ord(b)) & 255) \ - for (a,b) in zip(ent0,ent1)) - buf += ent1 - ent0 = ent1 - data = buf - self.data = data - self.rawdata = None - return - - def get_data(self): - if self.data is None: - self.decode() - return self.data - - def get_rawdata(self): - return self.rawdata - - def get_decdata(self): - if self.decdata is not None: - return self.decdata - data = self.rawdata - if self.decipher and data: - # Handle encryption - data = self.decipher(self.objid, self.genno, data) + return (AES, RSA) + +AES, RSA = _load_crypto() + +META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml') +NSMAP = {'adept': 'http://ns.adobe.com/adept', + 'enc': 'http://www.w3.org/2001/04/xmlenc#'} + +class Decryptor(object): + def __init__(self, bookkey, encryption): + enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag) + self._aes = AES(bookkey) + encryption = etree.fromstring(encryption) + self._encrypted = encrypted = set() + expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'), + enc('CipherReference')) + for elem in encryption.findall(expr): + path = elem.get('URI', None) + if path is not None: + path = path.encode('utf-8') + encrypted.add(path) + + def decompress(self, bytes): + dc = zlib.decompressobj(-15) + bytes = dc.decompress(bytes) + ex = dc.decompress('Z') + dc.flush() + if ex: + bytes = bytes + ex + return bytes + + def decrypt(self, path, data): + if path in self._encrypted: + data = self._aes.decrypt(data)[16:] + data = data[:-ord(data[-1])] + data = self.decompress(data) return data - -## PDF Exceptions -## -class PDFSyntaxError(PDFException): pass -class PDFNoValidXRef(PDFSyntaxError): pass -class PDFEncryptionError(PDFException): pass -class PDFPasswordIncorrect(PDFEncryptionError): pass - -# some predefined literals and keywords. -LITERAL_OBJSTM = PSLiteralTable.intern('ObjStm') -LITERAL_XREF = PSLiteralTable.intern('XRef') -LITERAL_PAGE = PSLiteralTable.intern('Page') -LITERAL_PAGES = PSLiteralTable.intern('Pages') -LITERAL_CATALOG = PSLiteralTable.intern('Catalog') - - -## XRefs -## - -## PDFXRef -## -class PDFXRef(object): - - def __init__(self): - self.offsets = None - return - - def __repr__(self): - return '' % len(self.offsets) - - def objids(self): - return self.offsets.iterkeys() - - def load(self, parser): - self.offsets = {} - while 1: - try: - (pos, line) = parser.nextline() - except PSEOF: - raise PDFNoValidXRef('Unexpected EOF - file corrupted?') - if not line: - raise PDFNoValidXRef('Premature eof: %r' % parser) - if line.startswith('trailer'): - parser.seek(pos) - break - f = line.strip().split(' ') - if len(f) != 2: - raise PDFNoValidXRef('Trailer not found: %r: line=%r' % (parser, line)) - try: - (start, nobjs) = map(int, f) - except ValueError: - raise PDFNoValidXRef('Invalid line: %r: line=%r' % (parser, line)) - for objid in xrange(start, start+nobjs): - try: - (_, line) = parser.nextline() - except PSEOF: - raise PDFNoValidXRef('Unexpected EOF - file corrupted?') - f = line.strip().split(' ') - if len(f) != 3: - raise PDFNoValidXRef('Invalid XRef format: %r, line=%r' % (parser, line)) - (pos, genno, use) = f - if use != 'n': continue - self.offsets[objid] = (int(genno), int(pos)) - self.load_trailer(parser) - return - - KEYWORD_TRAILER = PSKeywordTable.intern('trailer') - def load_trailer(self, parser): +# check file to make check whether it's probably an Adobe Adept encrypted ePub +def adeptBook(inpath): + with closing(ZipFile(open(inpath, 'rb'))) as inf: + namelist = set(inf.namelist()) + if 'META-INF/rights.xml' not in namelist or \ + 'META-INF/encryption.xml' not in namelist: + return False try: - (_,kwd) = parser.nexttoken() - assert kwd is self.KEYWORD_TRAILER - (_,dic) = parser.nextobject(direct=True) - except PSEOF: - x = parser.pop(1) - if not x: - raise PDFNoValidXRef('Unexpected EOF - file corrupted') - (_,dic) = x[0] - self.trailer = dict_value(dic) - return - - def getpos(self, objid): + rights = etree.fromstring(inf.read('META-INF/rights.xml')) + adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) + expr = './/%s' % (adept('encryptedKey'),) + bookkey = ''.join(rights.findtext(expr)) + if len(bookkey) == 172: + return True + except: + # if we couldn't check, assume it is + return True + return False + +def decryptBook(userkey, inpath, outpath): + if AES is None: + raise ADEPTError(u"PyCrypto or OpenSSL must be installed.") + rsa = RSA(userkey) + with closing(ZipFile(open(inpath, 'rb'))) as inf: + namelist = set(inf.namelist()) + if 'META-INF/rights.xml' not in namelist or \ + 'META-INF/encryption.xml' not in namelist: + print u"{0:s} is DRM-free.".format(os.path.basename(inpath)) + return 1 + for name in META_NAMES: + namelist.remove(name) try: - (genno, pos) = self.offsets[objid] - except KeyError: - raise - return (None, pos) - - -## PDFXRefStream -## -class PDFXRefStream(object): - - def __init__(self): - self.index = None - self.data = None - self.entlen = None - self.fl1 = self.fl2 = self.fl3 = None - return - - def __repr__(self): - return '' % self.index - - def objids(self): - for first, size in self.index: - for objid in xrange(first, first + size): - yield objid - - def load(self, parser, debug=0): - (_,objid) = parser.nexttoken() # ignored - (_,genno) = parser.nexttoken() # ignored - (_,kwd) = parser.nexttoken() - (_,stream) = parser.nextobject() - if not isinstance(stream, PDFStream) or \ - stream.dic['Type'] is not LITERAL_XREF: - raise PDFNoValidXRef('Invalid PDF stream spec.') - size = stream.dic['Size'] - index = stream.dic.get('Index', (0,size)) - self.index = zip(islice(index, 0, None, 2), - islice(index, 1, None, 2)) - (self.fl1, self.fl2, self.fl3) = stream.dic['W'] - self.data = stream.get_data() - self.entlen = self.fl1+self.fl2+self.fl3 - self.trailer = stream.dic - return - - def getpos(self, objid): - offset = 0 - for first, size in self.index: - if first <= objid and objid < (first + size): - break - offset += size - else: - raise KeyError(objid) - i = self.entlen * ((objid - first) + offset) - ent = self.data[i:i+self.entlen] - f1 = nunpack(ent[:self.fl1], 1) - if f1 == 1: - pos = nunpack(ent[self.fl1:self.fl1+self.fl2]) - genno = nunpack(ent[self.fl1+self.fl2:]) - return (None, pos) - elif f1 == 2: - objid = nunpack(ent[self.fl1:self.fl1+self.fl2]) - index = nunpack(ent[self.fl1+self.fl2:]) - return (objid, index) - # this is a free object - raise KeyError(objid) - - -## PDFDocument -## -## A PDFDocument object represents a PDF document. -## Since a PDF file is usually pretty big, normally it is not loaded -## at once. Rather it is parsed dynamically as processing goes. -## A PDF parser is associated with the document. -## -class PDFDocument(object): - - def __init__(self): - self.xrefs = [] - self.objs = {} - self.parsed_objs = {} - self.root = None - self.catalog = None - self.parser = None - self.encryption = None - self.decipher = None - return - - # set_parser(parser) - # Associates the document with an (already initialized) parser object. - def set_parser(self, parser): - if self.parser: return - self.parser = parser - # The document is set to be temporarily ready during collecting - # all the basic information about the document, e.g. - # the header, the encryption information, and the access rights - # for the document. - self.ready = True - # Retrieve the information of each header that was appended - # (maybe multiple times) at the end of the document. - self.xrefs = parser.read_xref() - for xref in self.xrefs: - trailer = xref.trailer - if not trailer: continue - - # If there's an encryption info, remember it. - if 'Encrypt' in trailer: - #assert not self.encryption + rights = etree.fromstring(inf.read('META-INF/rights.xml')) + adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) + expr = './/%s' % (adept('encryptedKey'),) + bookkey = ''.join(rights.findtext(expr)) + if len(bookkey) != 172: + print u"{0:s} is not a secure Adobe Adept ePub.".format(os.path.basename(inpath)) + return 1 + bookkey = rsa.decrypt(bookkey.decode('base64')) + # Padded as per RSAES-PKCS1-v1_5 + if bookkey[-17] != '\x00': + print u"Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath)) + return 2 + encryption = inf.read('META-INF/encryption.xml') + decryptor = Decryptor(bookkey[-16:], encryption) + kwds = dict(compression=ZIP_DEFLATED, allowZip64=False) + with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf: + zi = ZipInfo('mimetype') + zi.compress_type=ZIP_STORED try: - self.encryption = (list_value(trailer['ID']), - dict_value(trailer['Encrypt'])) - # fix for bad files + # if the mimetype is present, get its info, including time-stamp + oldzi = inf.getinfo('mimetype') + # copy across fields to be preserved + zi.date_time = oldzi.date_time + zi.comment = oldzi.comment + zi.extra = oldzi.extra + zi.internal_attr = oldzi.internal_attr + # external attributes are dependent on the create system, so copy both. + zi.external_attr = oldzi.external_attr + zi.create_system = oldzi.create_system except: - self.encryption = ('ffffffffffffffffffffffffffffffffffff', - dict_value(trailer['Encrypt'])) - if 'Root' in trailer: - self.set_root(dict_value(trailer['Root'])) - break - else: - raise PDFSyntaxError('No /Root object! - Is this really a PDF?') - # The document is set to be non-ready again, until all the - # proper initialization (asking the password key and - # verifying the access permission, so on) is finished. - self.ready = False - return - - # set_root(root) - # Set the Root dictionary of the document. - # Each PDF file must have exactly one /Root dictionary. - def set_root(self, root): - self.root = root - self.catalog = dict_value(self.root) - if self.catalog.get('Type') is not LITERAL_CATALOG: - if STRICT: - raise PDFSyntaxError('Catalog not found!') - return - # initialize(password='') - # Perform the initialization with a given password. - # This step is mandatory even if there's no password associated - # with the document. - def initialize(self, password=''): - if not self.encryption: - self.is_printable = self.is_modifiable = self.is_extractable = True - self.ready = True - return - (docid, param) = self.encryption - type = literal_name(param['Filter']) - if type == 'Adobe.APS': - return self.initialize_adobe_ps(password, docid, param) - if type == 'Standard': - return self.initialize_standard(password, docid, param) - if type == 'EBX_HANDLER': - return self.initialize_ebx(password, docid, param) - raise PDFEncryptionError('Unknown filter: param=%r' % param) - - def initialize_adobe_ps(self, password, docid, param): - global KEYFILEPATH - self.decrypt_key = self.genkey_adobe_ps(param) - self.genkey = self.genkey_v4 - self.decipher = self.decrypt_aes - self.ready = True - return - - def genkey_adobe_ps(self, param): - # nice little offline principal keys dictionary - # global static principal key for German Onleihe / Bibliothek Digital - principalkeys = { 'bibliothek-digital.de': 'rRwGv2tbpKov1krvv7PO0ws9S436/lArPlfipz5Pqhw='.decode('base64')} - self.is_printable = self.is_modifiable = self.is_extractable = True - length = int_value(param.get('Length', 0)) / 8 - edcdata = str_value(param.get('EDCData')).decode('base64') - pdrllic = str_value(param.get('PDRLLic')).decode('base64') - pdrlpol = str_value(param.get('PDRLPol')).decode('base64') - edclist = [] - for pair in edcdata.split('\n'): - edclist.append(pair) - # principal key request - for key in principalkeys: - if key in pdrllic: - principalkey = principalkeys[key] - else: - raise ADEPTError('Cannot find principal key for this pdf') - shakey = SHA256(principalkey) - ivector = 16 * chr(0) - plaintext = AES.new(shakey,AES.MODE_CBC,ivector).decrypt(edclist[9].decode('base64')) - if plaintext[-16:] != 16 * chr(16): - raise ADEPTError('Offlinekey cannot be decrypted, aborting ...') - pdrlpol = AES.new(plaintext[16:32],AES.MODE_CBC,edclist[2].decode('base64')).decrypt(pdrlpol) - if ord(pdrlpol[-1]) < 1 or ord(pdrlpol[-1]) > 16: - raise ADEPTError('Could not decrypt PDRLPol, aborting ...') - else: - cutter = -1 * ord(pdrlpol[-1]) - pdrlpol = pdrlpol[:cutter] - return plaintext[:16] - - PASSWORD_PADDING = '(\xbfN^Nu\x8aAd\x00NV\xff\xfa\x01\x08..' \ - '\x00\xb6\xd0h>\x80/\x0c\xa9\xfedSiz' - # experimental aes pw support - def initialize_standard(self, password, docid, param): - # copy from a global variable - V = int_value(param.get('V', 0)) - if (V <=0 or V > 4): - raise PDFEncryptionError('Unknown algorithm: param=%r' % param) - length = int_value(param.get('Length', 40)) # Key length (bits) - O = str_value(param['O']) - R = int_value(param['R']) # Revision - if 5 <= R: - raise PDFEncryptionError('Unknown revision: %r' % R) - U = str_value(param['U']) - P = int_value(param['P']) - try: - EncMetadata = str_value(param['EncryptMetadata']) - except: - EncMetadata = 'True' - self.is_printable = bool(P & 4) - self.is_modifiable = bool(P & 8) - self.is_extractable = bool(P & 16) - self.is_annotationable = bool(P & 32) - self.is_formsenabled = bool(P & 256) - self.is_textextractable = bool(P & 512) - self.is_assemblable = bool(P & 1024) - self.is_formprintable = bool(P & 2048) - # Algorithm 3.2 - password = (password+self.PASSWORD_PADDING)[:32] # 1 - hash = hashlib.md5(password) # 2 - hash.update(O) # 3 - hash.update(struct.pack('= 3: - # Algorithm 3.5 - hash = hashlib.md5(self.PASSWORD_PADDING) # 2 - hash.update(docid[0]) # 3 - x = ARC4.new(key).decrypt(hash.digest()[:16]) # 4 - for i in xrange(1,19+1): - k = ''.join( chr(ord(c) ^ i) for c in key ) - x = ARC4.new(k).decrypt(x) - u1 = x+x # 32bytes total - if R == 2: - is_authenticated = (u1 == U) - else: - is_authenticated = (u1[:16] == U[:16]) - if not is_authenticated: - raise ADEPTError('Password is not correct.') - self.decrypt_key = key - # genkey method - if V == 1 or V == 2: - self.genkey = self.genkey_v2 - elif V == 3: - self.genkey = self.genkey_v3 - elif V == 4: - self.genkey = self.genkey_v2 - #self.genkey = self.genkey_v3 if V == 3 else self.genkey_v2 - # rc4 - if V != 4: - self.decipher = self.decipher_rc4 # XXX may be AES - # aes - elif V == 4 and Length == 128: - elf.decipher = self.decipher_aes - elif V == 4 and Length == 256: - raise PDFNotImplementedError('AES256 encryption is currently unsupported') - self.ready = True - return - - def initialize_ebx(self, password, docid, param): - self.is_printable = self.is_modifiable = self.is_extractable = True - rsa = RSA(password) - length = int_value(param.get('Length', 0)) / 8 - rights = str_value(param.get('ADEPT_LICENSE')).decode('base64') - rights = zlib.decompress(rights, -15) - rights = etree.fromstring(rights) - expr = './/{http://ns.adobe.com/adept}encryptedKey' - bookkey = ''.join(rights.findtext(expr)).decode('base64') - bookkey = rsa.decrypt(bookkey) - if bookkey[0] != '\x02': - raise ADEPTError('error decrypting book session key') - index = bookkey.index('\0') + 1 - bookkey = bookkey[index:] - ebx_V = int_value(param.get('V', 4)) - ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6)) - # added because of improper booktype / decryption book session key errors - if length > 0: - if len(bookkey) == length: - if ebx_V == 3: - V = 3 - else: - V = 2 - elif len(bookkey) == length + 1: - V = ord(bookkey[0]) - bookkey = bookkey[1:] - else: - print "ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type) - print "length is %d and len(bookkey) is %d" % (length, len(bookkey)) - print "bookkey[0] is %d" % ord(bookkey[0]) - raise ADEPTError('error decrypting book session key - mismatched length') - else: - # proper length unknown try with whatever you have - print "ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type) - print "length is %d and len(bookkey) is %d" % (length, len(bookkey)) - print "bookkey[0] is %d" % ord(bookkey[0]) - if ebx_V == 3: - V = 3 - else: - V = 2 - self.decrypt_key = bookkey - self.genkey = self.genkey_v3 if V == 3 else self.genkey_v2 - self.decipher = self.decrypt_rc4 - self.ready = True - return - - # genkey functions - def genkey_v2(self, objid, genno): - objid = struct.pack(' PDFObjStmRef.maxindex: - PDFObjStmRef.maxindex = index - - -## PDFParser -## -class PDFParser(PSStackParser): - - def __init__(self, doc, fp): - PSStackParser.__init__(self, fp) - self.doc = doc - self.doc.set_parser(self) - return - - def __repr__(self): - return '' - - KEYWORD_R = PSKeywordTable.intern('R') - KEYWORD_ENDOBJ = PSKeywordTable.intern('endobj') - KEYWORD_STREAM = PSKeywordTable.intern('stream') - KEYWORD_XREF = PSKeywordTable.intern('xref') - KEYWORD_STARTXREF = PSKeywordTable.intern('startxref') - def do_keyword(self, pos, token): - if token in (self.KEYWORD_XREF, self.KEYWORD_STARTXREF): - self.add_results(*self.pop(1)) - return - if token is self.KEYWORD_ENDOBJ: - self.add_results(*self.pop(4)) - return - - if token is self.KEYWORD_R: - # reference to indirect object - try: - ((_,objid), (_,genno)) = self.pop(2) - (objid, genno) = (int(objid), int(genno)) - obj = PDFObjRef(self.doc, objid, genno) - self.push((pos, obj)) - except PSSyntaxError: - pass - return - - if token is self.KEYWORD_STREAM: - # stream object - ((_,dic),) = self.pop(1) - dic = dict_value(dic) - try: - objlen = int_value(dic['Length']) - except KeyError: - if STRICT: - raise PDFSyntaxError('/Length is undefined: %r' % dic) - objlen = 0 - self.seek(pos) - try: - (_, line) = self.nextline() # 'stream' - except PSEOF: - if STRICT: - raise PDFSyntaxError('Unexpected EOF') - return - pos += len(line) - self.fp.seek(pos) - data = self.fp.read(objlen) - self.seek(pos+objlen) - while 1: - try: - (linepos, line) = self.nextline() - except PSEOF: - if STRICT: - raise PDFSyntaxError('Unexpected EOF') - break - if 'endstream' in line: - i = line.index('endstream') - objlen += i - data += line[:i] - break - objlen += len(line) - data += line - self.seek(pos+objlen) - obj = PDFStream(dic, data, self.doc.decipher) - self.push((pos, obj)) - return - - # others - self.push((pos, token)) - return - - def find_xref(self): - # search the last xref table by scanning the file backwards. - prev = None - for line in self.revreadlines(): - line = line.strip() - if line == 'startxref': break - if line: - prev = line - else: - raise PDFNoValidXRef('Unexpected EOF') - return int(prev) - - # read xref table - def read_xref_from(self, start, xrefs): - self.seek(start) - self.reset() - try: - (pos, token) = self.nexttoken() - except PSEOF: - raise PDFNoValidXRef('Unexpected EOF') - if isinstance(token, int): - # XRefStream: PDF-1.5 - if GEN_XREF_STM == 1: - global gen_xref_stm - gen_xref_stm = True - self.seek(pos) - self.reset() - xref = PDFXRefStream() - xref.load(self) - else: - if token is not self.KEYWORD_XREF: - raise PDFNoValidXRef('xref not found: pos=%d, token=%r' % - (pos, token)) - self.nextline() - xref = PDFXRef() - xref.load(self) - xrefs.append(xref) - trailer = xref.trailer - if 'XRefStm' in trailer: - pos = int_value(trailer['XRefStm']) - self.read_xref_from(pos, xrefs) - if 'Prev' in trailer: - # find previous xref - pos = int_value(trailer['Prev']) - self.read_xref_from(pos, xrefs) - return - - # read xref tables and trailers - def read_xref(self): - xrefs = [] - trailerpos = None - try: - pos = self.find_xref() - self.read_xref_from(pos, xrefs) - except PDFNoValidXRef: - # fallback - self.seek(0) - pat = re.compile(r'^(\d+)\s+(\d+)\s+obj\b') - offsets = {} - xref = PDFXRef() - while 1: - try: - (pos, line) = self.nextline() - except PSEOF: - break - if line.startswith('trailer'): - trailerpos = pos # remember last trailer - m = pat.match(line) - if not m: continue - (objid, genno) = m.groups() - offsets[int(objid)] = (0, pos) - if not offsets: raise - xref.offsets = offsets - if trailerpos: - self.seek(trailerpos) - xref.load_trailer(self) - xrefs.append(xref) - return xrefs - -## PDFObjStrmParser -## -class PDFObjStrmParser(PDFParser): - - def __init__(self, data, doc): - PSStackParser.__init__(self, StringIO(data)) - self.doc = doc - return - - def flush(self): - self.add_results(*self.popall()) - return - - KEYWORD_R = KWD('R') - def do_keyword(self, pos, token): - if token is self.KEYWORD_R: - # reference to indirect object - try: - ((_,objid), (_,genno)) = self.pop(2) - (objid, genno) = (int(objid), int(genno)) - obj = PDFObjRef(self.doc, objid, genno) - self.push((pos, obj)) - except PSSyntaxError: - pass - return - # others - self.push((pos, token)) - return - -### -### My own code, for which there is none else to blame - -class PDFSerializer(object): - def __init__(self, inf, userkey): - global GEN_XREF_STM, gen_xref_stm - gen_xref_stm = GEN_XREF_STM > 1 - self.version = inf.read(8) - inf.seek(0) - self.doc = doc = PDFDocument() - parser = PDFParser(doc, inf) - doc.initialize(userkey) - self.objids = objids = set() - for xref in reversed(doc.xrefs): - trailer = xref.trailer - for objid in xref.objids(): - objids.add(objid) - trailer = dict(trailer) - trailer.pop('Prev', None) - trailer.pop('XRefStm', None) - if 'Encrypt' in trailer: - objids.remove(trailer.pop('Encrypt').objid) - self.trailer = trailer - - def dump(self, outf): - self.outf = outf - self.write(self.version) - self.write('\n%\xe2\xe3\xcf\xd3\n') - doc = self.doc - objids = self.objids - xrefs = {} - maxobj = max(objids) - trailer = dict(self.trailer) - trailer['Size'] = maxobj + 1 - for objid in objids: - obj = doc.getobj(objid) - if isinstance(obj, PDFObjStmRef): - xrefs[objid] = obj - continue - if obj is not None: - try: - genno = obj.genno - except AttributeError: - genno = 0 - xrefs[objid] = (self.tell(), genno) - self.serialize_indirect(objid, obj) - startxref = self.tell() - - if not gen_xref_stm: - self.write('xref\n') - self.write('0 %d\n' % (maxobj + 1,)) - for objid in xrange(0, maxobj + 1): - if objid in xrefs: - # force the genno to be 0 - self.write("%010d 00000 n \n" % xrefs[objid][0]) - else: - self.write("%010d %05d f \n" % (0, 65535)) - - self.write('trailer\n') - self.serialize_object(trailer) - self.write('\nstartxref\n%d\n%%%%EOF' % startxref) - - else: # Generate crossref stream. - - # Calculate size of entries - maxoffset = max(startxref, maxobj) - maxindex = PDFObjStmRef.maxindex - fl2 = 2 - power = 65536 - while maxoffset >= power: - fl2 += 1 - power *= 256 - fl3 = 1 - power = 256 - while maxindex >= power: - fl3 += 1 - power *= 256 - - index = [] - first = None - prev = None - data = [] - # Put the xrefstream's reference in itself - startxref = self.tell() - maxobj += 1 - xrefs[maxobj] = (startxref, 0) - for objid in sorted(xrefs): - if first is None: - first = objid - elif objid != prev + 1: - index.extend((first, prev - first + 1)) - first = objid - prev = objid - objref = xrefs[objid] - if isinstance(objref, PDFObjStmRef): - f1 = 2 - f2 = objref.stmid - f3 = objref.index - else: - f1 = 1 - f2 = objref[0] - # we force all generation numbers to be 0 - # f3 = objref[1] - f3 = 0 - - data.append(struct.pack('>B', f1)) - data.append(struct.pack('>L', f2)[-fl2:]) - data.append(struct.pack('>L', f3)[-fl3:]) - index.extend((first, prev - first + 1)) - data = zlib.compress(''.join(data)) - dic = {'Type': LITERAL_XREF, 'Size': prev + 1, 'Index': index, - 'W': [1, fl2, fl3], 'Length': len(data), - 'Filter': LITERALS_FLATE_DECODE[0], - 'Root': trailer['Root'],} - if 'Info' in trailer: - dic['Info'] = trailer['Info'] - xrefstm = PDFStream(dic, data) - self.serialize_indirect(maxobj, xrefstm) - self.write('startxref\n%d\n%%%%EOF' % startxref) - def write(self, data): - self.outf.write(data) - self.last = data[-1:] - - def tell(self): - return self.outf.tell() - - def escape_string(self, string): - string = string.replace('\\', '\\\\') - string = string.replace('\n', r'\n') - string = string.replace('(', r'\(') - string = string.replace(')', r'\)') - # get rid of ciando id - regularexp = re.compile(r'http://www.ciando.com/index.cfm/intRefererID/\d{5}') - if regularexp.match(string): return ('http://www.ciando.com') - return string - - def serialize_object(self, obj): - if isinstance(obj, dict): - # Correct malformed Mac OS resource forks for Stanza - if 'ResFork' in obj and 'Type' in obj and 'Subtype' not in obj \ - and isinstance(obj['Type'], int): - obj['Subtype'] = obj['Type'] - del obj['Type'] - # end - hope this doesn't have bad effects - self.write('<<') - for key, val in obj.items(): - self.write('/%s' % key) - self.serialize_object(val) - self.write('>>') - elif isinstance(obj, list): - self.write('[') - for val in obj: - self.serialize_object(val) - self.write(']') - elif isinstance(obj, str): - self.write('(%s)' % self.escape_string(obj)) - elif isinstance(obj, bool): - if self.last.isalnum(): - self.write(' ') - self.write(str(obj).lower()) - elif isinstance(obj, (int, long, float)): - if self.last.isalnum(): - self.write(' ') - self.write(str(obj)) - elif isinstance(obj, PDFObjRef): - if self.last.isalnum(): - self.write(' ') - self.write('%d %d R' % (obj.objid, 0)) - elif isinstance(obj, PDFStream): - ### If we don't generate cross ref streams the object streams - ### are no longer useful, as we have extracted all objects from - ### them. Therefore leave them out from the output. - if obj.dic.get('Type') == LITERAL_OBJSTM and not gen_xref_stm: - self.write('(deleted)') - else: - data = obj.get_decdata() - self.serialize_object(obj.dic) - self.write('stream\n') - self.write(data) - self.write('\nendstream') - else: - data = str(obj) - if data[0].isalnum() and self.last.isalnum(): - self.write(' ') - self.write(data) - - def serialize_indirect(self, objid, obj): - self.write('%d 0 obj' % (objid,)) - self.serialize_object(obj) - if self.last.isalnum(): - self.write('\n') - self.write('endobj\n') - - - - -def decryptBook(userkey, inpath, outpath): - if RSA is None: - raise ADEPTError(u"PyCrypto or OpenSSL must be installed.") - with open(inpath, 'rb') as inf: - try: - serializer = PDFSerializer(inf, userkey) + outf.writestr(zi, decryptor.decrypt(path, data)) except: - print u"Error serializing pdf {0}. Probably wrong key.".format(os.path.basename(inpath)) + print u"Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc()) return 2 - # hope this will fix the 'bad file descriptor' problem - with open(outpath, 'wb') as outf: - # help construct to make sure the method runs to the end - try: - serializer.dump(outf) - except Exception, e: - print u"error writing pdf: {0}".format(e.args[0]) - return 2 return 0 @@ -2197,7 +466,7 @@ def cli_main(): argv=unicode_argv() progname = os.path.basename(argv[0]) if len(argv) != 4: - print u"usage: {0} ".format(progname) + print u"usage: {0} ".format(progname) return 1 keypath, inpath, outpath = argv[1:] userkey = open(keypath,'rb').read() @@ -2206,7 +475,6 @@ def cli_main(): print u"Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath)) return result - def gui_main(): try: import Tkinter @@ -2266,8 +534,8 @@ def gui_main(): def get_inpath(self): inpath = tkFileDialog.askopenfilename( - parent=None, title=u"Select ADEPT-encrypted PDF file to decrypt", - defaultextension=u".pdf", filetypes=[('PDF files', '.pdf')]) + parent=None, title=u"Select ADEPT-encrypted ePub file to decrypt", + defaultextension=u".epub", filetypes=[('ePub files', '.epub')]) if inpath: inpath = os.path.normpath(inpath) self.inpath.delete(0, Tkconstants.END) @@ -2276,8 +544,8 @@ def gui_main(): def get_outpath(self): outpath = tkFileDialog.asksaveasfilename( - parent=None, title=u"Select unencrypted PDF file to produce", - defaultextension=u".pdf", filetypes=[('PDF files', '.pdf')]) + parent=None, title=u"Select unencrypted ePub file to produce", + defaultextension=u".epub", filetypes=[('ePub files', '.epub')]) if outpath: outpath = os.path.normpath(outpath) self.outpath.delete(0, Tkconstants.END) @@ -2305,30 +573,21 @@ def gui_main(): try: decrypt_status = decryptBook(userkey, inpath, outpath) except Exception, e: - self.status['text'] = u"Error; {0}".format(e.args[0]) + self.status['text'] = u"Error: {0}".format(e.args[0]) return if decrypt_status == 0: self.status['text'] = u"File successfully decrypted" else: self.status['text'] = u"The was an error decrypting the file." - root = Tkinter.Tk() - if RSA is None: - root.withdraw() - tkMessageBox.showerror( - "INEPT PDF", - "This script requires OpenSSL or PyCrypto, which must be installed " - "separately. Read the top-of-script comment for details.") - return 1 - root.title(u"Adobe Adept PDF Decrypter v.{0}".format(__version__)) + root.title(u"Adobe Adept ePub Decrypter v.{0}".format(__version__)) root.resizable(True, False) - root.minsize(370, 0) + root.minsize(300, 0) DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1) root.mainloop() return 0 - if __name__ == '__main__': if len(sys.argv) > 1: sys.exit(cli_main()) diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/ineptpdf.py b/DeDRM_calibre_plugin/DeDRM_plugin/ineptpdf.py index 929ce57..1986e20 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/ineptpdf.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/ineptpdf.py @@ -1,89 +1,74 @@ -#!/usr/bin/env python +#! /usr/bin/python # -*- coding: utf-8 -*- from __future__ import with_statement -# ignobleepub.pyw, version 3.6 -# Copyright © 2009-2012 by DiapDealer et al. - -# engine to remove drm from Kindle for Mac and Kindle for PC books -# for personal use for archiving and converting your ebooks - -# PLEASE DO NOT PIRATE EBOOKS! - -# We want all authors and publishers, and eBook stores to live -# long and prosperous lives but at the same time we just want to -# be able to read OUR books on whatever device we want and to keep -# readable for a long, long time - -# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle, -# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates -# and many many others -# Special thanks to The Dark Reverser for MobiDeDrm and CMBDTC for cmbdtc_dump -# from which this script borrows most unashamedly. - - -# Changelog -# 1.0 - Name change to k4mobidedrm. Adds Mac support, Adds plugin code -# 1.1 - Adds support for additional kindle.info files -# 1.2 - Better error handling for older Mobipocket -# 1.3 - Don't try to decrypt Topaz books -# 1.7 - Add support for Topaz books and Kindle serial numbers. Split code. -# 1.9 - Tidy up after Topaz, minor exception changes -# 2.1 - Topaz fix and filename sanitizing -# 2.2 - Topaz Fix and minor Mac code fix -# 2.3 - More Topaz fixes -# 2.4 - K4PC/Mac key generation fix -# 2.6 - Better handling of non-K4PC/Mac ebooks -# 2.7 - Better trailing bytes handling in mobidedrm -# 2.8 - Moved parsing of kindle.info files to mac & pc util files. -# 3.1 - Updated for new calibre interface. Now __init__ in plugin. -# 3.5 - Now support Kindle for PC/Mac 1.6 -# 3.6 - Even better trailing bytes handling in mobidedrm -# 3.7 - Add support for Amazon Print Replica ebooks. -# 3.8 - Improved Topaz support -# 4.1 - Improved Topaz support and faster decryption with alfcrypto -# 4.2 - Added support for Amazon's KF8 format ebooks -# 4.4 - Linux calls to Wine added, and improved configuration dialog -# 4.5 - Linux works again without Wine. Some Mac key file search changes -# 4.6 - First attempt to handle unicode properly -# 4.7 - Added timing reports, and changed search for Mac key files -# 4.8 - Much better unicode handling, matching the updated inept and ignoble scripts -# - Moved back into plugin, __init__ in plugin now only contains plugin code. -# 4.9 - Missed some invalid characters in cleanup_name -# 5.0 - Extraction of info from Kindle for PC/Mac moved into kindlekey.py -# - tweaked GetDecryptedBook interface to leave passed parameters unchanged -# 5.1 - moved unicode_argv call inside main for Windows DeDRM compatibility -# 5.2 - Fixed error in command line processing of unicode arguments - -__version__ = '5.2' - - -import sys, os, re -import csv -import getopt -import re -import traceback -import time -import htmlentitydefs -import json +# ineptpdf.pyw, version 7.11 +# Copyright © 2009-2010 by i♥cabbages -class DrmException(Exception): - pass +# Released under the terms of the GNU General Public Licence, version 3 +# + +# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf + +# Windows users: Before running this program, you must first install Python 2.6 +# from and PyCrypto from +# (make sure to +# install the version for Python 2.6). Save this script file as +# ineptpdf.pyw and double-click on it to run it. +# +# Mac OS X users: Save this script file as ineptpdf.pyw. You can run this +# program from the command line (pythonw ineptpdf.pyw) or by double-clicking +# it when it has been associated with PythonLauncher. -if 'calibre' in sys.modules: - inCalibre = True -else: - inCalibre = False +# Revision history: +# 1 - Initial release +# 2 - Improved determination of key-generation algorithm +# 3 - Correctly handle PDF >=1.5 cross-reference streams +# 4 - Removal of ciando's personal ID +# 5 - Automated decryption of a complete directory +# 6.1 - backward compatibility for 1.7.1 and old adeptkey.der +# 7 - Get cross reference streams and object streams working for input. +# Not yet supported on output but this only effects file size, +# not functionality. (anon2) +# 7.1 - Correct a problem when an old trailer is not followed by startxref +# 7.2 - Correct malformed Mac OS resource forks for Stanza (anon2) +# - Support for cross ref streams on output (decreases file size) +# 7.3 - Correct bug in trailer with cross ref stream that caused the error +# "The root object is missing or invalid" in Adobe Reader. (anon2) +# 7.4 - Force all generation numbers in output file to be 0, like in v6. +# Fallback code for wrong xref improved (search till last trailer +# instead of first) (anon2) +# 7.5 - allow support for OpenSSL to replace pycrypto on all platforms +# implemented ARC4 interface to OpenSSL +# fixed minor typos +# 7.6 - backported AES and other fixes from version 8.4.48 +# 7.7 - On Windows try PyCrypto first and OpenSSL next +# 7.8 - Modify interface to allow use of import +# 7.9 - Bug fix for some session key errors when len(bookkey) > length required +# 7.10 - Various tweaks to fix minor problems. +# 7.11 - More tweaks to fix minor problems. +# 7.12 - Revised to allow use in calibre plugins to eliminate need for duplicate code +# 7.13 - Fixed erroneous mentions of ineptepub +# 7.14 - moved unicode_argv call inside main for Windows DeDRM compatibility +# 8.0 - Work if TkInter is missing +# 8.0.1 - Broken Metadata fix. -if inCalibre: - from calibre_plugins.dedrm import mobidedrm - from calibre_plugins.dedrm import topazextract - from calibre_plugins.dedrm import kgenpids -else: - import mobidedrm - import topazextract - import kgenpids +""" +Decrypts Adobe ADEPT-encrypted PDF files. +""" + +__license__ = 'GPL v3' +__version__ = "8.0.1" + +import sys +import os +import re +import zlib +import struct +import hashlib +from itertools import chain, islice +import xml.etree.ElementTree as etree # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get @@ -134,192 +119,2217 @@ def unicode_argv(): start = argc.value - len(sys.argv) return [argv[i] for i in xrange(start, argc.value)] - # if we don't have any arguments at all, just pass back script name - # this should never happen - return [u"mobidedrm.py"] + return [u"ineptpdf.py"] else: argvencoding = sys.stdin.encoding if argvencoding == None: argvencoding = "utf-8" return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv] -# cleanup unicode filenames -# borrowed from calibre from calibre/src/calibre/__init__.py -# added in removal of control (<32) chars -# and removal of . at start and end -# and with some (heavily edited) code from Paul Durrant's kindlenamer.py -def cleanup_name(name): - # substitute filename unfriendly characters - name = name.replace(u"<",u"[").replace(u">",u"]").replace(u" : ",u" – ").replace(u": ",u" – ").replace(u":",u"—").replace(u"/",u"_").replace(u"\\",u"_").replace(u"|",u"_").replace(u"\"",u"\'").replace(u"*",u"_").replace(u"?",u"") - # delete control characters - name = u"".join(char for char in name if ord(char)>=32) - # white space to single space, delete leading and trailing while space - name = re.sub(ur"\s", u" ", name).strip() - # remove leading dots - while len(name)>0 and name[0] == u".": - name = name[1:] - # remove trailing dots (Windows doesn't like them) - if name.endswith(u'.'): - name = name[:-1] - return name - -# must be passed unicode -def unescape(text): - def fixup(m): - text = m.group(0) - if text[:2] == u"&#": - # character reference - try: - if text[:3] == u"&#x": - return unichr(int(text[3:-1], 16)) + +class ADEPTError(Exception): + pass + + +import hashlib + +def SHA256(message): + ctx = hashlib.sha256() + ctx.update(message) + return ctx.digest() + + +def _load_crypto_libcrypto(): + from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \ + Structure, c_ulong, create_string_buffer, cast + from ctypes.util import find_library + + if sys.platform.startswith('win'): + libcrypto = find_library('libeay32') + else: + libcrypto = find_library('crypto') + + if libcrypto is None: + raise ADEPTError('libcrypto not found') + libcrypto = CDLL(libcrypto) + + AES_MAXNR = 14 + + RSA_NO_PADDING = 3 + + 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) + + class RC4_KEY(Structure): + _fields_ = [('x', c_int), ('y', c_int), ('box', c_int * 256)] + RC4_KEY_p = POINTER(RC4_KEY) + + class RSA(Structure): + pass + RSA_p = POINTER(RSA) + + def F(restype, name, argtypes): + func = getattr(libcrypto, name) + func.restype = restype + func.argtypes = argtypes + return func + + AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,c_int]) + AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',[c_char_p, c_int, AES_KEY_p]) + + RC4_set_key = F(None,'RC4_set_key',[RC4_KEY_p, c_int, c_char_p]) + RC4_crypt = F(None,'RC4',[RC4_KEY_p, c_int, c_char_p, c_char_p]) + + d2i_RSAPrivateKey = F(RSA_p, 'd2i_RSAPrivateKey', + [RSA_p, c_char_pp, c_long]) + RSA_size = F(c_int, 'RSA_size', [RSA_p]) + RSA_private_decrypt = F(c_int, 'RSA_private_decrypt', + [c_int, c_char_p, c_char_p, RSA_p, c_int]) + RSA_free = F(None, 'RSA_free', [RSA_p]) + + class RSA(object): + def __init__(self, der): + buf = create_string_buffer(der) + pp = c_char_pp(cast(buf, c_char_p)) + rsa = self._rsa = d2i_RSAPrivateKey(None, pp, len(der)) + if rsa is None: + raise ADEPTError('Error parsing ADEPT user key DER') + + def decrypt(self, from_): + rsa = self._rsa + to = create_string_buffer(RSA_size(rsa)) + dlen = RSA_private_decrypt(len(from_), from_, to, rsa, + RSA_NO_PADDING) + if dlen < 0: + raise ADEPTError('RSA decryption failed') + return to[1:dlen] + + def __del__(self): + if self._rsa is not None: + RSA_free(self._rsa) + self._rsa = None + + class ARC4(object): + @classmethod + def new(cls, userkey): + self = ARC4() + self._blocksize = len(userkey) + key = self._key = RC4_KEY() + RC4_set_key(key, self._blocksize, userkey) + return self + def __init__(self): + self._blocksize = 0 + self._key = None + def decrypt(self, data): + out = create_string_buffer(len(data)) + RC4_crypt(self._key, len(data), data, out) + return out.raw + + class AES(object): + MODE_CBC = 0 + @classmethod + def new(cls, userkey, mode, iv): + self = AES() + self._blocksize = len(userkey) + # mode is ignored since CBCMODE is only thing supported/used so far + self._mode = mode + if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : + raise ADEPTError('AES improper key used') + return + keyctx = self._keyctx = AES_KEY() + self._iv = iv + rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx) + if rv < 0: + raise ADEPTError('Failed to initialize AES key') + return self + def __init__(self): + self._blocksize = 0 + self._keyctx = None + self._iv = 0 + self._mode = 0 + def decrypt(self, data): + out = create_string_buffer(len(data)) + rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self._iv, 0) + if rv == 0: + raise ADEPTError('AES decryption failed') + return out.raw + + return (ARC4, RSA, AES) + + +def _load_crypto_pycrypto(): + from Crypto.PublicKey import RSA as _RSA + from Crypto.Cipher import ARC4 as _ARC4 + from Crypto.Cipher import AES as _AES + + # ASN.1 parsing code from tlslite + class ASN1Error(Exception): + pass + + class ASN1Parser(object): + class Parser(object): + def __init__(self, bytes): + self.bytes = bytes + self.index = 0 + + def get(self, length): + if self.index + length > len(self.bytes): + raise ASN1Error("Error decoding ASN.1") + x = 0 + for count in range(length): + x <<= 8 + x |= self.bytes[self.index] + self.index += 1 + return x + + def getFixBytes(self, lengthBytes): + bytes = self.bytes[self.index : self.index+lengthBytes] + self.index += lengthBytes + return bytes + + def getVarBytes(self, lengthLength): + lengthBytes = self.get(lengthLength) + return self.getFixBytes(lengthBytes) + + def getFixList(self, length, lengthList): + l = [0] * lengthList + for x in range(lengthList): + l[x] = self.get(length) + return l + + def getVarList(self, length, lengthLength): + lengthList = self.get(lengthLength) + if lengthList % length != 0: + raise ASN1Error("Error decoding ASN.1") + lengthList = int(lengthList/length) + l = [0] * lengthList + for x in range(lengthList): + l[x] = self.get(length) + return l + + def startLengthCheck(self, lengthLength): + self.lengthCheck = self.get(lengthLength) + self.indexCheck = self.index + + def setLengthCheck(self, length): + self.lengthCheck = length + self.indexCheck = self.index + + def stopLengthCheck(self): + if (self.index - self.indexCheck) != self.lengthCheck: + raise ASN1Error("Error decoding ASN.1") + + def atLengthCheck(self): + if (self.index - self.indexCheck) < self.lengthCheck: + return False + elif (self.index - self.indexCheck) == self.lengthCheck: + return True else: - return unichr(int(text[2:-1])) + raise ASN1Error("Error decoding ASN.1") + + def __init__(self, bytes): + p = self.Parser(bytes) + p.get(1) + self.length = self._getASN1Length(p) + self.value = p.getFixBytes(self.length) + + def getChild(self, which): + p = self.Parser(self.value) + for x in range(which+1): + markIndex = p.index + p.get(1) + length = self._getASN1Length(p) + p.getFixBytes(length) + return ASN1Parser(p.bytes[markIndex:p.index]) + + def _getASN1Length(self, p): + firstLength = p.get(1) + if firstLength<=127: + return firstLength + else: + lengthLength = firstLength & 0x7F + return p.get(lengthLength) + + class ARC4(object): + @classmethod + def new(cls, userkey): + self = ARC4() + self._arc4 = _ARC4.new(userkey) + return self + def __init__(self): + self._arc4 = None + def decrypt(self, data): + return self._arc4.decrypt(data) + + class AES(object): + MODE_CBC = _AES.MODE_CBC + @classmethod + def new(cls, userkey, mode, iv): + self = AES() + self._aes = _AES.new(userkey, mode, iv) + return self + def __init__(self): + self._aes = None + def decrypt(self, data): + return self._aes.decrypt(data) + + class RSA(object): + def __init__(self, der): + key = ASN1Parser([ord(x) for x in der]) + key = [key.getChild(x).value for x in xrange(1, 4)] + key = [self.bytesToNumber(v) for v in key] + self._rsa = _RSA.construct(key) + + def bytesToNumber(self, bytes): + total = 0L + for byte in bytes: + total = (total << 8) + byte + return total + + def decrypt(self, data): + return self._rsa.decrypt(data) + + return (ARC4, RSA, AES) + +def _load_crypto(): + ARC4 = RSA = AES = None + cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto) + if sys.platform.startswith('win'): + cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto) + for loader in cryptolist: + try: + ARC4, RSA, AES = loader() + break + except (ImportError, ADEPTError): + pass + return (ARC4, RSA, AES) +ARC4, RSA, AES = _load_crypto() + + +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + + +# Do we generate cross reference streams on output? +# 0 = never +# 1 = only if present in input +# 2 = always + +GEN_XREF_STM = 1 + +# This is the value for the current document +gen_xref_stm = False # will be set in PDFSerializer + +# PDF parsing routines from pdfminer, with changes for EBX_HANDLER + +# Utilities + +def choplist(n, seq): + '''Groups every n elements of the list.''' + r = [] + for x in seq: + r.append(x) + if len(r) == n: + yield tuple(r) + r = [] + return + +def nunpack(s, default=0): + '''Unpacks up to 4 bytes big endian.''' + l = len(s) + if not l: + return default + elif l == 1: + return ord(s) + elif l == 2: + return struct.unpack('>H', s)[0] + elif l == 3: + return struct.unpack('>L', '\x00'+s)[0] + elif l == 4: + return struct.unpack('>L', s)[0] + else: + return TypeError('invalid length: %d' % l) + + +STRICT = 0 + + +# PS Exceptions + +class PSException(Exception): pass +class PSEOF(PSException): pass +class PSSyntaxError(PSException): pass +class PSTypeError(PSException): pass +class PSValueError(PSException): pass + + +# Basic PostScript Types + + +# PSLiteral +class PSObject(object): pass + +class PSLiteral(PSObject): + ''' + PS literals (e.g. "/Name"). + Caution: Never create these objects directly. + Use PSLiteralTable.intern() instead. + ''' + def __init__(self, name): + self.name = name + return + + def __repr__(self): + name = [] + for char in self.name: + if not char.isalnum(): + char = '#%02x' % ord(char) + name.append(char) + return '/%s' % ''.join(name) + +# PSKeyword +class PSKeyword(PSObject): + ''' + PS keywords (e.g. "showpage"). + Caution: Never create these objects directly. + Use PSKeywordTable.intern() instead. + ''' + def __init__(self, name): + self.name = name + return + + def __repr__(self): + return self.name + +# PSSymbolTable +class PSSymbolTable(object): + + ''' + Symbol table that stores PSLiteral or PSKeyword. + ''' + + def __init__(self, classe): + self.dic = {} + self.classe = classe + return + + def intern(self, name): + if name in self.dic: + lit = self.dic[name] + else: + lit = self.classe(name) + self.dic[name] = lit + return lit + +PSLiteralTable = PSSymbolTable(PSLiteral) +PSKeywordTable = PSSymbolTable(PSKeyword) +LIT = PSLiteralTable.intern +KWD = PSKeywordTable.intern +KEYWORD_BRACE_BEGIN = KWD('{') +KEYWORD_BRACE_END = KWD('}') +KEYWORD_ARRAY_BEGIN = KWD('[') +KEYWORD_ARRAY_END = KWD(']') +KEYWORD_DICT_BEGIN = KWD('<<') +KEYWORD_DICT_END = KWD('>>') + + +def literal_name(x): + if not isinstance(x, PSLiteral): + if STRICT: + raise PSTypeError('Literal required: %r' % x) + else: + return str(x) + return x.name + +def keyword_name(x): + if not isinstance(x, PSKeyword): + if STRICT: + raise PSTypeError('Keyword required: %r' % x) + else: + return str(x) + return x.name + + +## PSBaseParser +## +EOL = re.compile(r'[\r\n]') +SPC = re.compile(r'\s') +NONSPC = re.compile(r'\S') +HEX = re.compile(r'[0-9a-fA-F]') +END_LITERAL = re.compile(r'[#/%\[\]()<>{}\s]') +END_HEX_STRING = re.compile(r'[^\s0-9a-fA-F]') +HEX_PAIR = re.compile(r'[0-9a-fA-F]{2}|.') +END_NUMBER = re.compile(r'[^0-9]') +END_KEYWORD = re.compile(r'[#/%\[\]()<>{}\s]') +END_STRING = re.compile(r'[()\134]') +OCT_STRING = re.compile(r'[0-7]') +ESC_STRING = { 'b':8, 't':9, 'n':10, 'f':12, 'r':13, '(':40, ')':41, '\\':92 } + +class PSBaseParser(object): + + ''' + Most basic PostScript parser that performs only basic tokenization. + ''' + BUFSIZ = 4096 + + def __init__(self, fp): + self.fp = fp + self.seek(0) + return + + def __repr__(self): + return '' % (self.fp, self.bufpos) + + def flush(self): + return + + def close(self): + self.flush() + return + + def tell(self): + return self.bufpos+self.charpos + + def poll(self, pos=None, n=80): + pos0 = self.fp.tell() + if not pos: + pos = self.bufpos+self.charpos + self.fp.seek(pos) + ##print >>sys.stderr, 'poll(%d): %r' % (pos, self.fp.read(n)) + self.fp.seek(pos0) + return + + def seek(self, pos): + ''' + Seeks the parser to the given position. + ''' + self.fp.seek(pos) + # reset the status for nextline() + self.bufpos = pos + self.buf = '' + self.charpos = 0 + # reset the status for nexttoken() + self.parse1 = self.parse_main + self.tokens = [] + return + + def fillbuf(self): + if self.charpos < len(self.buf): return + # fetch next chunk. + self.bufpos = self.fp.tell() + self.buf = self.fp.read(self.BUFSIZ) + if not self.buf: + raise PSEOF('Unexpected EOF') + self.charpos = 0 + return + + def parse_main(self, s, i): + m = NONSPC.search(s, i) + if not m: + return (self.parse_main, len(s)) + j = m.start(0) + c = s[j] + self.tokenstart = self.bufpos+j + if c == '%': + self.token = '%' + return (self.parse_comment, j+1) + if c == '/': + self.token = '' + return (self.parse_literal, j+1) + if c in '-+' or c.isdigit(): + self.token = c + return (self.parse_number, j+1) + if c == '.': + self.token = c + return (self.parse_float, j+1) + if c.isalpha(): + self.token = c + return (self.parse_keyword, j+1) + if c == '(': + self.token = '' + self.paren = 1 + return (self.parse_string, j+1) + if c == '<': + self.token = '' + return (self.parse_wopen, j+1) + if c == '>': + self.token = '' + return (self.parse_wclose, j+1) + self.add_token(KWD(c)) + return (self.parse_main, j+1) + + def add_token(self, obj): + self.tokens.append((self.tokenstart, obj)) + return + + def parse_comment(self, s, i): + m = EOL.search(s, i) + if not m: + self.token += s[i:] + return (self.parse_comment, len(s)) + j = m.start(0) + self.token += s[i:j] + # We ignore comments. + #self.tokens.append(self.token) + return (self.parse_main, j) + + def parse_literal(self, s, i): + m = END_LITERAL.search(s, i) + if not m: + self.token += s[i:] + return (self.parse_literal, len(s)) + j = m.start(0) + self.token += s[i:j] + c = s[j] + if c == '#': + self.hex = '' + return (self.parse_literal_hex, j+1) + self.add_token(LIT(self.token)) + return (self.parse_main, j) + + def parse_literal_hex(self, s, i): + c = s[i] + if HEX.match(c) and len(self.hex) < 2: + self.hex += c + return (self.parse_literal_hex, i+1) + if self.hex: + self.token += chr(int(self.hex, 16)) + return (self.parse_literal, i) + + def parse_number(self, s, i): + m = END_NUMBER.search(s, i) + if not m: + self.token += s[i:] + return (self.parse_number, len(s)) + j = m.start(0) + self.token += s[i:j] + c = s[j] + if c == '.': + self.token += c + return (self.parse_float, j+1) + try: + self.add_token(int(self.token)) + except ValueError: + pass + return (self.parse_main, j) + def parse_float(self, s, i): + m = END_NUMBER.search(s, i) + if not m: + self.token += s[i:] + return (self.parse_float, len(s)) + j = m.start(0) + self.token += s[i:j] + self.add_token(float(self.token)) + return (self.parse_main, j) + + def parse_keyword(self, s, i): + m = END_KEYWORD.search(s, i) + if not m: + self.token += s[i:] + return (self.parse_keyword, len(s)) + j = m.start(0) + self.token += s[i:j] + if self.token == 'true': + token = True + elif self.token == 'false': + token = False + else: + token = KWD(self.token) + self.add_token(token) + return (self.parse_main, j) + + def parse_string(self, s, i): + m = END_STRING.search(s, i) + if not m: + self.token += s[i:] + return (self.parse_string, len(s)) + j = m.start(0) + self.token += s[i:j] + c = s[j] + if c == '\\': + self.oct = '' + return (self.parse_string_1, j+1) + if c == '(': + self.paren += 1 + self.token += c + return (self.parse_string, j+1) + if c == ')': + self.paren -= 1 + if self.paren: + self.token += c + return (self.parse_string, j+1) + self.add_token(self.token) + return (self.parse_main, j+1) + def parse_string_1(self, s, i): + c = s[i] + if OCT_STRING.match(c) and len(self.oct) < 3: + self.oct += c + return (self.parse_string_1, i+1) + if self.oct: + self.token += chr(int(self.oct, 8)) + return (self.parse_string, i) + if c in ESC_STRING: + self.token += chr(ESC_STRING[c]) + return (self.parse_string, i+1) + + def parse_wopen(self, s, i): + c = s[i] + if c.isspace() or HEX.match(c): + return (self.parse_hexstring, i) + if c == '<': + self.add_token(KEYWORD_DICT_BEGIN) + i += 1 + return (self.parse_main, i) + + def parse_wclose(self, s, i): + c = s[i] + if c == '>': + self.add_token(KEYWORD_DICT_END) + i += 1 + return (self.parse_main, i) + + def parse_hexstring(self, s, i): + m = END_HEX_STRING.search(s, i) + if not m: + self.token += s[i:] + return (self.parse_hexstring, len(s)) + j = m.start(0) + self.token += s[i:j] + token = HEX_PAIR.sub(lambda m: chr(int(m.group(0), 16)), + SPC.sub('', self.token)) + self.add_token(token) + return (self.parse_main, j) + + def nexttoken(self): + while not self.tokens: + self.fillbuf() + (self.parse1, self.charpos) = self.parse1(self.buf, self.charpos) + token = self.tokens.pop(0) + return token + + def nextline(self): + ''' + Fetches a next line that ends either with \\r or \\n. + ''' + linebuf = '' + linepos = self.bufpos + self.charpos + eol = False + while 1: + self.fillbuf() + if eol: + c = self.buf[self.charpos] + # handle '\r\n' + if c == '\n': + linebuf += c + self.charpos += 1 + break + m = EOL.search(self.buf, self.charpos) + if m: + linebuf += self.buf[self.charpos:m.end(0)] + self.charpos = m.end(0) + if linebuf[-1] == '\r': + eol = True + else: + break + else: + linebuf += self.buf[self.charpos:] + self.charpos = len(self.buf) + return (linepos, linebuf) + + def revreadlines(self): + ''' + Fetches a next line backword. This is used to locate + the trailers at the end of a file. + ''' + self.fp.seek(0, 2) + pos = self.fp.tell() + buf = '' + while 0 < pos: + prevpos = pos + pos = max(0, pos-self.BUFSIZ) + self.fp.seek(pos) + s = self.fp.read(prevpos-pos) + if not s: break + while 1: + n = max(s.rfind('\r'), s.rfind('\n')) + if n == -1: + buf = s + buf + break + yield s[n:]+buf + s = s[:n] + buf = '' + return + + +## PSStackParser +## +class PSStackParser(PSBaseParser): + + def __init__(self, fp): + PSBaseParser.__init__(self, fp) + self.reset() + return + + def reset(self): + self.context = [] + self.curtype = None + self.curstack = [] + self.results = [] + return + + def seek(self, pos): + PSBaseParser.seek(self, pos) + self.reset() + return + + def push(self, *objs): + self.curstack.extend(objs) + return + def pop(self, n): + objs = self.curstack[-n:] + self.curstack[-n:] = [] + return objs + def popall(self): + objs = self.curstack + self.curstack = [] + return objs + def add_results(self, *objs): + self.results.extend(objs) + return + + def start_type(self, pos, type): + self.context.append((pos, self.curtype, self.curstack)) + (self.curtype, self.curstack) = (type, []) + return + def end_type(self, type): + if self.curtype != type: + raise PSTypeError('Type mismatch: %r != %r' % (self.curtype, type)) + objs = [ obj for (_,obj) in self.curstack ] + (pos, self.curtype, self.curstack) = self.context.pop() + return (pos, objs) + + def do_keyword(self, pos, token): + return + + def nextobject(self, direct=False): + ''' + Yields a list of objects: keywords, literals, strings, + numbers, arrays and dictionaries. Arrays and dictionaries + are represented as Python sequence and dictionaries. + ''' + while not self.results: + (pos, token) = self.nexttoken() + ##print (pos,token), (self.curtype, self.curstack) + if (isinstance(token, int) or + isinstance(token, float) or + isinstance(token, bool) or + isinstance(token, str) or + isinstance(token, PSLiteral)): + # normal token + self.push((pos, token)) + elif token == KEYWORD_ARRAY_BEGIN: + # begin array + self.start_type(pos, 'a') + elif token == KEYWORD_ARRAY_END: + # end array + try: + self.push(self.end_type('a')) + except PSTypeError: + if STRICT: raise + elif token == KEYWORD_DICT_BEGIN: + # begin dictionary + self.start_type(pos, 'd') + elif token == KEYWORD_DICT_END: + # end dictionary + try: + (pos, objs) = self.end_type('d') + if len(objs) % 2 != 0: + print "Incomplete dictionary construct" + objs.append("") # this isn't necessary. + # temporary fix. is this due to rental books? + # raise PSSyntaxError( + # 'Invalid dictionary construct: %r' % objs) + d = dict((literal_name(k), v) \ + for (k,v) in choplist(2, objs)) + self.push((pos, d)) + except PSTypeError: + if STRICT: raise + else: + self.do_keyword(pos, token) + if self.context: + continue + else: + if direct: + return self.pop(1)[0] + self.flush() + obj = self.results.pop(0) + return obj + + +LITERAL_CRYPT = PSLiteralTable.intern('Crypt') +LITERALS_FLATE_DECODE = (PSLiteralTable.intern('FlateDecode'), PSLiteralTable.intern('Fl')) +LITERALS_LZW_DECODE = (PSLiteralTable.intern('LZWDecode'), PSLiteralTable.intern('LZW')) +LITERALS_ASCII85_DECODE = (PSLiteralTable.intern('ASCII85Decode'), PSLiteralTable.intern('A85')) + + +## PDF Objects +## +class PDFObject(PSObject): pass + +class PDFException(PSException): pass +class PDFTypeError(PDFException): pass +class PDFValueError(PDFException): pass +class PDFNotImplementedError(PSException): pass + + +## PDFObjRef +## +class PDFObjRef(PDFObject): + + def __init__(self, doc, objid, genno): + if objid == 0: + if STRICT: + raise PDFValueError('PDF object id cannot be 0.') + self.doc = doc + self.objid = objid + self.genno = genno + return + + def __repr__(self): + return '' % (self.objid, self.genno) + + def resolve(self): + return self.doc.getobj(self.objid) + + +# resolve +def resolve1(x): + ''' + Resolve an object. If this is an array or dictionary, + it may still contains some indirect objects inside. + ''' + while isinstance(x, PDFObjRef): + x = x.resolve() + return x + +def resolve_all(x): + ''' + Recursively resolve X and all the internals. + Make sure there is no indirect reference within the nested object. + This procedure might be slow. + ''' + while isinstance(x, PDFObjRef): + x = x.resolve() + if isinstance(x, list): + x = [ resolve_all(v) for v in x ] + elif isinstance(x, dict): + for (k,v) in x.iteritems(): + x[k] = resolve_all(v) + return x + +def decipher_all(decipher, objid, genno, x): + ''' + Recursively decipher X. + ''' + if isinstance(x, str): + return decipher(objid, genno, x) + decf = lambda v: decipher_all(decipher, objid, genno, v) + if isinstance(x, list): + x = [decf(v) for v in x] + elif isinstance(x, dict): + x = dict((k, decf(v)) for (k, v) in x.iteritems()) + return x + + +# Type cheking +def int_value(x): + x = resolve1(x) + if not isinstance(x, int): + if STRICT: + raise PDFTypeError('Integer required: %r' % x) + return 0 + return x + +def float_value(x): + x = resolve1(x) + if not isinstance(x, float): + if STRICT: + raise PDFTypeError('Float required: %r' % x) + return 0.0 + return x + +def num_value(x): + x = resolve1(x) + if not (isinstance(x, int) or isinstance(x, float)): + if STRICT: + raise PDFTypeError('Int or Float required: %r' % x) + return 0 + return x + +def str_value(x): + x = resolve1(x) + if not isinstance(x, str): + if STRICT: + raise PDFTypeError('String required: %r' % x) + return '' + return x + +def list_value(x): + x = resolve1(x) + if not (isinstance(x, list) or isinstance(x, tuple)): + if STRICT: + raise PDFTypeError('List required: %r' % x) + return [] + return x + +def dict_value(x): + x = resolve1(x) + if not isinstance(x, dict): + if STRICT: + raise PDFTypeError('Dict required: %r' % x) + return {} + return x + +def stream_value(x): + x = resolve1(x) + if not isinstance(x, PDFStream): + if STRICT: + raise PDFTypeError('PDFStream required: %r' % x) + return PDFStream({}, '') + return x + +# ascii85decode(data) +def ascii85decode(data): + n = b = 0 + out = '' + for c in data: + if '!' <= c and c <= 'u': + n += 1 + b = b*85+(ord(c)-33) + if n == 5: + out += struct.pack('>L',b) + n = b = 0 + elif c == 'z': + assert n == 0 + out += '\0\0\0\0' + elif c == '~': + if n: + for _ in range(5-n): + b = b*85+84 + out += struct.pack('>L',b)[:n-1] + break + return out + + +## PDFStream type +class PDFStream(PDFObject): + def __init__(self, dic, rawdata, decipher=None): + length = int_value(dic.get('Length', 0)) + eol = rawdata[length:] + # quick and dirty fix for false length attribute, + # might not work if the pdf stream parser has a problem + if decipher != None and decipher.__name__ == 'decrypt_aes': + if (len(rawdata) % 16) != 0: + cutdiv = len(rawdata) // 16 + rawdata = rawdata[:16*cutdiv] + else: + if eol in ('\r', '\n', '\r\n'): + rawdata = rawdata[:length] + + self.dic = dic + self.rawdata = rawdata + self.decipher = decipher + self.data = None + self.decdata = None + self.objid = None + self.genno = None + return + + def set_objid(self, objid, genno): + self.objid = objid + self.genno = genno + return + + def __repr__(self): + if self.rawdata: + return '' % \ + (self.objid, len(self.rawdata), self.dic) + else: + return '' % \ + (self.objid, len(self.data), self.dic) + + def decode(self): + assert self.data is None and self.rawdata is not None + data = self.rawdata + if self.decipher: + # Handle encryption + data = self.decipher(self.objid, self.genno, data) + if gen_xref_stm: + self.decdata = data # keep decrypted data + if 'Filter' not in self.dic: + self.data = data + self.rawdata = None + ##print self.dict + return + filters = self.dic['Filter'] + if not isinstance(filters, list): + filters = [ filters ] + for f in filters: + if f in LITERALS_FLATE_DECODE: + # will get errors if the document is encrypted. + data = zlib.decompress(data) + elif f in LITERALS_LZW_DECODE: + data = ''.join(LZWDecoder(StringIO(data)).run()) + elif f in LITERALS_ASCII85_DECODE: + data = ascii85decode(data) + elif f == LITERAL_CRYPT: + raise PDFNotImplementedError('/Crypt filter is unsupported') + else: + raise PDFNotImplementedError('Unsupported filter: %r' % f) + # apply predictors + if 'DP' in self.dic: + params = self.dic['DP'] + else: + params = self.dic.get('DecodeParms', {}) + if 'Predictor' in params: + pred = int_value(params['Predictor']) + if pred: + if pred != 12: + raise PDFNotImplementedError( + 'Unsupported predictor: %r' % pred) + if 'Columns' not in params: + raise PDFValueError( + 'Columns undefined for predictor=12') + columns = int_value(params['Columns']) + buf = '' + ent0 = '\x00' * columns + for i in xrange(0, len(data), columns+1): + pred = data[i] + ent1 = data[i+1:i+1+columns] + if pred == '\x02': + ent1 = ''.join(chr((ord(a)+ord(b)) & 255) \ + for (a,b) in zip(ent0,ent1)) + buf += ent1 + ent0 = ent1 + data = buf + self.data = data + self.rawdata = None + return + + def get_data(self): + if self.data is None: + self.decode() + return self.data + + def get_rawdata(self): + return self.rawdata + + def get_decdata(self): + if self.decdata is not None: + return self.decdata + data = self.rawdata + if self.decipher and data: + # Handle encryption + data = self.decipher(self.objid, self.genno, data) + return data + + +## PDF Exceptions +## +class PDFSyntaxError(PDFException): pass +class PDFNoValidXRef(PDFSyntaxError): pass +class PDFEncryptionError(PDFException): pass +class PDFPasswordIncorrect(PDFEncryptionError): pass + +# some predefined literals and keywords. +LITERAL_OBJSTM = PSLiteralTable.intern('ObjStm') +LITERAL_XREF = PSLiteralTable.intern('XRef') +LITERAL_PAGE = PSLiteralTable.intern('Page') +LITERAL_PAGES = PSLiteralTable.intern('Pages') +LITERAL_CATALOG = PSLiteralTable.intern('Catalog') + + +## XRefs +## + +## PDFXRef +## +class PDFXRef(object): + + def __init__(self): + self.offsets = None + return + + def __repr__(self): + return '' % len(self.offsets) + + def objids(self): + return self.offsets.iterkeys() + + def load(self, parser): + self.offsets = {} + while 1: + try: + (pos, line) = parser.nextline() + except PSEOF: + raise PDFNoValidXRef('Unexpected EOF - file corrupted?') + if not line: + raise PDFNoValidXRef('Premature eof: %r' % parser) + if line.startswith('trailer'): + parser.seek(pos) + break + f = line.strip().split(' ') + if len(f) != 2: + raise PDFNoValidXRef('Trailer not found: %r: line=%r' % (parser, line)) + try: + (start, nobjs) = map(int, f) except ValueError: - pass + raise PDFNoValidXRef('Invalid line: %r: line=%r' % (parser, line)) + for objid in xrange(start, start+nobjs): + try: + (_, line) = parser.nextline() + except PSEOF: + raise PDFNoValidXRef('Unexpected EOF - file corrupted?') + f = line.strip().split(' ') + if len(f) != 3: + raise PDFNoValidXRef('Invalid XRef format: %r, line=%r' % (parser, line)) + (pos, genno, use) = f + if use != 'n': continue + self.offsets[objid] = (int(genno), int(pos)) + self.load_trailer(parser) + return + + KEYWORD_TRAILER = PSKeywordTable.intern('trailer') + def load_trailer(self, parser): + try: + (_,kwd) = parser.nexttoken() + assert kwd is self.KEYWORD_TRAILER + (_,dic) = parser.nextobject(direct=True) + except PSEOF: + x = parser.pop(1) + if not x: + raise PDFNoValidXRef('Unexpected EOF - file corrupted') + (_,dic) = x[0] + self.trailer = dict_value(dic) + return + + def getpos(self, objid): + try: + (genno, pos) = self.offsets[objid] + except KeyError: + raise + return (None, pos) + + +## PDFXRefStream +## +class PDFXRefStream(object): + + def __init__(self): + self.index = None + self.data = None + self.entlen = None + self.fl1 = self.fl2 = self.fl3 = None + return + + def __repr__(self): + return '' % self.index + + def objids(self): + for first, size in self.index: + for objid in xrange(first, first + size): + yield objid + + def load(self, parser, debug=0): + (_,objid) = parser.nexttoken() # ignored + (_,genno) = parser.nexttoken() # ignored + (_,kwd) = parser.nexttoken() + (_,stream) = parser.nextobject() + if not isinstance(stream, PDFStream) or \ + stream.dic['Type'] is not LITERAL_XREF: + raise PDFNoValidXRef('Invalid PDF stream spec.') + size = stream.dic['Size'] + index = stream.dic.get('Index', (0,size)) + self.index = zip(islice(index, 0, None, 2), + islice(index, 1, None, 2)) + (self.fl1, self.fl2, self.fl3) = stream.dic['W'] + self.data = stream.get_data() + self.entlen = self.fl1+self.fl2+self.fl3 + self.trailer = stream.dic + return + + def getpos(self, objid): + offset = 0 + for first, size in self.index: + if first <= objid and objid < (first + size): + break + offset += size + else: + raise KeyError(objid) + i = self.entlen * ((objid - first) + offset) + ent = self.data[i:i+self.entlen] + f1 = nunpack(ent[:self.fl1], 1) + if f1 == 1: + pos = nunpack(ent[self.fl1:self.fl1+self.fl2]) + genno = nunpack(ent[self.fl1+self.fl2:]) + return (None, pos) + elif f1 == 2: + objid = nunpack(ent[self.fl1:self.fl1+self.fl2]) + index = nunpack(ent[self.fl1+self.fl2:]) + return (objid, index) + # this is a free object + raise KeyError(objid) + + +## PDFDocument +## +## A PDFDocument object represents a PDF document. +## Since a PDF file is usually pretty big, normally it is not loaded +## at once. Rather it is parsed dynamically as processing goes. +## A PDF parser is associated with the document. +## +class PDFDocument(object): + + def __init__(self): + self.xrefs = [] + self.objs = {} + self.parsed_objs = {} + self.root = None + self.catalog = None + self.parser = None + self.encryption = None + self.decipher = None + return + + # set_parser(parser) + # Associates the document with an (already initialized) parser object. + def set_parser(self, parser): + if self.parser: return + self.parser = parser + # The document is set to be temporarily ready during collecting + # all the basic information about the document, e.g. + # the header, the encryption information, and the access rights + # for the document. + self.ready = True + # Retrieve the information of each header that was appended + # (maybe multiple times) at the end of the document. + self.xrefs = parser.read_xref() + for xref in self.xrefs: + trailer = xref.trailer + if not trailer: continue + + # If there's an encryption info, remember it. + if 'Encrypt' in trailer: + #assert not self.encryption + try: + self.encryption = (list_value(trailer['ID']), + dict_value(trailer['Encrypt'])) + # fix for bad files + except: + self.encryption = ('ffffffffffffffffffffffffffffffffffff', + dict_value(trailer['Encrypt'])) + if 'Root' in trailer: + self.set_root(dict_value(trailer['Root'])) + break + else: + raise PDFSyntaxError('No /Root object! - Is this really a PDF?') + # The document is set to be non-ready again, until all the + # proper initialization (asking the password key and + # verifying the access permission, so on) is finished. + self.ready = False + return + + # set_root(root) + # Set the Root dictionary of the document. + # Each PDF file must have exactly one /Root dictionary. + def set_root(self, root): + self.root = root + self.catalog = dict_value(self.root) + if self.catalog.get('Type') is not LITERAL_CATALOG: + if STRICT: + raise PDFSyntaxError('Catalog not found!') + return + # initialize(password='') + # Perform the initialization with a given password. + # This step is mandatory even if there's no password associated + # with the document. + def initialize(self, password=''): + if not self.encryption: + self.is_printable = self.is_modifiable = self.is_extractable = True + self.ready = True + return + (docid, param) = self.encryption + type = literal_name(param['Filter']) + if type == 'Adobe.APS': + return self.initialize_adobe_ps(password, docid, param) + if type == 'Standard': + return self.initialize_standard(password, docid, param) + if type == 'EBX_HANDLER': + return self.initialize_ebx(password, docid, param) + raise PDFEncryptionError('Unknown filter: param=%r' % param) + + def initialize_adobe_ps(self, password, docid, param): + global KEYFILEPATH + self.decrypt_key = self.genkey_adobe_ps(param) + self.genkey = self.genkey_v4 + self.decipher = self.decrypt_aes + self.ready = True + return + + def genkey_adobe_ps(self, param): + # nice little offline principal keys dictionary + # global static principal key for German Onleihe / Bibliothek Digital + principalkeys = { 'bibliothek-digital.de': 'rRwGv2tbpKov1krvv7PO0ws9S436/lArPlfipz5Pqhw='.decode('base64')} + self.is_printable = self.is_modifiable = self.is_extractable = True + length = int_value(param.get('Length', 0)) / 8 + edcdata = str_value(param.get('EDCData')).decode('base64') + pdrllic = str_value(param.get('PDRLLic')).decode('base64') + pdrlpol = str_value(param.get('PDRLPol')).decode('base64') + edclist = [] + for pair in edcdata.split('\n'): + edclist.append(pair) + # principal key request + for key in principalkeys: + if key in pdrllic: + principalkey = principalkeys[key] + else: + raise ADEPTError('Cannot find principal key for this pdf') + shakey = SHA256(principalkey) + ivector = 16 * chr(0) + plaintext = AES.new(shakey,AES.MODE_CBC,ivector).decrypt(edclist[9].decode('base64')) + if plaintext[-16:] != 16 * chr(16): + raise ADEPTError('Offlinekey cannot be decrypted, aborting ...') + pdrlpol = AES.new(plaintext[16:32],AES.MODE_CBC,edclist[2].decode('base64')).decrypt(pdrlpol) + if ord(pdrlpol[-1]) < 1 or ord(pdrlpol[-1]) > 16: + raise ADEPTError('Could not decrypt PDRLPol, aborting ...') + else: + cutter = -1 * ord(pdrlpol[-1]) + pdrlpol = pdrlpol[:cutter] + return plaintext[:16] + + PASSWORD_PADDING = '(\xbfN^Nu\x8aAd\x00NV\xff\xfa\x01\x08..' \ + '\x00\xb6\xd0h>\x80/\x0c\xa9\xfedSiz' + # experimental aes pw support + def initialize_standard(self, password, docid, param): + # copy from a global variable + V = int_value(param.get('V', 0)) + if (V <=0 or V > 4): + raise PDFEncryptionError('Unknown algorithm: param=%r' % param) + length = int_value(param.get('Length', 40)) # Key length (bits) + O = str_value(param['O']) + R = int_value(param['R']) # Revision + if 5 <= R: + raise PDFEncryptionError('Unknown revision: %r' % R) + U = str_value(param['U']) + P = int_value(param['P']) + try: + EncMetadata = str_value(param['EncryptMetadata']) + except: + EncMetadata = 'True' + self.is_printable = bool(P & 4) + self.is_modifiable = bool(P & 8) + self.is_extractable = bool(P & 16) + self.is_annotationable = bool(P & 32) + self.is_formsenabled = bool(P & 256) + self.is_textextractable = bool(P & 512) + self.is_assemblable = bool(P & 1024) + self.is_formprintable = bool(P & 2048) + # Algorithm 3.2 + password = (password+self.PASSWORD_PADDING)[:32] # 1 + hash = hashlib.md5(password) # 2 + hash.update(O) # 3 + hash.update(struct.pack('= 3: + # Algorithm 3.5 + hash = hashlib.md5(self.PASSWORD_PADDING) # 2 + hash.update(docid[0]) # 3 + x = ARC4.new(key).decrypt(hash.digest()[:16]) # 4 + for i in xrange(1,19+1): + k = ''.join( chr(ord(c) ^ i) for c in key ) + x = ARC4.new(k).decrypt(x) + u1 = x+x # 32bytes total + if R == 2: + is_authenticated = (u1 == U) + else: + is_authenticated = (u1[:16] == U[:16]) + if not is_authenticated: + raise ADEPTError('Password is not correct.') + self.decrypt_key = key + # genkey method + if V == 1 or V == 2: + self.genkey = self.genkey_v2 + elif V == 3: + self.genkey = self.genkey_v3 + elif V == 4: + self.genkey = self.genkey_v2 + #self.genkey = self.genkey_v3 if V == 3 else self.genkey_v2 + # rc4 + if V != 4: + self.decipher = self.decipher_rc4 # XXX may be AES + # aes + elif V == 4 and Length == 128: + elf.decipher = self.decipher_aes + elif V == 4 and Length == 256: + raise PDFNotImplementedError('AES256 encryption is currently unsupported') + self.ready = True + return + + def initialize_ebx(self, password, docid, param): + self.is_printable = self.is_modifiable = self.is_extractable = True + rsa = RSA(password) + length = int_value(param.get('Length', 0)) / 8 + rights = str_value(param.get('ADEPT_LICENSE')).decode('base64') + rights = zlib.decompress(rights, -15) + rights = etree.fromstring(rights) + expr = './/{http://ns.adobe.com/adept}encryptedKey' + bookkey = ''.join(rights.findtext(expr)).decode('base64') + bookkey = rsa.decrypt(bookkey) + if bookkey[0] != '\x02': + raise ADEPTError('error decrypting book session key') + index = bookkey.index('\0') + 1 + bookkey = bookkey[index:] + ebx_V = int_value(param.get('V', 4)) + ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6)) + # added because of improper booktype / decryption book session key errors + if length > 0: + if len(bookkey) == length: + if ebx_V == 3: + V = 3 + else: + V = 2 + elif len(bookkey) == length + 1: + V = ord(bookkey[0]) + bookkey = bookkey[1:] + else: + print "ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type) + print "length is %d and len(bookkey) is %d" % (length, len(bookkey)) + print "bookkey[0] is %d" % ord(bookkey[0]) + raise ADEPTError('error decrypting book session key - mismatched length') + else: + # proper length unknown try with whatever you have + print "ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type) + print "length is %d and len(bookkey) is %d" % (length, len(bookkey)) + print "bookkey[0] is %d" % ord(bookkey[0]) + if ebx_V == 3: + V = 3 + else: + V = 2 + self.decrypt_key = bookkey + self.genkey = self.genkey_v3 if V == 3 else self.genkey_v2 + self.decipher = self.decrypt_rc4 + self.ready = True + return + + # genkey functions + def genkey_v2(self, objid, genno): + objid = struct.pack(' PDFObjStmRef.maxindex: + PDFObjStmRef.maxindex = index + + +## PDFParser +## +class PDFParser(PSStackParser): + + def __init__(self, doc, fp): + PSStackParser.__init__(self, fp) + self.doc = doc + self.doc.set_parser(self) + return + + def __repr__(self): + return '' + + KEYWORD_R = PSKeywordTable.intern('R') + KEYWORD_ENDOBJ = PSKeywordTable.intern('endobj') + KEYWORD_STREAM = PSKeywordTable.intern('stream') + KEYWORD_XREF = PSKeywordTable.intern('xref') + KEYWORD_STARTXREF = PSKeywordTable.intern('startxref') + def do_keyword(self, pos, token): + if token in (self.KEYWORD_XREF, self.KEYWORD_STARTXREF): + self.add_results(*self.pop(1)) + return + if token is self.KEYWORD_ENDOBJ: + self.add_results(*self.pop(4)) + return + + if token is self.KEYWORD_R: + # reference to indirect object try: - text = unichr(htmlentitydefs.name2codepoint[text[1:-1]]) - except KeyError: + ((_,objid), (_,genno)) = self.pop(2) + (objid, genno) = (int(objid), int(genno)) + obj = PDFObjRef(self.doc, objid, genno) + self.push((pos, obj)) + except PSSyntaxError: pass - return text # leave as is - return re.sub(u"&#?\w+;", fixup, text) + return -def GetDecryptedBook(infile, kDatabases, serials, pids, starttime = time.time()): - # handle the obvious cases at the beginning - if not os.path.isfile(infile): - raise DrmException(u"Input file does not exist.") + if token is self.KEYWORD_STREAM: + # stream object + ((_,dic),) = self.pop(1) + dic = dict_value(dic) + try: + objlen = int_value(dic['Length']) + except KeyError: + if STRICT: + raise PDFSyntaxError('/Length is undefined: %r' % dic) + objlen = 0 + self.seek(pos) + try: + (_, line) = self.nextline() # 'stream' + except PSEOF: + if STRICT: + raise PDFSyntaxError('Unexpected EOF') + return + pos += len(line) + self.fp.seek(pos) + data = self.fp.read(objlen) + self.seek(pos+objlen) + while 1: + try: + (linepos, line) = self.nextline() + except PSEOF: + if STRICT: + raise PDFSyntaxError('Unexpected EOF') + break + if 'endstream' in line: + i = line.index('endstream') + objlen += i + data += line[:i] + break + objlen += len(line) + data += line + self.seek(pos+objlen) + obj = PDFStream(dic, data, self.doc.decipher) + self.push((pos, obj)) + return - mobi = True - magic3 = open(infile,'rb').read(3) - if magic3 == 'TPZ': - mobi = False + # others + self.push((pos, token)) + return - if mobi: - mb = mobidedrm.MobiBook(infile) - else: - mb = topazextract.TopazBook(infile) + def find_xref(self): + # search the last xref table by scanning the file backwards. + prev = None + for line in self.revreadlines(): + line = line.strip() + if line == 'startxref': break + if line: + prev = line + else: + raise PDFNoValidXRef('Unexpected EOF') + return int(prev) - bookname = unescape(mb.getBookTitle()) - print u"Decrypting {1} ebook: {0}".format(bookname, mb.getBookType()) + # read xref table + def read_xref_from(self, start, xrefs): + self.seek(start) + self.reset() + try: + (pos, token) = self.nexttoken() + except PSEOF: + raise PDFNoValidXRef('Unexpected EOF') + if isinstance(token, int): + # XRefStream: PDF-1.5 + if GEN_XREF_STM == 1: + global gen_xref_stm + gen_xref_stm = True + self.seek(pos) + self.reset() + xref = PDFXRefStream() + xref.load(self) + else: + if token is not self.KEYWORD_XREF: + raise PDFNoValidXRef('xref not found: pos=%d, token=%r' % + (pos, token)) + self.nextline() + xref = PDFXRef() + xref.load(self) + xrefs.append(xref) + trailer = xref.trailer + if 'XRefStm' in trailer: + pos = int_value(trailer['XRefStm']) + self.read_xref_from(pos, xrefs) + if 'Prev' in trailer: + # find previous xref + pos = int_value(trailer['Prev']) + self.read_xref_from(pos, xrefs) + return - # copy list of pids - totalpids = list(pids) - # extend PID list with book-specific PIDs - md1, md2 = mb.getPIDMetaInfo() - totalpids.extend(kgenpids.getPidList(md1, md2, serials, kDatabases)) - print u"Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(totalpids)) + # read xref tables and trailers + def read_xref(self): + xrefs = [] + trailerpos = None + try: + pos = self.find_xref() + self.read_xref_from(pos, xrefs) + except PDFNoValidXRef: + # fallback + self.seek(0) + pat = re.compile(r'^(\d+)\s+(\d+)\s+obj\b') + offsets = {} + xref = PDFXRef() + while 1: + try: + (pos, line) = self.nextline() + except PSEOF: + break + if line.startswith('trailer'): + trailerpos = pos # remember last trailer + m = pat.match(line) + if not m: continue + (objid, genno) = m.groups() + offsets[int(objid)] = (0, pos) + if not offsets: raise + xref.offsets = offsets + if trailerpos: + self.seek(trailerpos) + xref.load_trailer(self) + xrefs.append(xref) + return xrefs - try: - mb.processBook(totalpids) - except: - mb.cleanup - raise +## PDFObjStrmParser +## +class PDFObjStrmParser(PDFParser): - print u"Decryption succeeded after {0:.1f} seconds".format(time.time()-starttime) - return mb + def __init__(self, data, doc): + PSStackParser.__init__(self, StringIO(data)) + self.doc = doc + return + def flush(self): + self.add_results(*self.popall()) + return -# kDatabaseFiles is a list of files created by kindlekey -def decryptBook(infile, outdir, kDatabaseFiles, serials, pids): - starttime = time.time() - kDatabases = [] - for dbfile in kDatabaseFiles: - kindleDatabase = {} - try: - with open(dbfile, 'r') as keyfilein: - kindleDatabase = json.loads(keyfilein.read()) - kDatabases.append([dbfile,kindleDatabase]) - except Exception, e: - print u"Error getting database from file {0:s}: {1:s}".format(dbfile,e) - traceback.print_exc() + KEYWORD_R = KWD('R') + def do_keyword(self, pos, token): + if token is self.KEYWORD_R: + # reference to indirect object + try: + ((_,objid), (_,genno)) = self.pop(2) + (objid, genno) = (int(objid), int(genno)) + obj = PDFObjRef(self.doc, objid, genno) + self.push((pos, obj)) + except PSSyntaxError: + pass + return + # others + self.push((pos, token)) + return +### +### My own code, for which there is none else to blame +class PDFSerializer(object): + def __init__(self, inf, userkey): + global GEN_XREF_STM, gen_xref_stm + gen_xref_stm = GEN_XREF_STM > 1 + self.version = inf.read(8) + inf.seek(0) + self.doc = doc = PDFDocument() + parser = PDFParser(doc, inf) + doc.initialize(userkey) + self.objids = objids = set() + for xref in reversed(doc.xrefs): + trailer = xref.trailer + for objid in xref.objids(): + objids.add(objid) + trailer = dict(trailer) + trailer.pop('Prev', None) + trailer.pop('XRefStm', None) + if 'Encrypt' in trailer: + objids.remove(trailer.pop('Encrypt').objid) + self.trailer = trailer - try: - book = GetDecryptedBook(infile, kDatabases, serials, pids, starttime) - except Exception, e: - print u"Error decrypting book after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime) - traceback.print_exc() - return 1 + def dump(self, outf): + self.outf = outf + self.write(self.version) + self.write('\n%\xe2\xe3\xcf\xd3\n') + doc = self.doc + objids = self.objids + xrefs = {} + maxobj = max(objids) + trailer = dict(self.trailer) + trailer['Size'] = maxobj + 1 + for objid in objids: + obj = doc.getobj(objid) + if isinstance(obj, PDFObjStmRef): + xrefs[objid] = obj + continue + if obj is not None: + try: + genno = obj.genno + except AttributeError: + genno = 0 + xrefs[objid] = (self.tell(), genno) + self.serialize_indirect(objid, obj) + startxref = self.tell() - # if we're saving to the same folder as the original, use file name_ - # if to a different folder, use book name - if os.path.normcase(os.path.normpath(outdir)) == os.path.normcase(os.path.normpath(os.path.dirname(infile))): - outfilename = os.path.splitext(os.path.basename(infile))[0] - else: - outfilename = cleanup_name(book.getBookTitle()) + if not gen_xref_stm: + self.write('xref\n') + self.write('0 %d\n' % (maxobj + 1,)) + for objid in xrange(0, maxobj + 1): + if objid in xrefs: + # force the genno to be 0 + self.write("%010d 00000 n \n" % xrefs[objid][0]) + else: + self.write("%010d %05d f \n" % (0, 65535)) - # avoid excessively long file names - if len(outfilename)>150: - outfilename = outfilename[:150] + self.write('trailer\n') + self.serialize_object(trailer) + self.write('\nstartxref\n%d\n%%%%EOF' % startxref) - outfilename = outfilename+u"_nodrm" - outfile = os.path.join(outdir, outfilename + book.getBookExtension()) + else: # Generate crossref stream. - book.getFile(outfile) - print u"Saved decrypted book {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename) + # Calculate size of entries + maxoffset = max(startxref, maxobj) + maxindex = PDFObjStmRef.maxindex + fl2 = 2 + power = 65536 + while maxoffset >= power: + fl2 += 1 + power *= 256 + fl3 = 1 + power = 256 + while maxindex >= power: + fl3 += 1 + power *= 256 - if book.getBookType()==u"Topaz": - zipname = os.path.join(outdir, outfilename + u"_SVG.zip") - book.getSVGZip(zipname) - print u"Saved SVG ZIP Archive for {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename) + index = [] + first = None + prev = None + data = [] + # Put the xrefstream's reference in itself + startxref = self.tell() + maxobj += 1 + xrefs[maxobj] = (startxref, 0) + for objid in sorted(xrefs): + if first is None: + first = objid + elif objid != prev + 1: + index.extend((first, prev - first + 1)) + first = objid + prev = objid + objref = xrefs[objid] + if isinstance(objref, PDFObjStmRef): + f1 = 2 + f2 = objref.stmid + f3 = objref.index + else: + f1 = 1 + f2 = objref[0] + # we force all generation numbers to be 0 + # f3 = objref[1] + f3 = 0 - # remove internal temporary directory of Topaz pieces - book.cleanup() - return 0 + data.append(struct.pack('>B', f1)) + data.append(struct.pack('>L', f2)[-fl2:]) + data.append(struct.pack('>L', f3)[-fl3:]) + index.extend((first, prev - first + 1)) + data = zlib.compress(''.join(data)) + dic = {'Type': LITERAL_XREF, 'Size': prev + 1, 'Index': index, + 'W': [1, fl2, fl3], 'Length': len(data), + 'Filter': LITERALS_FLATE_DECODE[0], + 'Root': trailer['Root'],} + if 'Info' in trailer: + dic['Info'] = trailer['Info'] + xrefstm = PDFStream(dic, data) + self.serialize_indirect(maxobj, xrefstm) + self.write('startxref\n%d\n%%%%EOF' % startxref) + def write(self, data): + self.outf.write(data) + self.last = data[-1:] + def tell(self): + return self.outf.tell() + + def escape_string(self, string): + string = string.replace('\\', '\\\\') + string = string.replace('\n', r'\n') + string = string.replace('(', r'\(') + string = string.replace(')', r'\)') + # get rid of ciando id + regularexp = re.compile(r'http://www.ciando.com/index.cfm/intRefererID/\d{5}') + if regularexp.match(string): return ('http://www.ciando.com') + return string + + def serialize_object(self, obj): + if isinstance(obj, dict): + # Correct malformed Mac OS resource forks for Stanza + if 'ResFork' in obj and 'Type' in obj and 'Subtype' not in obj \ + and isinstance(obj['Type'], int): + obj['Subtype'] = obj['Type'] + del obj['Type'] + # end - hope this doesn't have bad effects + self.write('<<') + for key, val in obj.items(): + self.write('/%s' % key) + self.serialize_object(val) + self.write('>>') + elif isinstance(obj, list): + self.write('[') + for val in obj: + self.serialize_object(val) + self.write(']') + elif isinstance(obj, str): + self.write('(%s)' % self.escape_string(obj)) + elif isinstance(obj, bool): + if self.last.isalnum(): + self.write(' ') + self.write(str(obj).lower()) + elif isinstance(obj, (int, long, float)): + if self.last.isalnum(): + self.write(' ') + self.write(str(obj)) + elif isinstance(obj, PDFObjRef): + if self.last.isalnum(): + self.write(' ') + self.write('%d %d R' % (obj.objid, 0)) + elif isinstance(obj, PDFStream): + ### If we don't generate cross ref streams the object streams + ### are no longer useful, as we have extracted all objects from + ### them. Therefore leave them out from the output. + if obj.dic.get('Type') == LITERAL_OBJSTM and not gen_xref_stm: + self.write('(deleted)') + else: + data = obj.get_decdata() + self.serialize_object(obj.dic) + self.write('stream\n') + self.write(data) + self.write('\nendstream') + else: + data = str(obj) + if data[0].isalnum() and self.last.isalnum(): + self.write(' ') + self.write(data) + + def serialize_indirect(self, objid, obj): + self.write('%d 0 obj' % (objid,)) + self.serialize_object(obj) + if self.last.isalnum(): + self.write('\n') + self.write('endobj\n') + + + + +def decryptBook(userkey, inpath, outpath): + if RSA is None: + raise ADEPTError(u"PyCrypto or OpenSSL must be installed.") + with open(inpath, 'rb') as inf: + try: + serializer = PDFSerializer(inf, userkey) + except: + print u"Error serializing pdf {0}. Probably wrong key.".format(os.path.basename(inpath)) + return 2 + # hope this will fix the 'bad file descriptor' problem + with open(outpath, 'wb') as outf: + # help construct to make sure the method runs to the end + try: + serializer.dump(outf) + except Exception, e: + print u"error writing pdf: {0}".format(e.args[0]) + return 2 + return 0 -def usage(progname): - print u"Removes DRM protection from Mobipocket, Amazon KF8, Amazon Print Replica and Amazon Topaz ebooks" - print u"Usage:" - print u" {0} [-k ] [-p ] [-s ] ".format(progname) -# -# Main -# def cli_main(): + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) argv=unicode_argv() progname = os.path.basename(argv[0]) - print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2013 The Dark Reverser et al.".format(__version__) + if len(argv) != 4: + print u"usage: {0} ".format(progname) + return 1 + keypath, inpath, outpath = argv[1:] + userkey = open(keypath,'rb').read() + result = decryptBook(userkey, inpath, outpath) + if result == 0: + print u"Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath)) + return result + +def gui_main(): try: - opts, args = getopt.getopt(argv[1:], "k:p:s:") - except getopt.GetoptError, err: - print u"Error in options or arguments: {0}".format(err.args[0]) - usage(progname) - sys.exit(2) - if len(args)<2: - usage(progname) - sys.exit(2) - - infile = args[0] - outdir = args[1] - kDatabaseFiles = [] - serials = [] - pids = [] - - for o, a in opts: - if o == "-k": - if a == None : - raise DrmException("Invalid parameter for -k") - kDatabaseFiles.append(a) - if o == "-p": - if a == None : - raise DrmException("Invalid parameter for -p") - pids = a.split(',') - if o == "-s": - if a == None : - raise DrmException("Invalid parameter for -s") - serials = a.split(',') - - # try with built in Kindle Info files if not on Linux - k4 = not sys.platform.startswith('linux') - - return decryptBook(infile, outdir, kDatabaseFiles, serials, pids) + import Tkinter + import Tkconstants + import tkMessageBox + import traceback + except: + return cli_main() + + class DecryptionDialog(Tkinter.Frame): + def __init__(self, root): + Tkinter.Frame.__init__(self, root, border=5) + self.status = Tkinter.Label(self, text=u"Select files for decryption") + self.status.pack(fill=Tkconstants.X, expand=1) + body = Tkinter.Frame(self) + body.pack(fill=Tkconstants.X, expand=1) + sticky = Tkconstants.E + Tkconstants.W + body.grid_columnconfigure(1, weight=2) + Tkinter.Label(body, text=u"Key file").grid(row=0) + self.keypath = Tkinter.Entry(body, width=30) + self.keypath.grid(row=0, column=1, sticky=sticky) + if os.path.exists(u"adeptkey.der"): + self.keypath.insert(0, u"adeptkey.der") + button = Tkinter.Button(body, text=u"...", command=self.get_keypath) + button.grid(row=0, column=2) + Tkinter.Label(body, text=u"Input file").grid(row=1) + self.inpath = Tkinter.Entry(body, width=30) + self.inpath.grid(row=1, column=1, sticky=sticky) + button = Tkinter.Button(body, text=u"...", command=self.get_inpath) + button.grid(row=1, column=2) + Tkinter.Label(body, text=u"Output file").grid(row=2) + self.outpath = Tkinter.Entry(body, width=30) + self.outpath.grid(row=2, column=1, sticky=sticky) + button = Tkinter.Button(body, text=u"...", command=self.get_outpath) + button.grid(row=2, column=2) + buttons = Tkinter.Frame(self) + buttons.pack() + botton = Tkinter.Button( + buttons, text=u"Decrypt", width=10, command=self.decrypt) + botton.pack(side=Tkconstants.LEFT) + Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) + button = Tkinter.Button( + buttons, text=u"Quit", width=10, command=self.quit) + button.pack(side=Tkconstants.RIGHT) + + def get_keypath(self): + keypath = tkFileDialog.askopenfilename( + parent=None, title=u"Select Adobe Adept \'.der\' key file", + defaultextension=u".der", + filetypes=[('Adobe Adept DER-encoded files', '.der'), + ('All Files', '.*')]) + if keypath: + keypath = os.path.normpath(keypath) + self.keypath.delete(0, Tkconstants.END) + self.keypath.insert(0, keypath) + return + + def get_inpath(self): + inpath = tkFileDialog.askopenfilename( + parent=None, title=u"Select ADEPT-encrypted PDF file to decrypt", + defaultextension=u".pdf", filetypes=[('PDF files', '.pdf')]) + if inpath: + inpath = os.path.normpath(inpath) + self.inpath.delete(0, Tkconstants.END) + self.inpath.insert(0, inpath) + return + + def get_outpath(self): + outpath = tkFileDialog.asksaveasfilename( + parent=None, title=u"Select unencrypted PDF file to produce", + defaultextension=u".pdf", filetypes=[('PDF files', '.pdf')]) + if outpath: + outpath = os.path.normpath(outpath) + self.outpath.delete(0, Tkconstants.END) + self.outpath.insert(0, outpath) + return + + def decrypt(self): + keypath = self.keypath.get() + inpath = self.inpath.get() + outpath = self.outpath.get() + if not keypath or not os.path.exists(keypath): + self.status['text'] = u"Specified key file does not exist" + return + if not inpath or not os.path.exists(inpath): + self.status['text'] = u"Specified input file does not exist" + return + if not outpath: + self.status['text'] = u"Output file not specified" + return + if inpath == outpath: + self.status['text'] = u"Must have different input and output files" + return + userkey = open(keypath,'rb').read() + self.status['text'] = u"Decrypting..." + try: + decrypt_status = decryptBook(userkey, inpath, outpath) + except Exception, e: + self.status['text'] = u"Error; {0}".format(e.args[0]) + return + if decrypt_status == 0: + self.status['text'] = u"File successfully decrypted" + else: + self.status['text'] = u"The was an error decrypting the file." + + + root = Tkinter.Tk() + if RSA is None: + root.withdraw() + tkMessageBox.showerror( + "INEPT PDF", + "This script requires OpenSSL or PyCrypto, which must be installed " + "separately. Read the top-of-script comment for details.") + return 1 + root.title(u"Adobe Adept PDF Decrypter v.{0}".format(__version__)) + root.resizable(True, False) + root.minsize(370, 0) + DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1) + root.mainloop() + return 0 if __name__ == '__main__': - sys.stdout=SafeUnbuffered(sys.stdout) - sys.stderr=SafeUnbuffered(sys.stderr) - sys.exit(cli_main()) + if len(sys.argv) > 1: + sys.exit(cli_main()) + sys.exit(gui_main()) diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/k4mobidedrm.py b/DeDRM_calibre_plugin/DeDRM_plugin/k4mobidedrm.py index dd88797..504105b 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/k4mobidedrm.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/k4mobidedrm.py @@ -2,266 +2,331 @@ # -*- coding: utf-8 -*- from __future__ import with_statement -import sys -import os, csv -import binascii -import zlib + +# ignobleepub.pyw, version 3.6 +# Copyright © 2009-2012 by DiapDealer et al. + +# engine to remove drm from Kindle for Mac and Kindle for PC books +# for personal use for archiving and converting your ebooks + +# PLEASE DO NOT PIRATE EBOOKS! + +# We want all authors and publishers, and eBook stores to live +# long and prosperous lives but at the same time we just want to +# be able to read OUR books on whatever device we want and to keep +# readable for a long, long time + +# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle, +# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates +# and many many others +# Special thanks to The Dark Reverser for MobiDeDrm and CMBDTC for cmbdtc_dump +# from which this script borrows most unashamedly. + + +# Changelog +# 1.0 - Name change to k4mobidedrm. Adds Mac support, Adds plugin code +# 1.1 - Adds support for additional kindle.info files +# 1.2 - Better error handling for older Mobipocket +# 1.3 - Don't try to decrypt Topaz books +# 1.7 - Add support for Topaz books and Kindle serial numbers. Split code. +# 1.9 - Tidy up after Topaz, minor exception changes +# 2.1 - Topaz fix and filename sanitizing +# 2.2 - Topaz Fix and minor Mac code fix +# 2.3 - More Topaz fixes +# 2.4 - K4PC/Mac key generation fix +# 2.6 - Better handling of non-K4PC/Mac ebooks +# 2.7 - Better trailing bytes handling in mobidedrm +# 2.8 - Moved parsing of kindle.info files to mac & pc util files. +# 3.1 - Updated for new calibre interface. Now __init__ in plugin. +# 3.5 - Now support Kindle for PC/Mac 1.6 +# 3.6 - Even better trailing bytes handling in mobidedrm +# 3.7 - Add support for Amazon Print Replica ebooks. +# 3.8 - Improved Topaz support +# 4.1 - Improved Topaz support and faster decryption with alfcrypto +# 4.2 - Added support for Amazon's KF8 format ebooks +# 4.4 - Linux calls to Wine added, and improved configuration dialog +# 4.5 - Linux works again without Wine. Some Mac key file search changes +# 4.6 - First attempt to handle unicode properly +# 4.7 - Added timing reports, and changed search for Mac key files +# 4.8 - Much better unicode handling, matching the updated inept and ignoble scripts +# - Moved back into plugin, __init__ in plugin now only contains plugin code. +# 4.9 - Missed some invalid characters in cleanup_name +# 5.0 - Extraction of info from Kindle for PC/Mac moved into kindlekey.py +# - tweaked GetDecryptedBook interface to leave passed parameters unchanged +# 5.1 - moved unicode_argv call inside main for Windows DeDRM compatibility +# 5.2 - Fixed error in command line processing of unicode arguments + +__version__ = '5.2' + + +import sys, os, re +import csv +import getopt import re -from struct import pack, unpack, unpack_from import traceback +import time +import htmlentitydefs +import json class DrmException(Exception): pass -global charMap1 -global charMap3 -global charMap4 - - -charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M' -charMap3 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' -charMap4 = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789' - -# crypto digestroutines -import hashlib - -def MD5(message): - ctx = hashlib.md5() - ctx.update(message) - return ctx.digest() - -def SHA1(message): - ctx = hashlib.sha1() - ctx.update(message) - return ctx.digest() - - -# Encode the bytes in data with the characters in map -def encode(data, map): - result = '' - for char in data: - value = ord(char) - Q = (value ^ 0x80) // len(map) - R = value % len(map) - result += map[Q] - result += map[R] - return result - -# Hash the bytes in data and then encode the digest with the characters in map -def encodeHash(data,map): - return encode(MD5(data),map) - -# Decode the string in data with the characters in map. Returns the decoded bytes -def decode(data,map): - result = '' - for i in range (0,len(data)-1,2): - high = map.find(data[i]) - low = map.find(data[i+1]) - if (high == -1) or (low == -1) : - break - value = (((high * len(map)) ^ 0x80) & 0xFF) + low - result += pack('B',value) - return result - -# -# PID generation routines -# - -# Returns two bit at offset from a bit field -def getTwoBitsFromBitField(bitField,offset): - byteNumber = offset // 4 - bitPosition = 6 - 2*(offset % 4) - return ord(bitField[byteNumber]) >> bitPosition & 3 - -# Returns the six bits at offset from a bit field -def getSixBitsFromBitField(bitField,offset): - offset *= 3 - value = (getTwoBitsFromBitField(bitField,offset) <<4) + (getTwoBitsFromBitField(bitField,offset+1) << 2) +getTwoBitsFromBitField(bitField,offset+2) - return value - -# 8 bits to six bits encoding from hash to generate PID string -def encodePID(hash): - global charMap3 - PID = '' - for position in range (0,8): - PID += charMap3[getSixBitsFromBitField(hash,position)] - return PID - -# Encryption table used to generate the device PID -def generatePidEncryptionTable() : - table = [] - for counter1 in range (0,0x100): - value = counter1 - for counter2 in range (0,8): - if (value & 1 == 0) : - value = value >> 1 - else : - value = value >> 1 - value = value ^ 0xEDB88320 - table.append(value) - return table - -# Seed value used to generate the device PID -def generatePidSeed(table,dsn) : - value = 0 - for counter in range (0,4) : - index = (ord(dsn[counter]) ^ value) &0xFF - value = (value >> 8) ^ table[index] - return value - -# Generate the device PID -def generateDevicePID(table,dsn,nbRoll): - global charMap4 - seed = generatePidSeed(table,dsn) - pidAscii = '' - pid = [(seed >>24) &0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF,(seed>>24) & 0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF] - index = 0 - for counter in range (0,nbRoll): - pid[index] = pid[index] ^ ord(dsn[counter]) - index = (index+1) %8 - for counter in range (0,8): - index = ((((pid[counter] >>5) & 3) ^ pid[counter]) & 0x1f) + (pid[counter] >> 7) - pidAscii += charMap4[index] - return pidAscii - -def crc32(s): - return (~binascii.crc32(s,-1))&0xFFFFFFFF - -# convert from 8 digit PID to 10 digit PID with checksum -def checksumPid(s): - global charMap4 - crc = crc32(s) - crc = crc ^ (crc >> 16) - res = s - l = len(charMap4) - for i in (0,1): - b = crc & 0xff - pos = (b // l) ^ (b % l) - res += charMap4[pos%l] - crc >>= 8 - return res - - -# old kindle serial number to fixed pid -def pidFromSerial(s, l): - global charMap4 - crc = crc32(s) - arr1 = [0]*l - for i in xrange(len(s)): - arr1[i%l] ^= ord(s[i]) - crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff] - for i in xrange(l): - arr1[i] ^= crc_bytes[i&3] - pid = "" - for i in xrange(l): - b = arr1[i] & 0xff - pid+=charMap4[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))] - return pid - - -# Parse the EXTH header records and use the Kindle serial number to calculate the book pid. -def getKindlePids(rec209, token, serialnum): - pids=[] - - if isinstance(serialnum,unicode): - serialnum = serialnum.encode('ascii') - - # Compute book PID - pidHash = SHA1(serialnum+rec209+token) - bookPID = encodePID(pidHash) - bookPID = checksumPid(bookPID) - pids.append(bookPID) - - # compute fixed pid for old pre 2.5 firmware update pid as well - kindlePID = pidFromSerial(serialnum, 7) + "*" - kindlePID = checksumPid(kindlePID) - pids.append(kindlePID) - - return pids - - -# parse the Kindleinfo file to calculate the book pid. - -keynames = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber'] - -def getK4Pids(rec209, token, kindleDatabase): - global charMap1 - pids = [] +if 'calibre' in sys.modules: + inCalibre = True +else: + inCalibre = False + +if inCalibre: + from calibre_plugins.dedrm import mobidedrm + from calibre_plugins.dedrm import topazextract + from calibre_plugins.dedrm import kgenpids + from calibre_plugins.dedrm import android +else: + import mobidedrm + import topazextract + import kgenpids + import android + +# Wrap a stream so that output gets flushed immediately +# and also make sure that any unicode strings get +# encoded using "replace" before writing them. +class SafeUnbuffered: + def __init__(self, stream): + self.stream = stream + self.encoding = stream.encoding + if self.encoding == None: + self.encoding = "utf-8" + def write(self, data): + if isinstance(data,unicode): + data = data.encode(self.encoding,"replace") + self.stream.write(data) + self.stream.flush() + def __getattr__(self, attr): + return getattr(self.stream, attr) + +iswindows = sys.platform.startswith('win') +isosx = sys.platform.startswith('darwin') + +def unicode_argv(): + if iswindows: + # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode + # strings. + + # Versions 2.x of Python don't support Unicode in sys.argv on + # Windows, with the underlying Windows API instead replacing multi-byte + # characters with '?'. + + + from ctypes import POINTER, byref, cdll, c_int, windll + from ctypes.wintypes import LPCWSTR, LPWSTR + + GetCommandLineW = cdll.kernel32.GetCommandLineW + GetCommandLineW.argtypes = [] + GetCommandLineW.restype = LPCWSTR + + CommandLineToArgvW = windll.shell32.CommandLineToArgvW + CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] + CommandLineToArgvW.restype = POINTER(LPWSTR) + + cmd = GetCommandLineW() + argc = c_int(0) + argv = CommandLineToArgvW(cmd, byref(argc)) + if argc.value > 0: + # Remove Python executable and commands if present + start = argc.value - len(sys.argv) + return [argv[i] for i in + xrange(start, argc.value)] + # if we don't have any arguments at all, just pass back script name + # this should never happen + return [u"mobidedrm.py"] + else: + argvencoding = sys.stdin.encoding + if argvencoding == None: + argvencoding = "utf-8" + return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv] + +# cleanup unicode filenames +# borrowed from calibre from calibre/src/calibre/__init__.py +# added in removal of control (<32) chars +# and removal of . at start and end +# and with some (heavily edited) code from Paul Durrant's kindlenamer.py +def cleanup_name(name): + # substitute filename unfriendly characters + name = name.replace(u"<",u"[").replace(u">",u"]").replace(u" : ",u" – ").replace(u": ",u" – ").replace(u":",u"—").replace(u"/",u"_").replace(u"\\",u"_").replace(u"|",u"_").replace(u"\"",u"\'").replace(u"*",u"_").replace(u"?",u"") + # delete control characters + name = u"".join(char for char in name if ord(char)>=32) + # white space to single space, delete leading and trailing while space + name = re.sub(ur"\s", u" ", name).strip() + # remove leading dots + while len(name)>0 and name[0] == u".": + name = name[1:] + # remove trailing dots (Windows doesn't like them) + if name.endswith(u'.'): + name = name[:-1] + return name + +# must be passed unicode +def unescape(text): + def fixup(m): + text = m.group(0) + if text[:2] == u"&#": + # character reference + try: + if text[:3] == u"&#x": + return unichr(int(text[3:-1], 16)) + else: + return unichr(int(text[2:-1])) + except ValueError: + pass + else: + # named entity + try: + text = unichr(htmlentitydefs.name2codepoint[text[1:-1]]) + except KeyError: + pass + return text # leave as is + return re.sub(u"&#?\w+;", fixup, text) + +def GetDecryptedBook(infile, kDatabases, serials, pids, starttime = time.time()): + # handle the obvious cases at the beginning + if not os.path.isfile(infile): + raise DrmException(u"Input file does not exist.") + + mobi = True + magic3 = open(infile,'rb').read(3) + if magic3 == 'TPZ': + mobi = False + + if mobi: + mb = mobidedrm.MobiBook(infile) + else: + mb = topazextract.TopazBook(infile) + + bookname = unescape(mb.getBookTitle()) + print u"Decrypting {1} ebook: {0}".format(bookname, mb.getBookType()) + + # copy list of pids + totalpids = list(pids) + # extend PID list with book-specific PIDs + md1, md2 = mb.getPIDMetaInfo() + totalpids.extend(kgenpids.getPidList(md1, md2, serials, kDatabases)) + print u"Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(totalpids)) try: - # Get the Mazama Random number - MazamaRandomNumber = (kindleDatabase[1])['MazamaRandomNumber'].decode('hex').encode('ascii') + mb.processBook(totalpids) + except: + mb.cleanup + raise - # Get the kindle account token - kindleAccountToken = (kindleDatabase[1])['kindle.account.tokens'].decode('hex').encode('ascii') + print u"Decryption succeeded after {0:.1f} seconds".format(time.time()-starttime) + return mb - # Get the IDString used to decode the Kindle Info file - IDString = (kindleDatabase[1])['IDString'].decode('hex').encode('ascii') - # Get the UserName stored when the Kindle Info file was decoded - UserName = (kindleDatabase[1])['UserName'].decode('hex').encode('ascii') - - except KeyError: - print u"Keys not found in the database {0}.".format(kindleDatabase[0]) - return pids +# kDatabaseFiles is a list of files created by kindlekey +def decryptBook(infile, outdir, kDatabaseFiles, serials, pids): + starttime = time.time() + kDatabases = [] + for dbfile in kDatabaseFiles: + kindleDatabase = {} + try: + with open(dbfile, 'r') as keyfilein: + kindleDatabase = json.loads(keyfilein.read()) + kDatabases.append([dbfile,kindleDatabase]) + except Exception, e: + print u"Error getting database from file {0:s}: {1:s}".format(dbfile,e) + traceback.print_exc() - # Get the ID string used - encodedIDString = encodeHash(IDString,charMap1) - # Get the current user name - encodedUsername = encodeHash(UserName,charMap1) - # concat, hash and encode to calculate the DSN - DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1) + try: + book = GetDecryptedBook(infile, kDatabases, serials, pids, starttime) + except Exception, e: + print u"Error decrypting book after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime) + traceback.print_exc() + return 1 - # Compute the device PID (for which I can tell, is used for nothing). - table = generatePidEncryptionTable() - devicePID = generateDevicePID(table,DSN,4) - devicePID = checksumPid(devicePID) - pids.append(devicePID) + # if we're saving to the same folder as the original, use file name_ + # if to a different folder, use book name + if os.path.normcase(os.path.normpath(outdir)) == os.path.normcase(os.path.normpath(os.path.dirname(infile))): + outfilename = os.path.splitext(os.path.basename(infile))[0] + else: + outfilename = cleanup_name(book.getBookTitle()) - # Compute book PIDs + # avoid excessively long file names + if len(outfilename)>150: + outfilename = outfilename[:150] - # book pid - pidHash = SHA1(DSN+kindleAccountToken+rec209+token) - bookPID = encodePID(pidHash) - bookPID = checksumPid(bookPID) - pids.append(bookPID) + outfilename = outfilename+u"_nodrm" + outfile = os.path.join(outdir, outfilename + book.getBookExtension()) - # variant 1 - pidHash = SHA1(kindleAccountToken+rec209+token) - bookPID = encodePID(pidHash) - bookPID = checksumPid(bookPID) - pids.append(bookPID) + book.getFile(outfile) + print u"Saved decrypted book {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename) - # variant 2 - pidHash = SHA1(DSN+rec209+token) - bookPID = encodePID(pidHash) - bookPID = checksumPid(bookPID) - pids.append(bookPID) + if book.getBookType()==u"Topaz": + zipname = os.path.join(outdir, outfilename + u"_SVG.zip") + book.getSVGZip(zipname) + print u"Saved SVG ZIP Archive for {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename) - return pids + # remove internal temporary directory of Topaz pieces + book.cleanup() + return 0 -def getPidList(md1, md2, serials=[], kDatabases=[]): - pidlst = [] - if kDatabases is None: - kDatabases = [] - if serials is None: - serials = [] +def usage(progname): + print u"Removes DRM protection from Mobipocket, Amazon KF8, Amazon Print Replica and Amazon Topaz ebooks" + print u"Usage:" + print u" {0} [-k ] [-p ] [-s ] [ -a ] ".format(progname) - for kDatabase in kDatabases: - try: - pidlst.extend(getK4Pids(md1, md2, kDatabase)) - except Exception, e: - print u"Error getting PIDs from database {0}: {1}".format(kDatabase[0],e.args[0]) - traceback.print_exc() +# +# Main +# +def cli_main(): + argv=unicode_argv() + progname = os.path.basename(argv[0]) + print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2013 The Dark Reverser et al.".format(__version__) - for serialnum in serials: - try: - pidlst.extend(getKindlePids(md1, md2, serialnum)) - except Exception, e: - print u"Error getting PIDs from serial number {0}: {1}".format(serialnum ,e.args[0]) - traceback.print_exc() + try: + opts, args = getopt.getopt(argv[1:], "k:p:s:a:") + except getopt.GetoptError, err: + print u"Error in options or arguments: {0}".format(err.args[0]) + usage(progname) + sys.exit(2) + if len(args)<2: + usage(progname) + sys.exit(2) + + infile = args[0] + outdir = args[1] + kDatabaseFiles = [] + serials = [] + pids = [] - return pidlst + for o, a in opts: + if o == "-k": + if a == None : + raise DrmException("Invalid parameter for -k") + kDatabaseFiles.append(a) + if o == "-p": + if a == None : + raise DrmException("Invalid parameter for -p") + pids = a.split(',') + if o == "-s": + if a == None : + raise DrmException("Invalid parameter for -s") + serials = a.split(',') + if o == '-a': + if a == None: + continue + serials.extend(android.get_serials(a)) + serials.extend(android.get_serials()) + + # try with built in Kindle Info files if not on Linux + k4 = not sys.platform.startswith('linux') + + return decryptBook(infile, outdir, kDatabaseFiles, serials, pids) + + +if __name__ == '__main__': + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) + sys.exit(cli_main()) diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/kgenpids.py b/DeDRM_calibre_plugin/DeDRM_plugin/kgenpids.py index f58e973..dd88797 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/kgenpids.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/kgenpids.py @@ -2,102 +2,25 @@ # -*- coding: utf-8 -*- from __future__ import with_statement - -# kindlekey.py -# Copyright © 2010-2013 by some_updates and Apprentice Alf -# -# Currently requires alfcrypto.py which requires the alfcrypto library - -# Revision history: -# 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc. -# 1.1 - Added Tkinter to match adobekey.py -# 1.2 - Fixed testing of successful retrieval on Mac -# 1.3 - Added getkey interface for Windows DeDRM application -# Simplified some of the Kindle for Mac code. -# 1.4 - Remove dependency on alfcrypto -# 1.5 - moved unicode_argv call inside main for Windows DeDRM compatibility -# 1.6 - Fixed a problem getting the disk serial numbers -# 1.7 - Work if TkInter is missing -# 1.8 - Fixes for Kindle for Mac, and non-ascii in Windows user names - - -""" -Retrieve Kindle for PC/Mac user key. -""" - -__license__ = 'GPL v3' -__version__ = '1.8' - -import sys, os, re +import sys +import os, csv +import binascii +import zlib +import re from struct import pack, unpack, unpack_from -import json -import getopt - -# Routines common to Mac and PC +import traceback -# Wrap a stream so that output gets flushed immediately -# and also make sure that any unicode strings get -# encoded using "replace" before writing them. -class SafeUnbuffered: - def __init__(self, stream): - self.stream = stream - self.encoding = stream.encoding - if self.encoding == None: - self.encoding = "utf-8" - def write(self, data): - if isinstance(data,unicode): - data = data.encode(self.encoding,"replace") - self.stream.write(data) - self.stream.flush() - def __getattr__(self, attr): - return getattr(self.stream, attr) - -try: - from calibre.constants import iswindows, isosx -except: - iswindows = sys.platform.startswith('win') - isosx = sys.platform.startswith('darwin') - -def unicode_argv(): - if iswindows: - # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode - # strings. - - # Versions 2.x of Python don't support Unicode in sys.argv on - # Windows, with the underlying Windows API instead replacing multi-byte - # characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv - # as a list of Unicode strings and encode them as utf-8 - - from ctypes import POINTER, byref, cdll, c_int, windll - from ctypes.wintypes import LPCWSTR, LPWSTR +class DrmException(Exception): + pass - GetCommandLineW = cdll.kernel32.GetCommandLineW - GetCommandLineW.argtypes = [] - GetCommandLineW.restype = LPCWSTR +global charMap1 +global charMap3 +global charMap4 - CommandLineToArgvW = windll.shell32.CommandLineToArgvW - CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] - CommandLineToArgvW.restype = POINTER(LPWSTR) - cmd = GetCommandLineW() - argc = c_int(0) - argv = CommandLineToArgvW(cmd, byref(argc)) - if argc.value > 0: - # Remove Python executable and commands if present - start = argc.value - len(sys.argv) - return [argv[i] for i in - xrange(start, argc.value)] - # if we don't have any arguments at all, just pass back script name - # this should never happen - return [u"kindlekey.py"] - else: - argvencoding = sys.stdin.encoding - if argvencoding == None: - argvencoding = "utf-8" - return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv] - -class DrmException(Exception): - pass +charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M' +charMap3 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' +charMap4 = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789' # crypto digestroutines import hashlib @@ -112,31 +35,6 @@ def SHA1(message): ctx.update(message) return ctx.digest() -def SHA256(message): - ctx = hashlib.sha256() - ctx.update(message) - return ctx.digest() - -# For K4M/PC 1.6.X and later -# generate table of prime number less than or equal to int n -def primes(n): - if n==2: return [2] - elif n<2: return [] - s=range(3,n+1,2) - mroot = n ** 0.5 - half=(n+1)/2-1 - i=0 - m=3 - while m <= mroot: - if s[i]: - j=(m*m-3)/2 - s[j]=0 - while j> bitPosition & 3 + +# Returns the six bits at offset from a bit field +def getSixBitsFromBitField(bitField,offset): + offset *= 3 + value = (getTwoBitsFromBitField(bitField,offset) <<4) + (getTwoBitsFromBitField(bitField,offset+1) << 2) +getTwoBitsFromBitField(bitField,offset+2) + return value + +# 8 bits to six bits encoding from hash to generate PID string +def encodePID(hash): + global charMap3 + PID = '' + for position in range (0,8): + PID += charMap3[getSixBitsFromBitField(hash,position)] + return PID + +# Encryption table used to generate the device PID +def generatePidEncryptionTable() : + table = [] + for counter1 in range (0,0x100): + value = counter1 + for counter2 in range (0,8): + if (value & 1 == 0) : + value = value >> 1 + else : + value = value >> 1 + value = value ^ 0xEDB88320 + table.append(value) + return table + +# Seed value used to generate the device PID +def generatePidSeed(table,dsn) : + value = 0 + for counter in range (0,4) : + index = (ord(dsn[counter]) ^ value) &0xFF + value = (value >> 8) ^ table[index] + return value + +# Generate the device PID +def generateDevicePID(table,dsn,nbRoll): + global charMap4 + seed = generatePidSeed(table,dsn) + pidAscii = '' + pid = [(seed >>24) &0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF,(seed>>24) & 0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF] + index = 0 + for counter in range (0,nbRoll): + pid[index] = pid[index] ^ ord(dsn[counter]) + index = (index+1) %8 + for counter in range (0,8): + index = ((((pid[counter] >>5) & 3) ^ pid[counter]) & 0x1f) + (pid[counter] >> 7) + pidAscii += charMap4[index] + return pidAscii + +def crc32(s): + return (~binascii.crc32(s,-1))&0xFFFFFFFF + +# convert from 8 digit PID to 10 digit PID with checksum +def checksumPid(s): + global charMap4 + crc = crc32(s) + crc = crc ^ (crc >> 16) + res = s + l = len(charMap4) + for i in (0,1): + b = crc & 0xff + pos = (b // l) ^ (b % l) + res += charMap4[pos%l] + crc >>= 8 + return res + + +# old kindle serial number to fixed pid +def pidFromSerial(s, l): + global charMap4 + crc = crc32(s) + arr1 = [0]*l + for i in xrange(len(s)): + arr1[i%l] ^= ord(s[i]) + crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff] + for i in xrange(l): + arr1[i] ^= crc_bytes[i&3] + pid = "" + for i in xrange(l): + b = arr1[i] & 0xff + pid+=charMap4[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))] + return pid + + +# Parse the EXTH header records and use the Kindle serial number to calculate the book pid. +def getKindlePids(rec209, token, serialnum): + pids=[] + + if isinstance(serialnum,unicode): + serialnum = serialnum.encode('ascii') + + # Compute book PID + pidHash = SHA1(serialnum+rec209+token) + bookPID = encodePID(pidHash) + bookPID = checksumPid(bookPID) + pids.append(bookPID) + + # compute fixed pid for old pre 2.5 firmware update pid as well + kindlePID = pidFromSerial(serialnum, 7) + "*" + kindlePID = checksumPid(kindlePID) + pids.append(kindlePID) + + return pids + + +# parse the Kindleinfo file to calculate the book pid. + +keynames = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber'] + +def getK4Pids(rec209, token, kindleDatabase): + global charMap1 + pids = [] try: - # try to get fast routines from alfcrypto - from alfcrypto import AES_CBC, KeyIVGen - except: - # alfcrypto not available, so use python implementations - """ - Routines for doing AES CBC in one file - - Modified by some_updates to extract - and combine only those parts needed for AES CBC - into one simple to add python file - - Original Version - Copyright (c) 2002 by Paul A. Lambert - Under: - CryptoPy Artisitic License Version 1.0 - See the wonderful pure python package cryptopy-1.2.5 - and read its LICENSE.txt for complete license details. - """ - - class CryptoError(Exception): - """ Base class for crypto exceptions """ - def __init__(self,errorMessage='Error!'): - self.message = errorMessage - def __str__(self): - return self.message - - class InitCryptoError(CryptoError): - """ Crypto errors during algorithm initialization """ - class BadKeySizeError(InitCryptoError): - """ Bad key size error """ - class EncryptError(CryptoError): - """ Error in encryption processing """ - class DecryptError(CryptoError): - """ Error in decryption processing """ - class DecryptNotBlockAlignedError(DecryptError): - """ Error in decryption processing """ - - def xorS(a,b): - """ XOR two strings """ - assert len(a)==len(b) - x = [] - for i in range(len(a)): - x.append( chr(ord(a[i])^ord(b[i]))) - return ''.join(x) - - def xor(a,b): - """ XOR two strings """ - x = [] - for i in range(min(len(a),len(b))): - x.append( chr(ord(a[i])^ord(b[i]))) - return ''.join(x) - - """ - Base 'BlockCipher' and Pad classes for cipher instances. - BlockCipher supports automatic padding and type conversion. The BlockCipher - class was written to make the actual algorithm code more readable and - not for performance. - """ - - class BlockCipher: - """ Block ciphers """ - def __init__(self): - self.reset() - - def reset(self): - self.resetEncrypt() - self.resetDecrypt() - def resetEncrypt(self): - self.encryptBlockCount = 0 - self.bytesToEncrypt = '' - def resetDecrypt(self): - self.decryptBlockCount = 0 - self.bytesToDecrypt = '' - - def encrypt(self, plainText, more = None): - """ Encrypt a string and return a binary string """ - self.bytesToEncrypt += plainText # append plainText to any bytes from prior encrypt - numBlocks, numExtraBytes = divmod(len(self.bytesToEncrypt), self.blockSize) - cipherText = '' - for i in range(numBlocks): - bStart = i*self.blockSize - ctBlock = self.encryptBlock(self.bytesToEncrypt[bStart:bStart+self.blockSize]) - self.encryptBlockCount += 1 - cipherText += ctBlock - if numExtraBytes > 0: # save any bytes that are not block aligned - self.bytesToEncrypt = self.bytesToEncrypt[-numExtraBytes:] - else: - self.bytesToEncrypt = '' - - if more == None: # no more data expected from caller - finalBytes = self.padding.addPad(self.bytesToEncrypt,self.blockSize) - if len(finalBytes) > 0: - ctBlock = self.encryptBlock(finalBytes) - self.encryptBlockCount += 1 - cipherText += ctBlock - self.resetEncrypt() - return cipherText - - def decrypt(self, cipherText, more = None): - """ Decrypt a string and return a string """ - self.bytesToDecrypt += cipherText # append to any bytes from prior decrypt - - numBlocks, numExtraBytes = divmod(len(self.bytesToDecrypt), self.blockSize) - if more == None: # no more calls to decrypt, should have all the data - if numExtraBytes != 0: - raise DecryptNotBlockAlignedError, 'Data not block aligned on decrypt' - - # hold back some bytes in case last decrypt has zero len - if (more != None) and (numExtraBytes == 0) and (numBlocks >0) : - numBlocks -= 1 - numExtraBytes = self.blockSize - - plainText = '' - for i in range(numBlocks): - bStart = i*self.blockSize - ptBlock = self.decryptBlock(self.bytesToDecrypt[bStart : bStart+self.blockSize]) - self.decryptBlockCount += 1 - plainText += ptBlock - - if numExtraBytes > 0: # save any bytes that are not block aligned - self.bytesToEncrypt = self.bytesToEncrypt[-numExtraBytes:] - else: - self.bytesToEncrypt = '' - - if more == None: # last decrypt remove padding - plainText = self.padding.removePad(plainText, self.blockSize) - self.resetDecrypt() - return plainText - - - class Pad: - def __init__(self): - pass # eventually could put in calculation of min and max size extension - - class padWithPadLen(Pad): - """ Pad a binary string with the length of the padding """ - - def addPad(self, extraBytes, blockSize): - """ Add padding to a binary string to make it an even multiple - of the block size """ - blocks, numExtraBytes = divmod(len(extraBytes), blockSize) - padLength = blockSize - numExtraBytes - return extraBytes + padLength*chr(padLength) - - def removePad(self, paddedBinaryString, blockSize): - """ Remove padding from a binary string """ - if not(0 6 and i%Nk == 4 : - temp = [ Sbox[byte] for byte in temp ] # SubWord(temp) - w.append( [ w[i-Nk][byte]^temp[byte] for byte in range(4) ] ) - return w - - Rcon = (0,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36, # note extra '0' !!! - 0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6, - 0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91) - - #------------------------------------- - def AddRoundKey(algInstance, keyBlock): - """ XOR the algorithm state with a block of key material """ - for column in range(algInstance.Nb): - for row in range(4): - algInstance.state[column][row] ^= keyBlock[column][row] - #------------------------------------- - - def SubBytes(algInstance): - for column in range(algInstance.Nb): - for row in range(4): - algInstance.state[column][row] = Sbox[algInstance.state[column][row]] - - def InvSubBytes(algInstance): - for column in range(algInstance.Nb): - for row in range(4): - algInstance.state[column][row] = InvSbox[algInstance.state[column][row]] - - Sbox = (0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5, - 0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76, - 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0, - 0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0, - 0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc, - 0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15, - 0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a, - 0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75, - 0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0, - 0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84, - 0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b, - 0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf, - 0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85, - 0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8, - 0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5, - 0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2, - 0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17, - 0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73, - 0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88, - 0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb, - 0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c, - 0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79, - 0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9, - 0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08, - 0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6, - 0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a, - 0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e, - 0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e, - 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94, - 0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf, - 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68, - 0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16) - - InvSbox = (0x52,0x09,0x6a,0xd5,0x30,0x36,0xa5,0x38, - 0xbf,0x40,0xa3,0x9e,0x81,0xf3,0xd7,0xfb, - 0x7c,0xe3,0x39,0x82,0x9b,0x2f,0xff,0x87, - 0x34,0x8e,0x43,0x44,0xc4,0xde,0xe9,0xcb, - 0x54,0x7b,0x94,0x32,0xa6,0xc2,0x23,0x3d, - 0xee,0x4c,0x95,0x0b,0x42,0xfa,0xc3,0x4e, - 0x08,0x2e,0xa1,0x66,0x28,0xd9,0x24,0xb2, - 0x76,0x5b,0xa2,0x49,0x6d,0x8b,0xd1,0x25, - 0x72,0xf8,0xf6,0x64,0x86,0x68,0x98,0x16, - 0xd4,0xa4,0x5c,0xcc,0x5d,0x65,0xb6,0x92, - 0x6c,0x70,0x48,0x50,0xfd,0xed,0xb9,0xda, - 0x5e,0x15,0x46,0x57,0xa7,0x8d,0x9d,0x84, - 0x90,0xd8,0xab,0x00,0x8c,0xbc,0xd3,0x0a, - 0xf7,0xe4,0x58,0x05,0xb8,0xb3,0x45,0x06, - 0xd0,0x2c,0x1e,0x8f,0xca,0x3f,0x0f,0x02, - 0xc1,0xaf,0xbd,0x03,0x01,0x13,0x8a,0x6b, - 0x3a,0x91,0x11,0x41,0x4f,0x67,0xdc,0xea, - 0x97,0xf2,0xcf,0xce,0xf0,0xb4,0xe6,0x73, - 0x96,0xac,0x74,0x22,0xe7,0xad,0x35,0x85, - 0xe2,0xf9,0x37,0xe8,0x1c,0x75,0xdf,0x6e, - 0x47,0xf1,0x1a,0x71,0x1d,0x29,0xc5,0x89, - 0x6f,0xb7,0x62,0x0e,0xaa,0x18,0xbe,0x1b, - 0xfc,0x56,0x3e,0x4b,0xc6,0xd2,0x79,0x20, - 0x9a,0xdb,0xc0,0xfe,0x78,0xcd,0x5a,0xf4, - 0x1f,0xdd,0xa8,0x33,0x88,0x07,0xc7,0x31, - 0xb1,0x12,0x10,0x59,0x27,0x80,0xec,0x5f, - 0x60,0x51,0x7f,0xa9,0x19,0xb5,0x4a,0x0d, - 0x2d,0xe5,0x7a,0x9f,0x93,0xc9,0x9c,0xef, - 0xa0,0xe0,0x3b,0x4d,0xae,0x2a,0xf5,0xb0, - 0xc8,0xeb,0xbb,0x3c,0x83,0x53,0x99,0x61, - 0x17,0x2b,0x04,0x7e,0xba,0x77,0xd6,0x26, - 0xe1,0x69,0x14,0x63,0x55,0x21,0x0c,0x7d) - - #------------------------------------- - """ For each block size (Nb), the ShiftRow operation shifts row i - by the amount Ci. Note that row 0 is not shifted. - Nb C1 C2 C3 - ------------------- """ - shiftOffset = { 4 : ( 0, 1, 2, 3), - 5 : ( 0, 1, 2, 3), - 6 : ( 0, 1, 2, 3), - 7 : ( 0, 1, 2, 4), - 8 : ( 0, 1, 3, 4) } - def ShiftRows(algInstance): - tmp = [0]*algInstance.Nb # list of size Nb - for r in range(1,4): # row 0 reamains unchanged and can be skipped - for c in range(algInstance.Nb): - tmp[c] = algInstance.state[(c+shiftOffset[algInstance.Nb][r]) % algInstance.Nb][r] - for c in range(algInstance.Nb): - algInstance.state[c][r] = tmp[c] - def InvShiftRows(algInstance): - tmp = [0]*algInstance.Nb # list of size Nb - for r in range(1,4): # row 0 reamains unchanged and can be skipped - for c in range(algInstance.Nb): - tmp[c] = algInstance.state[(c+algInstance.Nb-shiftOffset[algInstance.Nb][r]) % algInstance.Nb][r] - for c in range(algInstance.Nb): - algInstance.state[c][r] = tmp[c] - #------------------------------------- - def MixColumns(a): - Sprime = [0,0,0,0] - for j in range(a.Nb): # for each column - Sprime[0] = mul(2,a.state[j][0])^mul(3,a.state[j][1])^mul(1,a.state[j][2])^mul(1,a.state[j][3]) - Sprime[1] = mul(1,a.state[j][0])^mul(2,a.state[j][1])^mul(3,a.state[j][2])^mul(1,a.state[j][3]) - Sprime[2] = mul(1,a.state[j][0])^mul(1,a.state[j][1])^mul(2,a.state[j][2])^mul(3,a.state[j][3]) - Sprime[3] = mul(3,a.state[j][0])^mul(1,a.state[j][1])^mul(1,a.state[j][2])^mul(2,a.state[j][3]) - for i in range(4): - a.state[j][i] = Sprime[i] - - def InvMixColumns(a): - """ Mix the four bytes of every column in a linear way - This is the opposite operation of Mixcolumn """ - Sprime = [0,0,0,0] - for j in range(a.Nb): # for each column - Sprime[0] = mul(0x0E,a.state[j][0])^mul(0x0B,a.state[j][1])^mul(0x0D,a.state[j][2])^mul(0x09,a.state[j][3]) - Sprime[1] = mul(0x09,a.state[j][0])^mul(0x0E,a.state[j][1])^mul(0x0B,a.state[j][2])^mul(0x0D,a.state[j][3]) - Sprime[2] = mul(0x0D,a.state[j][0])^mul(0x09,a.state[j][1])^mul(0x0E,a.state[j][2])^mul(0x0B,a.state[j][3]) - Sprime[3] = mul(0x0B,a.state[j][0])^mul(0x0D,a.state[j][1])^mul(0x09,a.state[j][2])^mul(0x0E,a.state[j][3]) - for i in range(4): - a.state[j][i] = Sprime[i] - - #------------------------------------- - def mul(a, b): - """ Multiply two elements of GF(2^m) - needed for MixColumn and InvMixColumn """ - if (a !=0 and b!=0): - return Alogtable[(Logtable[a] + Logtable[b])%255] - else: - return 0 - - Logtable = ( 0, 0, 25, 1, 50, 2, 26, 198, 75, 199, 27, 104, 51, 238, 223, 3, - 100, 4, 224, 14, 52, 141, 129, 239, 76, 113, 8, 200, 248, 105, 28, 193, - 125, 194, 29, 181, 249, 185, 39, 106, 77, 228, 166, 114, 154, 201, 9, 120, - 101, 47, 138, 5, 33, 15, 225, 36, 18, 240, 130, 69, 53, 147, 218, 142, - 150, 143, 219, 189, 54, 208, 206, 148, 19, 92, 210, 241, 64, 70, 131, 56, - 102, 221, 253, 48, 191, 6, 139, 98, 179, 37, 226, 152, 34, 136, 145, 16, - 126, 110, 72, 195, 163, 182, 30, 66, 58, 107, 40, 84, 250, 133, 61, 186, - 43, 121, 10, 21, 155, 159, 94, 202, 78, 212, 172, 229, 243, 115, 167, 87, - 175, 88, 168, 80, 244, 234, 214, 116, 79, 174, 233, 213, 231, 230, 173, 232, - 44, 215, 117, 122, 235, 22, 11, 245, 89, 203, 95, 176, 156, 169, 81, 160, - 127, 12, 246, 111, 23, 196, 73, 236, 216, 67, 31, 45, 164, 118, 123, 183, - 204, 187, 62, 90, 251, 96, 177, 134, 59, 82, 161, 108, 170, 85, 41, 157, - 151, 178, 135, 144, 97, 190, 220, 252, 188, 149, 207, 205, 55, 63, 91, 209, - 83, 57, 132, 60, 65, 162, 109, 71, 20, 42, 158, 93, 86, 242, 211, 171, - 68, 17, 146, 217, 35, 32, 46, 137, 180, 124, 184, 38, 119, 153, 227, 165, - 103, 74, 237, 222, 197, 49, 254, 24, 13, 99, 140, 128, 192, 247, 112, 7) - - Alogtable= ( 1, 3, 5, 15, 17, 51, 85, 255, 26, 46, 114, 150, 161, 248, 19, 53, - 95, 225, 56, 72, 216, 115, 149, 164, 247, 2, 6, 10, 30, 34, 102, 170, - 229, 52, 92, 228, 55, 89, 235, 38, 106, 190, 217, 112, 144, 171, 230, 49, - 83, 245, 4, 12, 20, 60, 68, 204, 79, 209, 104, 184, 211, 110, 178, 205, - 76, 212, 103, 169, 224, 59, 77, 215, 98, 166, 241, 8, 24, 40, 120, 136, - 131, 158, 185, 208, 107, 189, 220, 127, 129, 152, 179, 206, 73, 219, 118, 154, - 181, 196, 87, 249, 16, 48, 80, 240, 11, 29, 39, 105, 187, 214, 97, 163, - 254, 25, 43, 125, 135, 146, 173, 236, 47, 113, 147, 174, 233, 32, 96, 160, - 251, 22, 58, 78, 210, 109, 183, 194, 93, 231, 50, 86, 250, 21, 63, 65, - 195, 94, 226, 61, 71, 201, 64, 192, 91, 237, 44, 116, 156, 191, 218, 117, - 159, 186, 213, 100, 172, 239, 42, 126, 130, 157, 188, 223, 122, 142, 137, 128, - 155, 182, 193, 88, 232, 35, 101, 175, 234, 37, 111, 177, 200, 67, 197, 84, - 252, 31, 33, 99, 165, 244, 7, 9, 27, 45, 119, 153, 176, 203, 70, 202, - 69, 207, 74, 222, 121, 139, 134, 145, 168, 227, 62, 66, 198, 81, 243, 14, - 18, 54, 90, 238, 41, 123, 141, 140, 143, 138, 133, 148, 167, 242, 13, 23, - 57, 75, 221, 124, 132, 151, 162, 253, 28, 36, 108, 180, 199, 82, 246, 1) - - - - - """ - AES Encryption Algorithm - The AES algorithm is just Rijndael algorithm restricted to the default - blockSize of 128 bits. - """ - - class AES(Rijndael): - """ The AES algorithm is the Rijndael block cipher restricted to block - sizes of 128 bits and key sizes of 128, 192 or 256 bits - """ - def __init__(self, key = None, padding = padWithPadLen(), keySize=16): - """ Initialize AES, keySize is in bytes """ - if not (keySize == 16 or keySize == 24 or keySize == 32) : - raise BadKeySizeError, 'Illegal AES key size, must be 16, 24, or 32 bytes' - - Rijndael.__init__( self, key, padding=padding, keySize=keySize, blockSize=16 ) - - self.name = 'AES' - - - """ - CBC mode of encryption for block ciphers. - This algorithm mode wraps any BlockCipher to make a - Cipher Block Chaining mode. - """ - from random import Random # should change to crypto.random!!! - - - class CBC(BlockCipher): - """ The CBC class wraps block ciphers to make cipher block chaining (CBC) mode - algorithms. The initialization (IV) is automatic if set to None. Padding - is also automatic based on the Pad class used to initialize the algorithm - """ - def __init__(self, blockCipherInstance, padding = padWithPadLen()): - """ CBC algorithms are created by initializing with a BlockCipher instance """ - self.baseCipher = blockCipherInstance - self.name = self.baseCipher.name + '_CBC' - self.blockSize = self.baseCipher.blockSize - self.keySize = self.baseCipher.keySize - self.padding = padding - self.baseCipher.padding = noPadding() # baseCipher should NOT pad!! - self.r = Random() # for IV generation, currently uses - # mediocre standard distro version <---------------- - import time - newSeed = time.ctime()+str(self.r) # seed with instance location - self.r.seed(newSeed) # to make unique - self.reset() - - def setKey(self, key): - self.baseCipher.setKey(key) - - # Overload to reset both CBC state and the wrapped baseCipher - def resetEncrypt(self): - BlockCipher.resetEncrypt(self) # reset CBC encrypt state (super class) - self.baseCipher.resetEncrypt() # reset base cipher encrypt state - - def resetDecrypt(self): - BlockCipher.resetDecrypt(self) # reset CBC state (super class) - self.baseCipher.resetDecrypt() # reset base cipher decrypt state - - def encrypt(self, plainText, iv=None, more=None): - """ CBC encryption - overloads baseCipher to allow optional explicit IV - when iv=None, iv is auto generated! - """ - if self.encryptBlockCount == 0: - self.iv = iv - else: - assert(iv==None), 'IV used only on first call to encrypt' - - return BlockCipher.encrypt(self,plainText, more=more) - - def decrypt(self, cipherText, iv=None, more=None): - """ CBC decryption - overloads baseCipher to allow optional explicit IV - when iv=None, iv is auto generated! - """ - if self.decryptBlockCount == 0: - self.iv = iv - else: - assert(iv==None), 'IV used only on first call to decrypt' - - return BlockCipher.decrypt(self, cipherText, more=more) - - def encryptBlock(self, plainTextBlock): - """ CBC block encryption, IV is set with 'encrypt' """ - auto_IV = '' - if self.encryptBlockCount == 0: - if self.iv == None: - # generate IV and use - self.iv = ''.join([chr(self.r.randrange(256)) for i in range(self.blockSize)]) - self.prior_encr_CT_block = self.iv - auto_IV = self.prior_encr_CT_block # prepend IV if it's automatic - else: # application provided IV - assert(len(self.iv) == self.blockSize ),'IV must be same length as block' - self.prior_encr_CT_block = self.iv - """ encrypt the prior CT XORed with the PT """ - ct = self.baseCipher.encryptBlock( xor(self.prior_encr_CT_block, plainTextBlock) ) - self.prior_encr_CT_block = ct - return auto_IV+ct - - def decryptBlock(self, encryptedBlock): - """ Decrypt a single block """ - - if self.decryptBlockCount == 0: # first call, process IV - if self.iv == None: # auto decrypt IV? - self.prior_CT_block = encryptedBlock - return '' - else: - assert(len(self.iv)==self.blockSize),"Bad IV size on CBC decryption" - self.prior_CT_block = self.iv - - dct = self.baseCipher.decryptBlock(encryptedBlock) - """ XOR the prior decrypted CT with the prior CT """ - dct_XOR_priorCT = xor( self.prior_CT_block, dct ) - - self.prior_CT_block = encryptedBlock - - return dct_XOR_priorCT - - - """ - AES_CBC Encryption Algorithm - """ - - class aescbc_AES_CBC(CBC): - """ AES encryption in CBC feedback mode """ - def __init__(self, key=None, padding=padWithPadLen(), keySize=16): - CBC.__init__( self, AES(key, noPadding(), keySize), padding) - self.name = 'AES_CBC' - - class AES_CBC(object): - def __init__(self): - self._key = None - self._iv = None - self.aes = None - - def set_decrypt_key(self, userkey, iv): - self._key = userkey - self._iv = iv - self.aes = aescbc_AES_CBC(userkey, noPadding(), len(userkey)) - - def decrypt(self, data): - iv = self._iv - cleartext = self.aes.decrypt(iv + data) - return cleartext - - import hmac - - class KeyIVGen(object): - # this only exists in openssl so we will use pure python implementation instead - # PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1', - # [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p]) - def pbkdf2(self, passwd, salt, iter, keylen): - - def xorstr( a, b ): - if len(a) != len(b): - raise Exception("xorstr(): lengths differ") - return ''.join((chr(ord(x)^ord(y)) for x, y in zip(a, b))) - - def prf( h, data ): - hm = h.copy() - hm.update( data ) - return hm.digest() - - def pbkdf2_F( h, salt, itercount, blocknum ): - U = prf( h, salt + pack('>i',blocknum ) ) - T = U - for i in range(2, itercount+1): - U = prf( h, U ) - T = xorstr( T, U ) - return T - - sha = hashlib.sha1 - digest_size = sha().digest_size - # l - number of output blocks to produce - l = keylen / digest_size - if keylen % digest_size != 0: - l += 1 - h = hmac.new( passwd, None, sha ) - T = "" - for i in range(1, l+1): - T += pbkdf2_F( h, salt, iter, i ) - return T[0: keylen] - - def UnprotectHeaderData(encryptedData): - passwdData = 'header_key_data' - salt = 'HEADER.2011' - iter = 0x80 - keylen = 0x100 - key_iv = KeyIVGen().pbkdf2(passwdData, salt, iter, keylen) - key = key_iv[0:32] - iv = key_iv[32:48] - aes=AES_CBC() - aes.set_decrypt_key(key, iv) - cleartext = aes.decrypt(encryptedData) - return cleartext - - # Various character maps used to decrypt kindle info values. - # Probably supposed to act as obfuscation - charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_" - charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE" - # New maps in K4PC 1.9.0 - testMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" - testMap6 = "9YzAb0Cd1Ef2n5Pr6St7Uvh3Jk4M8WxG" - testMap8 = "YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD" - - # interface with Windows OS Routines - class DataBlob(Structure): - _fields_ = [('cbData', c_uint), - ('pbData', c_void_p)] - DataBlob_p = POINTER(DataBlob) - - - def GetSystemDirectory(): - GetSystemDirectoryW = kernel32.GetSystemDirectoryW - GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint] - GetSystemDirectoryW.restype = c_uint - def GetSystemDirectory(): - buffer = create_unicode_buffer(MAX_PATH + 1) - GetSystemDirectoryW(buffer, len(buffer)) - return buffer.value - return GetSystemDirectory - GetSystemDirectory = GetSystemDirectory() - - def GetVolumeSerialNumber(): - GetVolumeInformationW = kernel32.GetVolumeInformationW - GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint, - POINTER(c_uint), POINTER(c_uint), - POINTER(c_uint), c_wchar_p, c_uint] - GetVolumeInformationW.restype = c_uint - def GetVolumeSerialNumber(path = GetSystemDirectory().split('\\')[0] + '\\'): - vsn = c_uint(0) - GetVolumeInformationW(path, None, 0, byref(vsn), None, None, None, 0) - return str(vsn.value) - return GetVolumeSerialNumber - GetVolumeSerialNumber = GetVolumeSerialNumber() - - def GetIDString(): - vsn = GetVolumeSerialNumber() - #print('Using Volume Serial Number for ID: '+vsn) - return vsn - - def getLastError(): - GetLastError = kernel32.GetLastError - GetLastError.argtypes = None - GetLastError.restype = c_uint - def getLastError(): - return GetLastError() - return getLastError - getLastError = getLastError() + # Get the Mazama Random number + MazamaRandomNumber = (kindleDatabase[1])['MazamaRandomNumber'].decode('hex').encode('ascii') - def GetUserName(): - GetUserNameW = advapi32.GetUserNameW - GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)] - GetUserNameW.restype = c_uint - def GetUserName(): - buffer = create_unicode_buffer(2) - size = c_uint(len(buffer)) - while not GetUserNameW(buffer, byref(size)): - errcd = getLastError() - if errcd == 234: - # bad wine implementation up through wine 1.3.21 - return "AlternateUserName" - buffer = create_unicode_buffer(len(buffer) * 2) - size.value = len(buffer) - return buffer.value.encode('utf-16-le')[::2] - return GetUserName - GetUserName = GetUserName() + # Get the kindle account token + kindleAccountToken = (kindleDatabase[1])['kindle.account.tokens'].decode('hex').encode('ascii') - def CryptUnprotectData(): - _CryptUnprotectData = crypt32.CryptUnprotectData - _CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p, - c_void_p, c_void_p, c_uint, DataBlob_p] - _CryptUnprotectData.restype = c_uint - def CryptUnprotectData(indata, entropy, flags): - indatab = create_string_buffer(indata) - indata = DataBlob(len(indata), cast(indatab, c_void_p)) - entropyb = create_string_buffer(entropy) - entropy = DataBlob(len(entropy), cast(entropyb, c_void_p)) - outdata = DataBlob() - if not _CryptUnprotectData(byref(indata), None, byref(entropy), - None, None, flags, byref(outdata)): - # raise DrmException("Failed to Unprotect Data") - return 'failed' - return string_at(outdata.pbData, outdata.cbData) - return CryptUnprotectData - CryptUnprotectData = CryptUnprotectData() + # Get the IDString used to decode the Kindle Info file + IDString = (kindleDatabase[1])['IDString'].decode('hex').encode('ascii') + # Get the UserName stored when the Kindle Info file was decoded + UserName = (kindleDatabase[1])['UserName'].decode('hex').encode('ascii') - # Locate all of the kindle-info style files and return as list - def getKindleInfoFiles(): - kInfoFiles = [] - # some 64 bit machines do not have the proper registry key for some reason - # or the pythonn interface to the 32 vs 64 bit registry is broken - path = "" - if 'LOCALAPPDATA' in os.environ.keys(): - path = os.environ['LOCALAPPDATA'] - else: - # User Shell Folders show take precedent over Shell Folders if present - try: - regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\") - path = winreg.QueryValueEx(regkey, 'Local AppData')[0] - if not os.path.isdir(path): - path = "" - try: - regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\") - path = winreg.QueryValueEx(regkey, 'Local AppData')[0] - if not os.path.isdir(path): - path = "" - except RegError: - pass - except RegError: - pass + except KeyError: + print u"Keys not found in the database {0}.".format(kindleDatabase[0]) + return pids - found = False - if path == "": - print ('Could not find the folder in which to look for kinfoFiles.') - else: - print('searching for kinfoFiles in ' + path) + # Get the ID string used + encodedIDString = encodeHash(IDString,charMap1) - # look for (K4PC 1.9.0 and later) .kinf2011 file - kinfopath = path +'\\Amazon\\Kindle\\storage\\.kinf2011' - if os.path.isfile(kinfopath): - found = True - print('Found K4PC 1.9+ kinf2011 file: ' + kinfopath) - kInfoFiles.append(kinfopath) + # Get the current user name + encodedUsername = encodeHash(UserName,charMap1) - # look for (K4PC 1.6.0 and later) rainier.2.1.1.kinf file - kinfopath = path +'\\Amazon\\Kindle\\storage\\rainier.2.1.1.kinf' - if os.path.isfile(kinfopath): - found = True - print('Found K4PC 1.6-1.8 kinf file: ' + kinfopath) - kInfoFiles.append(kinfopath) + # concat, hash and encode to calculate the DSN + DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1) - # look for (K4PC 1.5.0 and later) rainier.2.1.1.kinf file - kinfopath = path +'\\Amazon\\Kindle For PC\\storage\\rainier.2.1.1.kinf' - if os.path.isfile(kinfopath): - found = True - print('Found K4PC 1.5 kinf file: ' + kinfopath) - kInfoFiles.append(kinfopath) + # Compute the device PID (for which I can tell, is used for nothing). + table = generatePidEncryptionTable() + devicePID = generateDevicePID(table,DSN,4) + devicePID = checksumPid(devicePID) + pids.append(devicePID) - # look for original (earlier than K4PC 1.5.0) kindle-info files - kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info' - if os.path.isfile(kinfopath): - found = True - print('Found K4PC kindle.info file: ' + kinfopath) - kInfoFiles.append(kinfopath) + # Compute book PIDs - if not found: - print('No K4PC kindle.info/kinf/kinf2011 files have been found.') - return kInfoFiles + # book pid + pidHash = SHA1(DSN+kindleAccountToken+rec209+token) + bookPID = encodePID(pidHash) + bookPID = checksumPid(bookPID) + pids.append(bookPID) + # variant 1 + pidHash = SHA1(kindleAccountToken+rec209+token) + bookPID = encodePID(pidHash) + bookPID = checksumPid(bookPID) + pids.append(bookPID) - # determine type of kindle info provided and return a - # database of keynames and values - def getDBfromFile(kInfoFile): - names = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber', 'max_date', 'SIGVERIF'] - DB = {} - with open(kInfoFile, 'rb') as infoReader: - hdr = infoReader.read(1) - data = infoReader.read() + # variant 2 + pidHash = SHA1(DSN+rec209+token) + bookPID = encodePID(pidHash) + bookPID = checksumPid(bookPID) + pids.append(bookPID) - if data.find('{') != -1 : - # older style kindle-info file - items = data.split('{') - for item in items: - if item != '': - keyhash, rawdata = item.split(':') - keyname = "unknown" - for name in names: - if encodeHash(name,charMap2) == keyhash: - keyname = name - break - if keyname == "unknown": - keyname = keyhash - encryptedValue = decode(rawdata,charMap2) - DB[keyname] = CryptUnprotectData(encryptedValue, "", 0) - elif hdr == '/': - # else rainier-2-1-1 .kinf file - # the .kinf file uses "/" to separate it into records - # so remove the trailing "/" to make it easy to use split - data = data[:-1] - items = data.split('/') + return pids - # loop through the item records until all are processed - while len(items) > 0: +def getPidList(md1, md2, serials=[], kDatabases=[]): + pidlst = [] - # get the first item record - item = items.pop(0) + if kDatabases is None: + kDatabases = [] + if serials is None: + serials = [] - # the first 32 chars of the first record of a group - # is the MD5 hash of the key name encoded by charMap5 - keyhash = item[0:32] - - # the raw keyhash string is used to create entropy for the actual - # CryptProtectData Blob that represents that keys contents - entropy = SHA1(keyhash) - - # the remainder of the first record when decoded with charMap5 - # has the ':' split char followed by the string representation - # of the number of records that follow - # and make up the contents - srcnt = decode(item[34:],charMap5) - rcnt = int(srcnt) - - # read and store in rcnt records of data - # that make up the contents value - edlst = [] - for i in xrange(rcnt): - item = items.pop(0) - edlst.append(item) - - keyname = "unknown" - for name in names: - if encodeHash(name,charMap5) == keyhash: - keyname = name - break - if keyname == "unknown": - keyname = keyhash - # the charMap5 encoded contents data has had a length - # of chars (always odd) cut off of the front and moved - # to the end to prevent decoding using charMap5 from - # working properly, and thereby preventing the ensuing - # CryptUnprotectData call from succeeding. - - # The offset into the charMap5 encoded contents seems to be: - # len(contents)-largest prime number <= int(len(content)/3) - # (in other words split "about" 2/3rds of the way through) - - # move first offsets chars to end to align for decode by charMap5 - encdata = "".join(edlst) - contlen = len(encdata) - noffset = contlen - primes(int(contlen/3))[-1] - - # now properly split and recombine - # by moving noffset chars from the start of the - # string to the end of the string - pfx = encdata[0:noffset] - encdata = encdata[noffset:] - encdata = encdata + pfx - - # decode using Map5 to get the CryptProtect Data - encryptedValue = decode(encdata,charMap5) - DB[keyname] = CryptUnprotectData(encryptedValue, entropy, 1) - else: - # else newest .kinf2011 style .kinf file - # the .kinf file uses "/" to separate it into records - # so remove the trailing "/" to make it easy to use split - # need to put back the first char read because it it part - # of the added entropy blob - data = hdr + data[:-1] - items = data.split('/') - - # starts with and encoded and encrypted header blob - headerblob = items.pop(0) - encryptedValue = decode(headerblob, testMap1) - cleartext = UnprotectHeaderData(encryptedValue) - # now extract the pieces that form the added entropy - pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) - for m in re.finditer(pattern, cleartext): - added_entropy = m.group(2) + m.group(4) - - - # loop through the item records until all are processed - while len(items) > 0: - - # get the first item record - item = items.pop(0) - - # the first 32 chars of the first record of a group - # is the MD5 hash of the key name encoded by charMap5 - keyhash = item[0:32] - - # the sha1 of raw keyhash string is used to create entropy along - # with the added entropy provided above from the headerblob - entropy = SHA1(keyhash) + added_entropy - - # the remainder of the first record when decoded with charMap5 - # has the ':' split char followed by the string representation - # of the number of records that follow - # and make up the contents - srcnt = decode(item[34:],charMap5) - rcnt = int(srcnt) - - # read and store in rcnt records of data - # that make up the contents value - edlst = [] - for i in xrange(rcnt): - item = items.pop(0) - edlst.append(item) - - # key names now use the new testMap8 encoding - keyname = "unknown" - for name in names: - if encodeHash(name,testMap8) == keyhash: - keyname = name - break - - # the testMap8 encoded contents data has had a length - # of chars (always odd) cut off of the front and moved - # to the end to prevent decoding using testMap8 from - # working properly, and thereby preventing the ensuing - # CryptUnprotectData call from succeeding. - - # The offset into the testMap8 encoded contents seems to be: - # len(contents)-largest prime number <= int(len(content)/3) - # (in other words split "about" 2/3rds of the way through) - - # move first offsets chars to end to align for decode by testMap8 - # by moving noffset chars from the start of the - # string to the end of the string - encdata = "".join(edlst) - contlen = len(encdata) - noffset = contlen - primes(int(contlen/3))[-1] - pfx = encdata[0:noffset] - encdata = encdata[noffset:] - encdata = encdata + pfx - - # decode using new testMap8 to get the original CryptProtect Data - encryptedValue = decode(encdata,testMap8) - cleartext = CryptUnprotectData(encryptedValue, entropy, 1) - DB[keyname] = cleartext - - if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB: - print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(GetIDString(), GetUserName().decode("latin-1")) - # store values used in decryption - DB['IDString'] = GetIDString() - DB['UserName'] = GetUserName() - else: - DB = {} - return DB -elif isosx: - import copy - import subprocess - - # interface to needed routines in openssl's libcrypto - def _load_crypto_libcrypto(): - from ctypes import CDLL, byref, POINTER, c_void_p, c_char_p, c_int, c_long, \ - Structure, c_ulong, create_string_buffer, addressof, string_at, cast - from ctypes.util import find_library - - libcrypto = find_library('crypto') - if libcrypto is None: - raise DrmException(u"libcrypto not found") - libcrypto = CDLL(libcrypto) - - # From OpenSSL's crypto aes header - # - # AES_ENCRYPT 1 - # AES_DECRYPT 0 - # AES_MAXNR 14 (in bytes) - # AES_BLOCK_SIZE 16 (in bytes) - # - # struct aes_key_st { - # unsigned long rd_key[4 *(AES_MAXNR + 1)]; - # int rounds; - # }; - # typedef struct aes_key_st AES_KEY; - # - # int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key); - # - # note: the ivec string, and output buffer are both mutable - # void AES_cbc_encrypt(const unsigned char *in, unsigned char *out, - # const unsigned long length, const AES_KEY *key, unsigned char *ivec, const int enc); - - AES_MAXNR = 14 - c_char_pp = POINTER(c_char_p) - c_int_p = POINTER(c_int) - - class AES_KEY(Structure): - _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)] - AES_KEY_p = POINTER(AES_KEY) - - def F(restype, name, argtypes): - func = getattr(libcrypto, name) - func.restype = restype - func.argtypes = argtypes - return func - - AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,c_int]) - - AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',[c_char_p, c_int, AES_KEY_p]) - - # From OpenSSL's Crypto evp/p5_crpt2.c - # - # int PKCS5_PBKDF2_HMAC_SHA1(const char *pass, int passlen, - # const unsigned char *salt, int saltlen, int iter, - # int keylen, unsigned char *out); - - PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1', - [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p]) - - class LibCrypto(object): - def __init__(self): - self._blocksize = 0 - self._keyctx = None - self._iv = 0 - - def set_decrypt_key(self, userkey, iv): - self._blocksize = len(userkey) - if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : - raise DrmException(u"AES improper key used") - return - keyctx = self._keyctx = AES_KEY() - self._iv = iv - self._userkey = userkey - rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx) - if rv < 0: - raise DrmException(u"Failed to initialize AES key") - - def decrypt(self, data): - out = create_string_buffer(len(data)) - mutable_iv = create_string_buffer(self._iv, len(self._iv)) - keyctx = self._keyctx - rv = AES_cbc_encrypt(data, out, len(data), keyctx, mutable_iv, 0) - if rv == 0: - raise DrmException(u"AES decryption failed") - return out.raw - - def keyivgen(self, passwd, salt, iter, keylen): - saltlen = len(salt) - passlen = len(passwd) - out = create_string_buffer(keylen) - rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out) - return out.raw - return LibCrypto - - def _load_crypto(): - LibCrypto = None + for kDatabase in kDatabases: try: - LibCrypto = _load_crypto_libcrypto() - except (ImportError, DrmException): - pass - return LibCrypto - - LibCrypto = _load_crypto() - - # Various character maps used to decrypt books. Probably supposed to act as obfuscation - charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M' - charMap2 = 'ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM' - - # For kinf approach of K4Mac 1.6.X or later - # On K4PC charMap5 = 'AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE' - # For Mac they seem to re-use charMap2 here - charMap5 = charMap2 - - # new in K4M 1.9.X - testMap8 = 'YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD' - - # uses a sub process to get the Hard Drive Serial Number using ioreg - # returns serial numbers of all internal hard drive drives - def GetVolumesSerialNumbers(): - sernums = [] - sernum = os.getenv('MYSERIALNUMBER') - if sernum != None: - sernums.append(sernum.strip()) - cmdline = '/usr/sbin/ioreg -w 0 -r -c AppleAHCIDiskDriver' - cmdline = cmdline.encode(sys.getfilesystemencoding()) - p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) - out1, out2 = p.communicate() - reslst = out1.split('\n') - cnt = len(reslst) - bsdname = None - sernum = None - foundIt = False - for j in xrange(cnt): - resline = reslst[j] - pp = resline.find('\"Serial Number\" = \"') - if pp >= 0: - sernum = resline[pp+19:-1] - sernums.append(sernum.strip()) - return sernums - - def GetUserHomeAppSupKindleDirParitionName(): - home = os.getenv('HOME') - dpath = home + '/Library' - cmdline = '/sbin/mount' - cmdline = cmdline.encode(sys.getfilesystemencoding()) - p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) - out1, out2 = p.communicate() - reslst = out1.split('\n') - cnt = len(reslst) - disk = '' - foundIt = False - for j in xrange(cnt): - resline = reslst[j] - if resline.startswith('/dev'): - (devpart, mpath) = resline.split(' on ') - dpart = devpart[5:] - pp = mpath.find('(') - if pp >= 0: - mpath = mpath[:pp-1] - if dpath.startswith(mpath): - disk = dpart - return disk - - # uses a sub process to get the UUID of the specified disk partition using ioreg - def GetDiskPartitionUUIDs(diskpart): - uuids = [] - uuidnum = os.getenv('MYUUIDNUMBER') - if uuidnum != None: - uuids.append(strip(uuidnum)) - cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver' - cmdline = cmdline.encode(sys.getfilesystemencoding()) - p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) - out1, out2 = p.communicate() - reslst = out1.split('\n') - cnt = len(reslst) - bsdname = None - uuidnum = None - foundIt = False - nest = 0 - uuidnest = -1 - partnest = -2 - for j in xrange(cnt): - resline = reslst[j] - if resline.find('{') >= 0: - nest += 1 - if resline.find('}') >= 0: - nest -= 1 - pp = resline.find('\"UUID\" = \"') - if pp >= 0: - uuidnum = resline[pp+10:-1] - uuidnum = uuidnum.strip() - uuidnest = nest - if partnest == uuidnest and uuidnest > 0: - foundIt = True - break - bb = resline.find('\"BSD Name\" = \"') - if bb >= 0: - bsdname = resline[bb+14:-1] - bsdname = bsdname.strip() - if (bsdname == diskpart): - partnest = nest - else : - partnest = -2 - if partnest == uuidnest and partnest > 0: - foundIt = True - break - if nest == 0: - partnest = -2 - uuidnest = -1 - uuidnum = None - bsdname = None - if foundIt: - uuids.append(uuidnum) - return uuids - - def GetMACAddressesMunged(): - macnums = [] - macnum = os.getenv('MYMACNUM') - if macnum != None: - macnums.append(macnum) - cmdline = '/sbin/ifconfig en0' - cmdline = cmdline.encode(sys.getfilesystemencoding()) - p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) - out1, out2 = p.communicate() - reslst = out1.split('\n') - cnt = len(reslst) - macnum = None - foundIt = False - for j in xrange(cnt): - resline = reslst[j] - pp = resline.find('ether ') - if pp >= 0: - macnum = resline[pp+6:-1] - macnum = macnum.strip() - # print 'original mac', macnum - # now munge it up the way Kindle app does - # by xoring it with 0xa5 and swapping elements 3 and 4 - maclst = macnum.split(':') - n = len(maclst) - if n != 6: - fountIt = False - break - for i in range(6): - maclst[i] = int('0x' + maclst[i], 0) - mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] - mlst[5] = maclst[5] ^ 0xa5 - mlst[4] = maclst[3] ^ 0xa5 - mlst[3] = maclst[4] ^ 0xa5 - mlst[2] = maclst[2] ^ 0xa5 - mlst[1] = maclst[1] ^ 0xa5 - mlst[0] = maclst[0] ^ 0xa5 - macnum = '%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x' % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5]) - foundIt = True - break - if foundIt: - macnums.append(macnum) - return macnums - - - # uses unix env to get username instead of using sysctlbyname - def GetUserName(): - username = os.getenv('USER') - return username - - def GetIDStrings(): - # Return all possible ID Strings - strings = [] - strings.extend(GetMACAddressesMunged()) - strings.extend(GetVolumesSerialNumbers()) - diskpart = GetUserHomeAppSupKindleDirParitionName() - strings.extend(GetDiskPartitionUUIDs(diskpart)) - strings.append('9999999999') - #print strings - return strings - - - # implements an Pseudo Mac Version of Windows built-in Crypto routine - # used by Kindle for Mac versions < 1.6.0 - class CryptUnprotectData(object): - def __init__(self, IDString): - sp = IDString + '!@#' + GetUserName() - passwdData = encode(SHA256(sp),charMap1) - salt = '16743' - self.crp = LibCrypto() - iter = 0x3e8 - keylen = 0x80 - key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen) - self.key = key_iv[0:32] - self.iv = key_iv[32:48] - self.crp.set_decrypt_key(self.key, self.iv) - - def decrypt(self, encryptedData): - cleartext = self.crp.decrypt(encryptedData) - cleartext = decode(cleartext,charMap1) - return cleartext - - - # implements an Pseudo Mac Version of Windows built-in Crypto routine - # used for Kindle for Mac Versions >= 1.6.0 - class CryptUnprotectDataV2(object): - def __init__(self, IDString): - sp = GetUserName() + ':&%:' + IDString - passwdData = encode(SHA256(sp),charMap5) - # salt generation as per the code - salt = 0x0512981d * 2 * 1 * 1 - salt = str(salt) + GetUserName() - salt = encode(salt,charMap5) - self.crp = LibCrypto() - iter = 0x800 - keylen = 0x400 - key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen) - self.key = key_iv[0:32] - self.iv = key_iv[32:48] - self.crp.set_decrypt_key(self.key, self.iv) - - def decrypt(self, encryptedData): - cleartext = self.crp.decrypt(encryptedData) - cleartext = decode(cleartext, charMap5) - return cleartext - - - # unprotect the new header blob in .kinf2011 - # used in Kindle for Mac Version >= 1.9.0 - def UnprotectHeaderData(encryptedData): - passwdData = 'header_key_data' - salt = 'HEADER.2011' - iter = 0x80 - keylen = 0x100 - crp = LibCrypto() - key_iv = crp.keyivgen(passwdData, salt, iter, keylen) - key = key_iv[0:32] - iv = key_iv[32:48] - crp.set_decrypt_key(key,iv) - cleartext = crp.decrypt(encryptedData) - return cleartext - + pidlst.extend(getK4Pids(md1, md2, kDatabase)) + except Exception, e: + print u"Error getting PIDs from database {0}: {1}".format(kDatabase[0],e.args[0]) + traceback.print_exc() - # implements an Pseudo Mac Version of Windows built-in Crypto routine - # used for Kindle for Mac Versions >= 1.9.0 - class CryptUnprotectDataV3(object): - def __init__(self, entropy, IDString): - sp = GetUserName() + '+@#$%+' + IDString - passwdData = encode(SHA256(sp),charMap2) - salt = entropy - self.crp = LibCrypto() - iter = 0x800 - keylen = 0x400 - key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen) - self.key = key_iv[0:32] - self.iv = key_iv[32:48] - self.crp.set_decrypt_key(self.key, self.iv) - - def decrypt(self, encryptedData): - cleartext = self.crp.decrypt(encryptedData) - cleartext = decode(cleartext, charMap2) - return cleartext - - - # Locate the .kindle-info files - def getKindleInfoFiles(): - # file searches can take a long time on some systems, so just look in known specific places. - kInfoFiles=[] - found = False - home = os.getenv('HOME') - # check for .kinf2011 file in new location (App Store Kindle for Mac) - testpath = home + '/Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/storage/.kinf2011' - if os.path.isfile(testpath): - kInfoFiles.append(testpath) - print('Found k4Mac kinf2011 file: ' + testpath) - found = True - # check for .kinf2011 files from 1.10 - testpath = home + '/Library/Application Support/Kindle/storage/.kinf2011' - if os.path.isfile(testpath): - kInfoFiles.append(testpath) - print('Found k4Mac kinf2011 file: ' + testpath) - found = True - # check for .rainier-2.1.1-kinf files from 1.6 - testpath = home + '/Library/Application Support/Kindle/storage/.rainier-2.1.1-kinf' - if os.path.isfile(testpath): - kInfoFiles.append(testpath) - print('Found k4Mac rainier file: ' + testpath) - found = True - # check for .kindle-info files from 1.4 - testpath = home + '/Library/Application Support/Kindle/storage/.kindle-info' - if os.path.isfile(testpath): - kInfoFiles.append(testpath) - print('Found k4Mac kindle-info file: ' + testpath) - found = True - # check for .kindle-info file from 1.2.2 - testpath = home + '/Library/Application Support/Amazon/Kindle/storage/.kindle-info' - if os.path.isfile(testpath): - kInfoFiles.append(testpath) - print('Found k4Mac kindle-info file: ' + testpath) - found = True - # check for .kindle-info file from 1.0 beta 1 (27214) - testpath = home + '/Library/Application Support/Amazon/Kindle for Mac/storage/.kindle-info' - if os.path.isfile(testpath): - kInfoFiles.append(testpath) - print('Found k4Mac kindle-info file: ' + testpath) - found = True - if not found: - print('No k4Mac kindle-info/rainier/kinf2011 files have been found.') - return kInfoFiles - - # determine type of kindle info provided and return a - # database of keynames and values - def getDBfromFile(kInfoFile): - names = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber', 'max_date', 'SIGVERIF'] - with open(kInfoFile, 'rb') as infoReader: - filehdr = infoReader.read(1) - filedata = infoReader.read() - - IDStrings = GetIDStrings() - for IDString in IDStrings: - DB = {} - #print "trying IDString:",IDString - try: - hdr = filehdr - data = filedata - if data.find('[') != -1 : - # older style kindle-info file - cud = CryptUnprotectData(IDString) - items = data.split('[') - for item in items: - if item != '': - keyhash, rawdata = item.split(':') - keyname = 'unknown' - for name in names: - if encodeHash(name,charMap2) == keyhash: - keyname = name - break - if keyname == 'unknown': - keyname = keyhash - encryptedValue = decode(rawdata,charMap2) - cleartext = cud.decrypt(encryptedValue) - if len(cleartext) > 0: - DB[keyname] = cleartext - if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB: - break - elif hdr == '/': - # else newer style .kinf file used by K4Mac >= 1.6.0 - # the .kinf file uses '/' to separate it into records - # so remove the trailing '/' to make it easy to use split - data = data[:-1] - items = data.split('/') - cud = CryptUnprotectDataV2(IDString) - - # loop through the item records until all are processed - while len(items) > 0: - - # get the first item record - item = items.pop(0) - - # the first 32 chars of the first record of a group - # is the MD5 hash of the key name encoded by charMap5 - keyhash = item[0:32] - keyname = 'unknown' - - # the raw keyhash string is also used to create entropy for the actual - # CryptProtectData Blob that represents that keys contents - # 'entropy' not used for K4Mac only K4PC - # entropy = SHA1(keyhash) - - # the remainder of the first record when decoded with charMap5 - # has the ':' split char followed by the string representation - # of the number of records that follow - # and make up the contents - srcnt = decode(item[34:],charMap5) - rcnt = int(srcnt) - - # read and store in rcnt records of data - # that make up the contents value - edlst = [] - for i in xrange(rcnt): - item = items.pop(0) - edlst.append(item) - - keyname = 'unknown' - for name in names: - if encodeHash(name,charMap5) == keyhash: - keyname = name - break - if keyname == 'unknown': - keyname = keyhash - - # the charMap5 encoded contents data has had a length - # of chars (always odd) cut off of the front and moved - # to the end to prevent decoding using charMap5 from - # working properly, and thereby preventing the ensuing - # CryptUnprotectData call from succeeding. - - # The offset into the charMap5 encoded contents seems to be: - # len(contents) - largest prime number less than or equal to int(len(content)/3) - # (in other words split 'about' 2/3rds of the way through) - - # move first offsets chars to end to align for decode by charMap5 - encdata = ''.join(edlst) - contlen = len(encdata) - - # now properly split and recombine - # by moving noffset chars from the start of the - # string to the end of the string - noffset = contlen - primes(int(contlen/3))[-1] - pfx = encdata[0:noffset] - encdata = encdata[noffset:] - encdata = encdata + pfx - - # decode using charMap5 to get the CryptProtect Data - encryptedValue = decode(encdata,charMap5) - cleartext = cud.decrypt(encryptedValue) - if len(cleartext) > 0: - DB[keyname] = cleartext - - if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB: - break - else: - # the latest .kinf2011 version for K4M 1.9.1 - # put back the hdr char, it is needed - data = hdr + data - data = data[:-1] - items = data.split('/') - - # the headerblob is the encrypted information needed to build the entropy string - headerblob = items.pop(0) - encryptedValue = decode(headerblob, charMap1) - cleartext = UnprotectHeaderData(encryptedValue) - - # now extract the pieces in the same way - # this version is different from K4PC it scales the build number by multipying by 735 - pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) - for m in re.finditer(pattern, cleartext): - entropy = str(int(m.group(2)) * 0x2df) + m.group(4) - - cud = CryptUnprotectDataV3(entropy,IDString) - - # loop through the item records until all are processed - while len(items) > 0: - - # get the first item record - item = items.pop(0) - - # the first 32 chars of the first record of a group - # is the MD5 hash of the key name encoded by charMap5 - keyhash = item[0:32] - keyname = 'unknown' - - # unlike K4PC the keyhash is not used in generating entropy - # entropy = SHA1(keyhash) + added_entropy - # entropy = added_entropy - - # the remainder of the first record when decoded with charMap5 - # has the ':' split char followed by the string representation - # of the number of records that follow - # and make up the contents - srcnt = decode(item[34:],charMap5) - rcnt = int(srcnt) - - # read and store in rcnt records of data - # that make up the contents value - edlst = [] - for i in xrange(rcnt): - item = items.pop(0) - edlst.append(item) - - keyname = 'unknown' - for name in names: - if encodeHash(name,testMap8) == keyhash: - keyname = name - break - if keyname == 'unknown': - keyname = keyhash - - # the testMap8 encoded contents data has had a length - # of chars (always odd) cut off of the front and moved - # to the end to prevent decoding using testMap8 from - # working properly, and thereby preventing the ensuing - # CryptUnprotectData call from succeeding. - - # The offset into the testMap8 encoded contents seems to be: - # len(contents) - largest prime number less than or equal to int(len(content)/3) - # (in other words split 'about' 2/3rds of the way through) - - # move first offsets chars to end to align for decode by testMap8 - encdata = ''.join(edlst) - contlen = len(encdata) - - # now properly split and recombine - # by moving noffset chars from the start of the - # string to the end of the string - noffset = contlen - primes(int(contlen/3))[-1] - pfx = encdata[0:noffset] - encdata = encdata[noffset:] - encdata = encdata + pfx - - # decode using testMap8 to get the CryptProtect Data - encryptedValue = decode(encdata,testMap8) - cleartext = cud.decrypt(encryptedValue) - # print keyname - # print cleartext - if len(cleartext) > 0: - DB[keyname] = cleartext - - if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB: - break - except: - pass - if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB: - # store values used in decryption - print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString, GetUserName()) - DB['IDString'] = IDString - DB['UserName'] = GetUserName() - else: - print u"Couldn't decrypt file." - DB = {} - return DB -else: - def getDBfromFile(kInfoFile): - raise DrmException(u"This script only runs under Windows or Mac OS X.") - return {} - -def kindlekeys(files = []): - keys = [] - if files == []: - files = getKindleInfoFiles() - for file in files: - key = getDBfromFile(file) - if key: - # convert all values to hex, just in case. - for keyname in key: - key[keyname]=key[keyname].encode('hex') - keys.append(key) - return keys - -# interface for Python DeDRM -# returns single key or multiple keys, depending on path or file passed in -def getkey(outpath, files=[]): - keys = kindlekeys(files) - if len(keys) > 0: - if not os.path.isdir(outpath): - outfile = outpath - with file(outfile, 'w') as keyfileout: - keyfileout.write(json.dumps(keys[0])) - print u"Saved a key to {0}".format(outfile) - else: - keycount = 0 - for key in keys: - while True: - keycount += 1 - outfile = os.path.join(outpath,u"kindlekey{0:d}.k4i".format(keycount)) - if not os.path.exists(outfile): - break - with file(outfile, 'w') as keyfileout: - keyfileout.write(json.dumps(key)) - print u"Saved a key to {0}".format(outfile) - return True - return False - -def usage(progname): - print u"Finds, decrypts and saves the default Kindle For Mac/PC encryption keys." - print u"Keys are saved to the current directory, or a specified output directory." - print u"If a file name is passed instead of a directory, only the first key is saved, in that file." - print u"Usage:" - print u" {0:s} [-h] [-k ] []".format(progname) - - -def cli_main(): - sys.stdout=SafeUnbuffered(sys.stdout) - sys.stderr=SafeUnbuffered(sys.stderr) - argv=unicode_argv() - progname = os.path.basename(argv[0]) - print u"{0} v{1}\nCopyright © 2010-2013 some_updates and Apprentice Alf".format(progname,__version__) - - try: - opts, args = getopt.getopt(argv[1:], "hk:") - except getopt.GetoptError, err: - print u"Error in options or arguments: {0}".format(err.args[0]) - usage(progname) - sys.exit(2) - - files = [] - for o, a in opts: - if o == "-h": - usage(progname) - sys.exit(0) - if o == "-k": - files = [a] - - if len(args) > 1: - usage(progname) - sys.exit(2) - - if len(args) == 1: - # save to the specified file or directory - outpath = args[0] - if not os.path.isabs(outpath): - outpath = os.path.abspath(outpath) - else: - # save to the same directory as the script - outpath = os.path.dirname(argv[0]) - - # make sure the outpath is the - outpath = os.path.realpath(os.path.normpath(outpath)) - - if not getkey(outpath, files): - print u"Could not retrieve Kindle for Mac/PC key." - return 0 - - -def gui_main(): - try: - import Tkinter - import Tkconstants - import tkMessageBox - import traceback - except: - return cli_main() - - class ExceptionDialog(Tkinter.Frame): - def __init__(self, root, text): - Tkinter.Frame.__init__(self, root, border=5) - label = Tkinter.Label(self, text=u"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) - - - argv=unicode_argv() - root = Tkinter.Tk() - root.withdraw() - progpath, progname = os.path.split(argv[0]) - success = False - try: - keys = kindlekeys() - keycount = 0 - for key in keys: - while True: - keycount += 1 - outfile = os.path.join(progpath,u"kindlekey{0:d}.k4i".format(keycount)) - if not os.path.exists(outfile): - break - - with file(outfile, 'w') as keyfileout: - keyfileout.write(json.dumps(key)) - success = True - tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile)) - except DrmException, e: - tkMessageBox.showerror(progname, u"Error: {0}".format(str(e))) - except Exception: - root.wm_state('normal') - root.title(progname) - text = traceback.format_exc() - ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1) - root.mainloop() - if not success: - return 1 - return 0 + for serialnum in serials: + try: + pidlst.extend(getKindlePids(md1, md2, serialnum)) + except Exception, e: + print u"Error getting PIDs from serial number {0}: {1}".format(serialnum ,e.args[0]) + traceback.print_exc() -if __name__ == '__main__': - if len(sys.argv) > 1: - sys.exit(cli_main()) - sys.exit(gui_main()) + return pidlst diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/kindlekey.py b/DeDRM_calibre_plugin/DeDRM_plugin/kindlekey.py index 8bbcf69..8852769 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/kindlekey.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/kindlekey.py @@ -1,18 +1,40 @@ -#!/usr/bin/python +#!/usr/bin/env python # -*- coding: utf-8 -*- -# Mobipocket PID calculator v0.4 for Amazon Kindle. -# Copyright (c) 2007, 2009 Igor Skochinsky -# History: -# 0.1 Initial release -# 0.2 Added support for generating PID for iPhone (thanks to mbp) -# 0.3 changed to autoflush stdout, fixed return code usage -# 0.3 updated for unicode -# 0.4 Added support for serial numbers starting with '9', fixed unicode bugs. -# 0.5 moved unicode_argv call inside main for Windows DeDRM compatibility +from __future__ import with_statement -import sys -import binascii +# kindlekey.py +# Copyright © 2010-2013 by some_updates and Apprentice Alf +# +# Currently requires alfcrypto.py which requires the alfcrypto library + +# Revision history: +# 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc. +# 1.1 - Added Tkinter to match adobekey.py +# 1.2 - Fixed testing of successful retrieval on Mac +# 1.3 - Added getkey interface for Windows DeDRM application +# Simplified some of the Kindle for Mac code. +# 1.4 - Remove dependency on alfcrypto +# 1.5 - moved unicode_argv call inside main for Windows DeDRM compatibility +# 1.6 - Fixed a problem getting the disk serial numbers +# 1.7 - Work if TkInter is missing +# 1.8 - Fixes for Kindle for Mac, and non-ascii in Windows user names +# 1.9 - Fixes for Unicode in Windows user names + + +""" +Retrieve Kindle for PC/Mac user key. +""" + +__license__ = 'GPL v3' +__version__ = '1.9' + +import sys, os, re +from struct import pack, unpack, unpack_from +import json +import getopt + +# Routines common to Mac and PC # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get @@ -31,8 +53,11 @@ class SafeUnbuffered: def __getattr__(self, attr): return getattr(self.stream, attr) -iswindows = sys.platform.startswith('win') -isosx = sys.platform.startswith('darwin') +try: + from calibre.constants import iswindows, isosx +except: + iswindows = sys.platform.startswith('win') + isosx = sys.platform.startswith('darwin') def unicode_argv(): if iswindows: @@ -41,8 +66,8 @@ def unicode_argv(): # Versions 2.x of Python don't support Unicode in sys.argv on # Windows, with the underlying Windows API instead replacing multi-byte - # characters with '?'. - + # characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv + # as a list of Unicode strings and encode them as utf-8 from ctypes import POINTER, byref, cdll, c_int, windll from ctypes.wintypes import LPCWSTR, LPWSTR @@ -65,80 +90,1847 @@ def unicode_argv(): xrange(start, argc.value)] # if we don't have any arguments at all, just pass back script name # this should never happen - return [u"kindlepid.py"] + return [u"kindlekey.py"] else: argvencoding = sys.stdin.encoding if argvencoding == None: argvencoding = "utf-8" return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv] -if sys.hexversion >= 0x3000000: - print 'This script is incompatible with Python 3.x. Please install Python 2.7.x.' - sys.exit(2) +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() + +def SHA256(message): + ctx = hashlib.sha256() + ctx.update(message) + return ctx.digest() + +# For K4M/PC 1.6.X and later +# generate table of prime number less than or equal to int n +def primes(n): + if n==2: return [2] + elif n<2: return [] + s=range(3,n+1,2) + mroot = n ** 0.5 + half=(n+1)/2-1 + i=0 + m=3 + while m <= mroot: + if s[i]: + j=(m*m-3)/2 + s[j]=0 + while j 0: # save any bytes that are not block aligned + self.bytesToEncrypt = self.bytesToEncrypt[-numExtraBytes:] + else: + self.bytesToEncrypt = '' + + if more == None: # no more data expected from caller + finalBytes = self.padding.addPad(self.bytesToEncrypt,self.blockSize) + if len(finalBytes) > 0: + ctBlock = self.encryptBlock(finalBytes) + self.encryptBlockCount += 1 + cipherText += ctBlock + self.resetEncrypt() + return cipherText + + def decrypt(self, cipherText, more = None): + """ Decrypt a string and return a string """ + self.bytesToDecrypt += cipherText # append to any bytes from prior decrypt + + numBlocks, numExtraBytes = divmod(len(self.bytesToDecrypt), self.blockSize) + if more == None: # no more calls to decrypt, should have all the data + if numExtraBytes != 0: + raise DecryptNotBlockAlignedError, 'Data not block aligned on decrypt' + + # hold back some bytes in case last decrypt has zero len + if (more != None) and (numExtraBytes == 0) and (numBlocks >0) : + numBlocks -= 1 + numExtraBytes = self.blockSize + + plainText = '' + for i in range(numBlocks): + bStart = i*self.blockSize + ptBlock = self.decryptBlock(self.bytesToDecrypt[bStart : bStart+self.blockSize]) + self.decryptBlockCount += 1 + plainText += ptBlock + + if numExtraBytes > 0: # save any bytes that are not block aligned + self.bytesToEncrypt = self.bytesToEncrypt[-numExtraBytes:] + else: + self.bytesToEncrypt = '' + + if more == None: # last decrypt remove padding + plainText = self.padding.removePad(plainText, self.blockSize) + self.resetDecrypt() + return plainText + + + class Pad: + def __init__(self): + pass # eventually could put in calculation of min and max size extension + + class padWithPadLen(Pad): + """ Pad a binary string with the length of the padding """ + + def addPad(self, extraBytes, blockSize): + """ Add padding to a binary string to make it an even multiple + of the block size """ + blocks, numExtraBytes = divmod(len(extraBytes), blockSize) + padLength = blockSize - numExtraBytes + return extraBytes + padLength*chr(padLength) + + def removePad(self, paddedBinaryString, blockSize): + """ Remove padding from a binary string """ + if not(0 6 and i%Nk == 4 : + temp = [ Sbox[byte] for byte in temp ] # SubWord(temp) + w.append( [ w[i-Nk][byte]^temp[byte] for byte in range(4) ] ) + return w + + Rcon = (0,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36, # note extra '0' !!! + 0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6, + 0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91) + + #------------------------------------- + def AddRoundKey(algInstance, keyBlock): + """ XOR the algorithm state with a block of key material """ + for column in range(algInstance.Nb): + for row in range(4): + algInstance.state[column][row] ^= keyBlock[column][row] + #------------------------------------- + + def SubBytes(algInstance): + for column in range(algInstance.Nb): + for row in range(4): + algInstance.state[column][row] = Sbox[algInstance.state[column][row]] + + def InvSubBytes(algInstance): + for column in range(algInstance.Nb): + for row in range(4): + algInstance.state[column][row] = InvSbox[algInstance.state[column][row]] + + Sbox = (0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5, + 0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76, + 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0, + 0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0, + 0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc, + 0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15, + 0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a, + 0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75, + 0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0, + 0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84, + 0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b, + 0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf, + 0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85, + 0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8, + 0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5, + 0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2, + 0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17, + 0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73, + 0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88, + 0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb, + 0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c, + 0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79, + 0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9, + 0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08, + 0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6, + 0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a, + 0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e, + 0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e, + 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94, + 0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf, + 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68, + 0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16) + + InvSbox = (0x52,0x09,0x6a,0xd5,0x30,0x36,0xa5,0x38, + 0xbf,0x40,0xa3,0x9e,0x81,0xf3,0xd7,0xfb, + 0x7c,0xe3,0x39,0x82,0x9b,0x2f,0xff,0x87, + 0x34,0x8e,0x43,0x44,0xc4,0xde,0xe9,0xcb, + 0x54,0x7b,0x94,0x32,0xa6,0xc2,0x23,0x3d, + 0xee,0x4c,0x95,0x0b,0x42,0xfa,0xc3,0x4e, + 0x08,0x2e,0xa1,0x66,0x28,0xd9,0x24,0xb2, + 0x76,0x5b,0xa2,0x49,0x6d,0x8b,0xd1,0x25, + 0x72,0xf8,0xf6,0x64,0x86,0x68,0x98,0x16, + 0xd4,0xa4,0x5c,0xcc,0x5d,0x65,0xb6,0x92, + 0x6c,0x70,0x48,0x50,0xfd,0xed,0xb9,0xda, + 0x5e,0x15,0x46,0x57,0xa7,0x8d,0x9d,0x84, + 0x90,0xd8,0xab,0x00,0x8c,0xbc,0xd3,0x0a, + 0xf7,0xe4,0x58,0x05,0xb8,0xb3,0x45,0x06, + 0xd0,0x2c,0x1e,0x8f,0xca,0x3f,0x0f,0x02, + 0xc1,0xaf,0xbd,0x03,0x01,0x13,0x8a,0x6b, + 0x3a,0x91,0x11,0x41,0x4f,0x67,0xdc,0xea, + 0x97,0xf2,0xcf,0xce,0xf0,0xb4,0xe6,0x73, + 0x96,0xac,0x74,0x22,0xe7,0xad,0x35,0x85, + 0xe2,0xf9,0x37,0xe8,0x1c,0x75,0xdf,0x6e, + 0x47,0xf1,0x1a,0x71,0x1d,0x29,0xc5,0x89, + 0x6f,0xb7,0x62,0x0e,0xaa,0x18,0xbe,0x1b, + 0xfc,0x56,0x3e,0x4b,0xc6,0xd2,0x79,0x20, + 0x9a,0xdb,0xc0,0xfe,0x78,0xcd,0x5a,0xf4, + 0x1f,0xdd,0xa8,0x33,0x88,0x07,0xc7,0x31, + 0xb1,0x12,0x10,0x59,0x27,0x80,0xec,0x5f, + 0x60,0x51,0x7f,0xa9,0x19,0xb5,0x4a,0x0d, + 0x2d,0xe5,0x7a,0x9f,0x93,0xc9,0x9c,0xef, + 0xa0,0xe0,0x3b,0x4d,0xae,0x2a,0xf5,0xb0, + 0xc8,0xeb,0xbb,0x3c,0x83,0x53,0x99,0x61, + 0x17,0x2b,0x04,0x7e,0xba,0x77,0xd6,0x26, + 0xe1,0x69,0x14,0x63,0x55,0x21,0x0c,0x7d) + + #------------------------------------- + """ For each block size (Nb), the ShiftRow operation shifts row i + by the amount Ci. Note that row 0 is not shifted. + Nb C1 C2 C3 + ------------------- """ + shiftOffset = { 4 : ( 0, 1, 2, 3), + 5 : ( 0, 1, 2, 3), + 6 : ( 0, 1, 2, 3), + 7 : ( 0, 1, 2, 4), + 8 : ( 0, 1, 3, 4) } + def ShiftRows(algInstance): + tmp = [0]*algInstance.Nb # list of size Nb + for r in range(1,4): # row 0 reamains unchanged and can be skipped + for c in range(algInstance.Nb): + tmp[c] = algInstance.state[(c+shiftOffset[algInstance.Nb][r]) % algInstance.Nb][r] + for c in range(algInstance.Nb): + algInstance.state[c][r] = tmp[c] + def InvShiftRows(algInstance): + tmp = [0]*algInstance.Nb # list of size Nb + for r in range(1,4): # row 0 reamains unchanged and can be skipped + for c in range(algInstance.Nb): + tmp[c] = algInstance.state[(c+algInstance.Nb-shiftOffset[algInstance.Nb][r]) % algInstance.Nb][r] + for c in range(algInstance.Nb): + algInstance.state[c][r] = tmp[c] + #------------------------------------- + def MixColumns(a): + Sprime = [0,0,0,0] + for j in range(a.Nb): # for each column + Sprime[0] = mul(2,a.state[j][0])^mul(3,a.state[j][1])^mul(1,a.state[j][2])^mul(1,a.state[j][3]) + Sprime[1] = mul(1,a.state[j][0])^mul(2,a.state[j][1])^mul(3,a.state[j][2])^mul(1,a.state[j][3]) + Sprime[2] = mul(1,a.state[j][0])^mul(1,a.state[j][1])^mul(2,a.state[j][2])^mul(3,a.state[j][3]) + Sprime[3] = mul(3,a.state[j][0])^mul(1,a.state[j][1])^mul(1,a.state[j][2])^mul(2,a.state[j][3]) + for i in range(4): + a.state[j][i] = Sprime[i] + + def InvMixColumns(a): + """ Mix the four bytes of every column in a linear way + This is the opposite operation of Mixcolumn """ + Sprime = [0,0,0,0] + for j in range(a.Nb): # for each column + Sprime[0] = mul(0x0E,a.state[j][0])^mul(0x0B,a.state[j][1])^mul(0x0D,a.state[j][2])^mul(0x09,a.state[j][3]) + Sprime[1] = mul(0x09,a.state[j][0])^mul(0x0E,a.state[j][1])^mul(0x0B,a.state[j][2])^mul(0x0D,a.state[j][3]) + Sprime[2] = mul(0x0D,a.state[j][0])^mul(0x09,a.state[j][1])^mul(0x0E,a.state[j][2])^mul(0x0B,a.state[j][3]) + Sprime[3] = mul(0x0B,a.state[j][0])^mul(0x0D,a.state[j][1])^mul(0x09,a.state[j][2])^mul(0x0E,a.state[j][3]) + for i in range(4): + a.state[j][i] = Sprime[i] + + #------------------------------------- + def mul(a, b): + """ Multiply two elements of GF(2^m) + needed for MixColumn and InvMixColumn """ + if (a !=0 and b!=0): + return Alogtable[(Logtable[a] + Logtable[b])%255] + else: + return 0 + + Logtable = ( 0, 0, 25, 1, 50, 2, 26, 198, 75, 199, 27, 104, 51, 238, 223, 3, + 100, 4, 224, 14, 52, 141, 129, 239, 76, 113, 8, 200, 248, 105, 28, 193, + 125, 194, 29, 181, 249, 185, 39, 106, 77, 228, 166, 114, 154, 201, 9, 120, + 101, 47, 138, 5, 33, 15, 225, 36, 18, 240, 130, 69, 53, 147, 218, 142, + 150, 143, 219, 189, 54, 208, 206, 148, 19, 92, 210, 241, 64, 70, 131, 56, + 102, 221, 253, 48, 191, 6, 139, 98, 179, 37, 226, 152, 34, 136, 145, 16, + 126, 110, 72, 195, 163, 182, 30, 66, 58, 107, 40, 84, 250, 133, 61, 186, + 43, 121, 10, 21, 155, 159, 94, 202, 78, 212, 172, 229, 243, 115, 167, 87, + 175, 88, 168, 80, 244, 234, 214, 116, 79, 174, 233, 213, 231, 230, 173, 232, + 44, 215, 117, 122, 235, 22, 11, 245, 89, 203, 95, 176, 156, 169, 81, 160, + 127, 12, 246, 111, 23, 196, 73, 236, 216, 67, 31, 45, 164, 118, 123, 183, + 204, 187, 62, 90, 251, 96, 177, 134, 59, 82, 161, 108, 170, 85, 41, 157, + 151, 178, 135, 144, 97, 190, 220, 252, 188, 149, 207, 205, 55, 63, 91, 209, + 83, 57, 132, 60, 65, 162, 109, 71, 20, 42, 158, 93, 86, 242, 211, 171, + 68, 17, 146, 217, 35, 32, 46, 137, 180, 124, 184, 38, 119, 153, 227, 165, + 103, 74, 237, 222, 197, 49, 254, 24, 13, 99, 140, 128, 192, 247, 112, 7) + + Alogtable= ( 1, 3, 5, 15, 17, 51, 85, 255, 26, 46, 114, 150, 161, 248, 19, 53, + 95, 225, 56, 72, 216, 115, 149, 164, 247, 2, 6, 10, 30, 34, 102, 170, + 229, 52, 92, 228, 55, 89, 235, 38, 106, 190, 217, 112, 144, 171, 230, 49, + 83, 245, 4, 12, 20, 60, 68, 204, 79, 209, 104, 184, 211, 110, 178, 205, + 76, 212, 103, 169, 224, 59, 77, 215, 98, 166, 241, 8, 24, 40, 120, 136, + 131, 158, 185, 208, 107, 189, 220, 127, 129, 152, 179, 206, 73, 219, 118, 154, + 181, 196, 87, 249, 16, 48, 80, 240, 11, 29, 39, 105, 187, 214, 97, 163, + 254, 25, 43, 125, 135, 146, 173, 236, 47, 113, 147, 174, 233, 32, 96, 160, + 251, 22, 58, 78, 210, 109, 183, 194, 93, 231, 50, 86, 250, 21, 63, 65, + 195, 94, 226, 61, 71, 201, 64, 192, 91, 237, 44, 116, 156, 191, 218, 117, + 159, 186, 213, 100, 172, 239, 42, 126, 130, 157, 188, 223, 122, 142, 137, 128, + 155, 182, 193, 88, 232, 35, 101, 175, 234, 37, 111, 177, 200, 67, 197, 84, + 252, 31, 33, 99, 165, 244, 7, 9, 27, 45, 119, 153, 176, 203, 70, 202, + 69, 207, 74, 222, 121, 139, 134, 145, 168, 227, 62, 66, 198, 81, 243, 14, + 18, 54, 90, 238, 41, 123, 141, 140, 143, 138, 133, 148, 167, 242, 13, 23, + 57, 75, 221, 124, 132, 151, 162, 253, 28, 36, 108, 180, 199, 82, 246, 1) + + + + + """ + AES Encryption Algorithm + The AES algorithm is just Rijndael algorithm restricted to the default + blockSize of 128 bits. + """ + + class AES(Rijndael): + """ The AES algorithm is the Rijndael block cipher restricted to block + sizes of 128 bits and key sizes of 128, 192 or 256 bits + """ + def __init__(self, key = None, padding = padWithPadLen(), keySize=16): + """ Initialize AES, keySize is in bytes """ + if not (keySize == 16 or keySize == 24 or keySize == 32) : + raise BadKeySizeError, 'Illegal AES key size, must be 16, 24, or 32 bytes' + + Rijndael.__init__( self, key, padding=padding, keySize=keySize, blockSize=16 ) + + self.name = 'AES' + + + """ + CBC mode of encryption for block ciphers. + This algorithm mode wraps any BlockCipher to make a + Cipher Block Chaining mode. + """ + from random import Random # should change to crypto.random!!! + + + class CBC(BlockCipher): + """ The CBC class wraps block ciphers to make cipher block chaining (CBC) mode + algorithms. The initialization (IV) is automatic if set to None. Padding + is also automatic based on the Pad class used to initialize the algorithm + """ + def __init__(self, blockCipherInstance, padding = padWithPadLen()): + """ CBC algorithms are created by initializing with a BlockCipher instance """ + self.baseCipher = blockCipherInstance + self.name = self.baseCipher.name + '_CBC' + self.blockSize = self.baseCipher.blockSize + self.keySize = self.baseCipher.keySize + self.padding = padding + self.baseCipher.padding = noPadding() # baseCipher should NOT pad!! + self.r = Random() # for IV generation, currently uses + # mediocre standard distro version <---------------- + import time + newSeed = time.ctime()+str(self.r) # seed with instance location + self.r.seed(newSeed) # to make unique + self.reset() + + def setKey(self, key): + self.baseCipher.setKey(key) + + # Overload to reset both CBC state and the wrapped baseCipher + def resetEncrypt(self): + BlockCipher.resetEncrypt(self) # reset CBC encrypt state (super class) + self.baseCipher.resetEncrypt() # reset base cipher encrypt state + + def resetDecrypt(self): + BlockCipher.resetDecrypt(self) # reset CBC state (super class) + self.baseCipher.resetDecrypt() # reset base cipher decrypt state + + def encrypt(self, plainText, iv=None, more=None): + """ CBC encryption - overloads baseCipher to allow optional explicit IV + when iv=None, iv is auto generated! + """ + if self.encryptBlockCount == 0: + self.iv = iv + else: + assert(iv==None), 'IV used only on first call to encrypt' + + return BlockCipher.encrypt(self,plainText, more=more) + + def decrypt(self, cipherText, iv=None, more=None): + """ CBC decryption - overloads baseCipher to allow optional explicit IV + when iv=None, iv is auto generated! + """ + if self.decryptBlockCount == 0: + self.iv = iv + else: + assert(iv==None), 'IV used only on first call to decrypt' + + return BlockCipher.decrypt(self, cipherText, more=more) + + def encryptBlock(self, plainTextBlock): + """ CBC block encryption, IV is set with 'encrypt' """ + auto_IV = '' + if self.encryptBlockCount == 0: + if self.iv == None: + # generate IV and use + self.iv = ''.join([chr(self.r.randrange(256)) for i in range(self.blockSize)]) + self.prior_encr_CT_block = self.iv + auto_IV = self.prior_encr_CT_block # prepend IV if it's automatic + else: # application provided IV + assert(len(self.iv) == self.blockSize ),'IV must be same length as block' + self.prior_encr_CT_block = self.iv + """ encrypt the prior CT XORed with the PT """ + ct = self.baseCipher.encryptBlock( xor(self.prior_encr_CT_block, plainTextBlock) ) + self.prior_encr_CT_block = ct + return auto_IV+ct + + def decryptBlock(self, encryptedBlock): + """ Decrypt a single block """ + + if self.decryptBlockCount == 0: # first call, process IV + if self.iv == None: # auto decrypt IV? + self.prior_CT_block = encryptedBlock + return '' + else: + assert(len(self.iv)==self.blockSize),"Bad IV size on CBC decryption" + self.prior_CT_block = self.iv + + dct = self.baseCipher.decryptBlock(encryptedBlock) + """ XOR the prior decrypted CT with the prior CT """ + dct_XOR_priorCT = xor( self.prior_CT_block, dct ) + + self.prior_CT_block = encryptedBlock + + return dct_XOR_priorCT + + + """ + AES_CBC Encryption Algorithm + """ + + class aescbc_AES_CBC(CBC): + """ AES encryption in CBC feedback mode """ + def __init__(self, key=None, padding=padWithPadLen(), keySize=16): + CBC.__init__( self, AES(key, noPadding(), keySize), padding) + self.name = 'AES_CBC' + + class AES_CBC(object): + def __init__(self): + self._key = None + self._iv = None + self.aes = None + + def set_decrypt_key(self, userkey, iv): + self._key = userkey + self._iv = iv + self.aes = aescbc_AES_CBC(userkey, noPadding(), len(userkey)) + + def decrypt(self, data): + iv = self._iv + cleartext = self.aes.decrypt(iv + data) + return cleartext + + import hmac + + class KeyIVGen(object): + # this only exists in openssl so we will use pure python implementation instead + # PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1', + # [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p]) + def pbkdf2(self, passwd, salt, iter, keylen): + + def xorstr( a, b ): + if len(a) != len(b): + raise Exception("xorstr(): lengths differ") + return ''.join((chr(ord(x)^ord(y)) for x, y in zip(a, b))) + + def prf( h, data ): + hm = h.copy() + hm.update( data ) + return hm.digest() + + def pbkdf2_F( h, salt, itercount, blocknum ): + U = prf( h, salt + pack('>i',blocknum ) ) + T = U + for i in range(2, itercount+1): + U = prf( h, U ) + T = xorstr( T, U ) + return T + + sha = hashlib.sha1 + digest_size = sha().digest_size + # l - number of output blocks to produce + l = keylen / digest_size + if keylen % digest_size != 0: + l += 1 + h = hmac.new( passwd, None, sha ) + T = "" + for i in range(1, l+1): + T += pbkdf2_F( h, salt, iter, i ) + return T[0: keylen] + + def UnprotectHeaderData(encryptedData): + passwdData = 'header_key_data' + salt = 'HEADER.2011' + iter = 0x80 + keylen = 0x100 + key_iv = KeyIVGen().pbkdf2(passwdData, salt, iter, keylen) + key = key_iv[0:32] + iv = key_iv[32:48] + aes=AES_CBC() + aes.set_decrypt_key(key, iv) + cleartext = aes.decrypt(encryptedData) + return cleartext + + # Various character maps used to decrypt kindle info values. + # Probably supposed to act as obfuscation + charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_" + charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE" + # New maps in K4PC 1.9.0 + testMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" + testMap6 = "9YzAb0Cd1Ef2n5Pr6St7Uvh3Jk4M8WxG" + testMap8 = "YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD" + + # interface with Windows OS Routines + class DataBlob(Structure): + _fields_ = [('cbData', c_uint), + ('pbData', c_void_p)] + DataBlob_p = POINTER(DataBlob) + + + def GetSystemDirectory(): + GetSystemDirectoryW = kernel32.GetSystemDirectoryW + GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint] + GetSystemDirectoryW.restype = c_uint + def GetSystemDirectory(): + buffer = create_unicode_buffer(MAX_PATH + 1) + GetSystemDirectoryW(buffer, len(buffer)) + return buffer.value + return GetSystemDirectory + GetSystemDirectory = GetSystemDirectory() + + def GetVolumeSerialNumber(): + GetVolumeInformationW = kernel32.GetVolumeInformationW + GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint, + POINTER(c_uint), POINTER(c_uint), + POINTER(c_uint), c_wchar_p, c_uint] + GetVolumeInformationW.restype = c_uint + def GetVolumeSerialNumber(path = GetSystemDirectory().split('\\')[0] + '\\'): + vsn = c_uint(0) + GetVolumeInformationW(path, None, 0, byref(vsn), None, None, None, 0) + return str(vsn.value) + return GetVolumeSerialNumber + GetVolumeSerialNumber = GetVolumeSerialNumber() + + def GetIDString(): + vsn = GetVolumeSerialNumber() + #print('Using Volume Serial Number for ID: '+vsn) + return vsn + + def getLastError(): + GetLastError = kernel32.GetLastError + GetLastError.argtypes = None + GetLastError.restype = c_uint + def getLastError(): + return GetLastError() + return getLastError + getLastError = getLastError() + + def GetUserName(): + GetUserNameW = advapi32.GetUserNameW + GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)] + GetUserNameW.restype = c_uint + def GetUserName(): + buffer = create_unicode_buffer(2) + size = c_uint(len(buffer)) + while not GetUserNameW(buffer, byref(size)): + errcd = getLastError() + if errcd == 234: + # bad wine implementation up through wine 1.3.21 + return "AlternateUserName" + buffer = create_unicode_buffer(len(buffer) * 2) + size.value = len(buffer) + return buffer.value.encode('utf-16-le')[::2] + return GetUserName + GetUserName = GetUserName() + + def CryptUnprotectData(): + _CryptUnprotectData = crypt32.CryptUnprotectData + _CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p, + c_void_p, c_void_p, c_uint, DataBlob_p] + _CryptUnprotectData.restype = c_uint + def CryptUnprotectData(indata, entropy, flags): + indatab = create_string_buffer(indata) + indata = DataBlob(len(indata), cast(indatab, c_void_p)) + entropyb = create_string_buffer(entropy) + entropy = DataBlob(len(entropy), cast(entropyb, c_void_p)) + outdata = DataBlob() + if not _CryptUnprotectData(byref(indata), None, byref(entropy), + None, None, flags, byref(outdata)): + # raise DrmException("Failed to Unprotect Data") + return 'failed' + return string_at(outdata.pbData, outdata.cbData) + return CryptUnprotectData + CryptUnprotectData = CryptUnprotectData() + + # Returns Environmental Variables that contain unicode + def getEnvironmentVariable(name): + import ctypes + name = unicode(name) # make sure string argument is unicode + n = ctypes.windll.kernel32.GetEnvironmentVariableW(name, None, 0) + if n == 0: + return None + buf = ctypes.create_unicode_buffer(u'\0'*n) + ctypes.windll.kernel32.GetEnvironmentVariableW(name, buf, n) + return buf.value + + # Locate all of the kindle-info style files and return as list + def getKindleInfoFiles(): + kInfoFiles = [] + # some 64 bit machines do not have the proper registry key for some reason + # or the python interface to the 32 vs 64 bit registry is broken + path = "" + if 'LOCALAPPDATA' in os.environ.keys(): + # Python 2.x does not return unicode env. Use Python 3.x + path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%") + # this is just another alternative. + # path = getEnvironmentVariable('LOCALAPPDATA') + if not os.path.isdir(path): + path = "" + else: + # User Shell Folders show take precedent over Shell Folders if present + try: + # this will still break + regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\") + path = winreg.QueryValueEx(regkey, 'Local AppData')[0] + if not os.path.isdir(path): + path = "" + try: + regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\") + path = winreg.QueryValueEx(regkey, 'Local AppData')[0] + if not os.path.isdir(path): + path = "" + except RegError: + pass + except RegError: + pass + + found = False + if path == "": + print ('Could not find the folder in which to look for kinfoFiles.') + else: + # Probably not the best. To Fix (shouldn't ignore in encoding) or use utf-8 + print(u'searching for kinfoFiles in ' + path.encode('ascii', 'ignore')) + + # look for (K4PC 1.9.0 and later) .kinf2011 file + kinfopath = path +'\\Amazon\\Kindle\\storage\\.kinf2011' + if os.path.isfile(kinfopath): + found = True + print('Found K4PC 1.9+ kinf2011 file: ' + kinfopath.encode('ascii','ignore')) + kInfoFiles.append(kinfopath) + + # look for (K4PC 1.6.0 and later) rainier.2.1.1.kinf file + kinfopath = path +'\\Amazon\\Kindle\\storage\\rainier.2.1.1.kinf' + if os.path.isfile(kinfopath): + found = True + print('Found K4PC 1.6-1.8 kinf file: ' + kinfopath) + kInfoFiles.append(kinfopath) + + # look for (K4PC 1.5.0 and later) rainier.2.1.1.kinf file + kinfopath = path +'\\Amazon\\Kindle For PC\\storage\\rainier.2.1.1.kinf' + if os.path.isfile(kinfopath): + found = True + print('Found K4PC 1.5 kinf file: ' + kinfopath) + kInfoFiles.append(kinfopath) -letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789' + # look for original (earlier than K4PC 1.5.0) kindle-info files + kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info' + if os.path.isfile(kinfopath): + found = True + print('Found K4PC kindle.info file: ' + kinfopath) + kInfoFiles.append(kinfopath) -def crc32(s): - return (~binascii.crc32(s,-1))&0xFFFFFFFF + if not found: + print('No K4PC kindle.info/kinf/kinf2011 files have been found.') + return kInfoFiles -def checksumPid(s): - crc = crc32(s) - crc = crc ^ (crc >> 16) - res = s - l = len(letters) - for i in (0,1): - b = crc & 0xff - pos = (b // l) ^ (b % l) - res += letters[pos%l] - crc >>= 8 - return res + # determine type of kindle info provided and return a + # database of keynames and values + def getDBfromFile(kInfoFile): + names = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber', 'max_date', 'SIGVERIF'] + DB = {} + with open(kInfoFile, 'rb') as infoReader: + hdr = infoReader.read(1) + data = infoReader.read() -def pidFromSerial(s, l): - crc = crc32(s) + if data.find('{') != -1 : + # older style kindle-info file + items = data.split('{') + for item in items: + if item != '': + keyhash, rawdata = item.split(':') + keyname = "unknown" + for name in names: + if encodeHash(name,charMap2) == keyhash: + keyname = name + break + if keyname == "unknown": + keyname = keyhash + encryptedValue = decode(rawdata,charMap2) + DB[keyname] = CryptUnprotectData(encryptedValue, "", 0) + elif hdr == '/': + # else rainier-2-1-1 .kinf file + # the .kinf file uses "/" to separate it into records + # so remove the trailing "/" to make it easy to use split + data = data[:-1] + items = data.split('/') - arr1 = [0]*l - for i in xrange(len(s)): - arr1[i%l] ^= ord(s[i]) + # loop through the item records until all are processed + while len(items) > 0: - crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff] - for i in xrange(l): - arr1[i] ^= crc_bytes[i&3] + # get the first item record + item = items.pop(0) - pid = '' - for i in xrange(l): - b = arr1[i] & 0xff - pid+=letters[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))] + # the first 32 chars of the first record of a group + # is the MD5 hash of the key name encoded by charMap5 + keyhash = item[0:32] + + # the raw keyhash string is used to create entropy for the actual + # CryptProtectData Blob that represents that keys contents + entropy = SHA1(keyhash) + + # the remainder of the first record when decoded with charMap5 + # has the ':' split char followed by the string representation + # of the number of records that follow + # and make up the contents + srcnt = decode(item[34:],charMap5) + rcnt = int(srcnt) + + # read and store in rcnt records of data + # that make up the contents value + edlst = [] + for i in xrange(rcnt): + item = items.pop(0) + edlst.append(item) + + keyname = "unknown" + for name in names: + if encodeHash(name,charMap5) == keyhash: + keyname = name + break + if keyname == "unknown": + keyname = keyhash + # the charMap5 encoded contents data has had a length + # of chars (always odd) cut off of the front and moved + # to the end to prevent decoding using charMap5 from + # working properly, and thereby preventing the ensuing + # CryptUnprotectData call from succeeding. + + # The offset into the charMap5 encoded contents seems to be: + # len(contents)-largest prime number <= int(len(content)/3) + # (in other words split "about" 2/3rds of the way through) + + # move first offsets chars to end to align for decode by charMap5 + encdata = "".join(edlst) + contlen = len(encdata) + noffset = contlen - primes(int(contlen/3))[-1] + + # now properly split and recombine + # by moving noffset chars from the start of the + # string to the end of the string + pfx = encdata[0:noffset] + encdata = encdata[noffset:] + encdata = encdata + pfx + + # decode using Map5 to get the CryptProtect Data + encryptedValue = decode(encdata,charMap5) + DB[keyname] = CryptUnprotectData(encryptedValue, entropy, 1) + else: + # else newest .kinf2011 style .kinf file + # the .kinf file uses "/" to separate it into records + # so remove the trailing "/" to make it easy to use split + # need to put back the first char read because it it part + # of the added entropy blob + data = hdr + data[:-1] + items = data.split('/') + + # starts with and encoded and encrypted header blob + headerblob = items.pop(0) + encryptedValue = decode(headerblob, testMap1) + cleartext = UnprotectHeaderData(encryptedValue) + # now extract the pieces that form the added entropy + pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) + for m in re.finditer(pattern, cleartext): + added_entropy = m.group(2) + m.group(4) + + + # loop through the item records until all are processed + while len(items) > 0: + + # get the first item record + item = items.pop(0) + + # the first 32 chars of the first record of a group + # is the MD5 hash of the key name encoded by charMap5 + keyhash = item[0:32] + + # the sha1 of raw keyhash string is used to create entropy along + # with the added entropy provided above from the headerblob + entropy = SHA1(keyhash) + added_entropy + + # the remainder of the first record when decoded with charMap5 + # has the ':' split char followed by the string representation + # of the number of records that follow + # and make up the contents + srcnt = decode(item[34:],charMap5) + rcnt = int(srcnt) + + # read and store in rcnt records of data + # that make up the contents value + edlst = [] + for i in xrange(rcnt): + item = items.pop(0) + edlst.append(item) + + # key names now use the new testMap8 encoding + keyname = "unknown" + for name in names: + if encodeHash(name,testMap8) == keyhash: + keyname = name + break + + # the testMap8 encoded contents data has had a length + # of chars (always odd) cut off of the front and moved + # to the end to prevent decoding using testMap8 from + # working properly, and thereby preventing the ensuing + # CryptUnprotectData call from succeeding. + + # The offset into the testMap8 encoded contents seems to be: + # len(contents)-largest prime number <= int(len(content)/3) + # (in other words split "about" 2/3rds of the way through) + + # move first offsets chars to end to align for decode by testMap8 + # by moving noffset chars from the start of the + # string to the end of the string + encdata = "".join(edlst) + contlen = len(encdata) + noffset = contlen - primes(int(contlen/3))[-1] + pfx = encdata[0:noffset] + encdata = encdata[noffset:] + encdata = encdata + pfx + + # decode using new testMap8 to get the original CryptProtect Data + encryptedValue = decode(encdata,testMap8) + cleartext = CryptUnprotectData(encryptedValue, entropy, 1) + DB[keyname] = cleartext + + if 'kindle.account.tokens' in DB: + print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(GetIDString(), GetUserName().decode("latin-1")) + # store values used in decryption + DB['IDString'] = GetIDString() + DB['UserName'] = GetUserName() + else: + DB = {} + return DB +elif isosx: + import copy + import subprocess + + # interface to needed routines in openssl's libcrypto + def _load_crypto_libcrypto(): + from ctypes import CDLL, byref, POINTER, c_void_p, c_char_p, c_int, c_long, \ + Structure, c_ulong, create_string_buffer, addressof, string_at, cast + from ctypes.util import find_library + + libcrypto = find_library('crypto') + if libcrypto is None: + raise DrmException(u"libcrypto not found") + libcrypto = CDLL(libcrypto) + + # From OpenSSL's crypto aes header + # + # AES_ENCRYPT 1 + # AES_DECRYPT 0 + # AES_MAXNR 14 (in bytes) + # AES_BLOCK_SIZE 16 (in bytes) + # + # struct aes_key_st { + # unsigned long rd_key[4 *(AES_MAXNR + 1)]; + # int rounds; + # }; + # typedef struct aes_key_st AES_KEY; + # + # int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key); + # + # note: the ivec string, and output buffer are both mutable + # void AES_cbc_encrypt(const unsigned char *in, unsigned char *out, + # const unsigned long length, const AES_KEY *key, unsigned char *ivec, const int enc); + + AES_MAXNR = 14 + c_char_pp = POINTER(c_char_p) + c_int_p = POINTER(c_int) + + class AES_KEY(Structure): + _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)] + AES_KEY_p = POINTER(AES_KEY) + + def F(restype, name, argtypes): + func = getattr(libcrypto, name) + func.restype = restype + func.argtypes = argtypes + return func + + AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,c_int]) + + AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',[c_char_p, c_int, AES_KEY_p]) + + # From OpenSSL's Crypto evp/p5_crpt2.c + # + # int PKCS5_PBKDF2_HMAC_SHA1(const char *pass, int passlen, + # const unsigned char *salt, int saltlen, int iter, + # int keylen, unsigned char *out); + + PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1', + [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p]) + + class LibCrypto(object): + def __init__(self): + self._blocksize = 0 + self._keyctx = None + self._iv = 0 + + def set_decrypt_key(self, userkey, iv): + self._blocksize = len(userkey) + if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : + raise DrmException(u"AES improper key used") + return + keyctx = self._keyctx = AES_KEY() + self._iv = iv + self._userkey = userkey + rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx) + if rv < 0: + raise DrmException(u"Failed to initialize AES key") + + def decrypt(self, data): + out = create_string_buffer(len(data)) + mutable_iv = create_string_buffer(self._iv, len(self._iv)) + keyctx = self._keyctx + rv = AES_cbc_encrypt(data, out, len(data), keyctx, mutable_iv, 0) + if rv == 0: + raise DrmException(u"AES decryption failed") + return out.raw + + def keyivgen(self, passwd, salt, iter, keylen): + saltlen = len(salt) + passlen = len(passwd) + out = create_string_buffer(keylen) + rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out) + return out.raw + return LibCrypto + + def _load_crypto(): + LibCrypto = None + try: + LibCrypto = _load_crypto_libcrypto() + except (ImportError, DrmException): + pass + return LibCrypto + + LibCrypto = _load_crypto() + + # Various character maps used to decrypt books. Probably supposed to act as obfuscation + charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M' + charMap2 = 'ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM' + + # For kinf approach of K4Mac 1.6.X or later + # On K4PC charMap5 = 'AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE' + # For Mac they seem to re-use charMap2 here + charMap5 = charMap2 + + # new in K4M 1.9.X + testMap8 = 'YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD' + + # uses a sub process to get the Hard Drive Serial Number using ioreg + # returns serial numbers of all internal hard drive drives + def GetVolumesSerialNumbers(): + sernums = [] + sernum = os.getenv('MYSERIALNUMBER') + if sernum != None: + sernums.append(sernum.strip()) + cmdline = '/usr/sbin/ioreg -w 0 -r -c AppleAHCIDiskDriver' + cmdline = cmdline.encode(sys.getfilesystemencoding()) + p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) + out1, out2 = p.communicate() + reslst = out1.split('\n') + cnt = len(reslst) + bsdname = None + sernum = None + foundIt = False + for j in xrange(cnt): + resline = reslst[j] + pp = resline.find('\"Serial Number\" = \"') + if pp >= 0: + sernum = resline[pp+19:-1] + sernums.append(sernum.strip()) + return sernums + + def GetUserHomeAppSupKindleDirParitionName(): + home = os.getenv('HOME') + dpath = home + '/Library' + cmdline = '/sbin/mount' + cmdline = cmdline.encode(sys.getfilesystemencoding()) + p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) + out1, out2 = p.communicate() + reslst = out1.split('\n') + cnt = len(reslst) + disk = '' + foundIt = False + for j in xrange(cnt): + resline = reslst[j] + if resline.startswith('/dev'): + (devpart, mpath) = resline.split(' on ') + dpart = devpart[5:] + pp = mpath.find('(') + if pp >= 0: + mpath = mpath[:pp-1] + if dpath.startswith(mpath): + disk = dpart + return disk + + # uses a sub process to get the UUID of the specified disk partition using ioreg + def GetDiskPartitionUUIDs(diskpart): + uuids = [] + uuidnum = os.getenv('MYUUIDNUMBER') + if uuidnum != None: + uuids.append(strip(uuidnum)) + cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver' + cmdline = cmdline.encode(sys.getfilesystemencoding()) + p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) + out1, out2 = p.communicate() + reslst = out1.split('\n') + cnt = len(reslst) + bsdname = None + uuidnum = None + foundIt = False + nest = 0 + uuidnest = -1 + partnest = -2 + for j in xrange(cnt): + resline = reslst[j] + if resline.find('{') >= 0: + nest += 1 + if resline.find('}') >= 0: + nest -= 1 + pp = resline.find('\"UUID\" = \"') + if pp >= 0: + uuidnum = resline[pp+10:-1] + uuidnum = uuidnum.strip() + uuidnest = nest + if partnest == uuidnest and uuidnest > 0: + foundIt = True + break + bb = resline.find('\"BSD Name\" = \"') + if bb >= 0: + bsdname = resline[bb+14:-1] + bsdname = bsdname.strip() + if (bsdname == diskpart): + partnest = nest + else : + partnest = -2 + if partnest == uuidnest and partnest > 0: + foundIt = True + break + if nest == 0: + partnest = -2 + uuidnest = -1 + uuidnum = None + bsdname = None + if foundIt: + uuids.append(uuidnum) + return uuids + + def GetMACAddressesMunged(): + macnums = [] + macnum = os.getenv('MYMACNUM') + if macnum != None: + macnums.append(macnum) + cmdline = '/sbin/ifconfig en0' + cmdline = cmdline.encode(sys.getfilesystemencoding()) + p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) + out1, out2 = p.communicate() + reslst = out1.split('\n') + cnt = len(reslst) + macnum = None + foundIt = False + for j in xrange(cnt): + resline = reslst[j] + pp = resline.find('ether ') + if pp >= 0: + macnum = resline[pp+6:-1] + macnum = macnum.strip() + # print 'original mac', macnum + # now munge it up the way Kindle app does + # by xoring it with 0xa5 and swapping elements 3 and 4 + maclst = macnum.split(':') + n = len(maclst) + if n != 6: + fountIt = False + break + for i in range(6): + maclst[i] = int('0x' + maclst[i], 0) + mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + mlst[5] = maclst[5] ^ 0xa5 + mlst[4] = maclst[3] ^ 0xa5 + mlst[3] = maclst[4] ^ 0xa5 + mlst[2] = maclst[2] ^ 0xa5 + mlst[1] = maclst[1] ^ 0xa5 + mlst[0] = maclst[0] ^ 0xa5 + macnum = '%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x' % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5]) + foundIt = True + break + if foundIt: + macnums.append(macnum) + return macnums + + + # uses unix env to get username instead of using sysctlbyname + def GetUserName(): + username = os.getenv('USER') + return username + + def GetIDStrings(): + # Return all possible ID Strings + strings = [] + strings.extend(GetMACAddressesMunged()) + strings.extend(GetVolumesSerialNumbers()) + diskpart = GetUserHomeAppSupKindleDirParitionName() + strings.extend(GetDiskPartitionUUIDs(diskpart)) + strings.append('9999999999') + #print strings + return strings + + + # implements an Pseudo Mac Version of Windows built-in Crypto routine + # used by Kindle for Mac versions < 1.6.0 + class CryptUnprotectData(object): + def __init__(self, IDString): + sp = IDString + '!@#' + GetUserName() + passwdData = encode(SHA256(sp),charMap1) + salt = '16743' + self.crp = LibCrypto() + iter = 0x3e8 + keylen = 0x80 + key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen) + self.key = key_iv[0:32] + self.iv = key_iv[32:48] + self.crp.set_decrypt_key(self.key, self.iv) + + def decrypt(self, encryptedData): + cleartext = self.crp.decrypt(encryptedData) + cleartext = decode(cleartext,charMap1) + return cleartext + + + # implements an Pseudo Mac Version of Windows built-in Crypto routine + # used for Kindle for Mac Versions >= 1.6.0 + class CryptUnprotectDataV2(object): + def __init__(self, IDString): + sp = GetUserName() + ':&%:' + IDString + passwdData = encode(SHA256(sp),charMap5) + # salt generation as per the code + salt = 0x0512981d * 2 * 1 * 1 + salt = str(salt) + GetUserName() + salt = encode(salt,charMap5) + self.crp = LibCrypto() + iter = 0x800 + keylen = 0x400 + key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen) + self.key = key_iv[0:32] + self.iv = key_iv[32:48] + self.crp.set_decrypt_key(self.key, self.iv) + + def decrypt(self, encryptedData): + cleartext = self.crp.decrypt(encryptedData) + cleartext = decode(cleartext, charMap5) + return cleartext + + + # unprotect the new header blob in .kinf2011 + # used in Kindle for Mac Version >= 1.9.0 + def UnprotectHeaderData(encryptedData): + passwdData = 'header_key_data' + salt = 'HEADER.2011' + iter = 0x80 + keylen = 0x100 + crp = LibCrypto() + key_iv = crp.keyivgen(passwdData, salt, iter, keylen) + key = key_iv[0:32] + iv = key_iv[32:48] + crp.set_decrypt_key(key,iv) + cleartext = crp.decrypt(encryptedData) + return cleartext + + + # implements an Pseudo Mac Version of Windows built-in Crypto routine + # used for Kindle for Mac Versions >= 1.9.0 + class CryptUnprotectDataV3(object): + def __init__(self, entropy, IDString): + sp = GetUserName() + '+@#$%+' + IDString + passwdData = encode(SHA256(sp),charMap2) + salt = entropy + self.crp = LibCrypto() + iter = 0x800 + keylen = 0x400 + key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen) + self.key = key_iv[0:32] + self.iv = key_iv[32:48] + self.crp.set_decrypt_key(self.key, self.iv) + + def decrypt(self, encryptedData): + cleartext = self.crp.decrypt(encryptedData) + cleartext = decode(cleartext, charMap2) + return cleartext + + + # Locate the .kindle-info files + def getKindleInfoFiles(): + # file searches can take a long time on some systems, so just look in known specific places. + kInfoFiles=[] + found = False + home = os.getenv('HOME') + # check for .kinf2011 file in new location (App Store Kindle for Mac) + testpath = home + '/Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/storage/.kinf2011' + if os.path.isfile(testpath): + kInfoFiles.append(testpath) + print('Found k4Mac kinf2011 file: ' + testpath) + found = True + # check for .kinf2011 files from 1.10 + testpath = home + '/Library/Application Support/Kindle/storage/.kinf2011' + if os.path.isfile(testpath): + kInfoFiles.append(testpath) + print('Found k4Mac kinf2011 file: ' + testpath) + found = True + # check for .rainier-2.1.1-kinf files from 1.6 + testpath = home + '/Library/Application Support/Kindle/storage/.rainier-2.1.1-kinf' + if os.path.isfile(testpath): + kInfoFiles.append(testpath) + print('Found k4Mac rainier file: ' + testpath) + found = True + # check for .kindle-info files from 1.4 + testpath = home + '/Library/Application Support/Kindle/storage/.kindle-info' + if os.path.isfile(testpath): + kInfoFiles.append(testpath) + print('Found k4Mac kindle-info file: ' + testpath) + found = True + # check for .kindle-info file from 1.2.2 + testpath = home + '/Library/Application Support/Amazon/Kindle/storage/.kindle-info' + if os.path.isfile(testpath): + kInfoFiles.append(testpath) + print('Found k4Mac kindle-info file: ' + testpath) + found = True + # check for .kindle-info file from 1.0 beta 1 (27214) + testpath = home + '/Library/Application Support/Amazon/Kindle for Mac/storage/.kindle-info' + if os.path.isfile(testpath): + kInfoFiles.append(testpath) + print('Found k4Mac kindle-info file: ' + testpath) + found = True + if not found: + print('No k4Mac kindle-info/rainier/kinf2011 files have been found.') + return kInfoFiles + + # determine type of kindle info provided and return a + # database of keynames and values + def getDBfromFile(kInfoFile): + names = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber', 'max_date', 'SIGVERIF'] + with open(kInfoFile, 'rb') as infoReader: + filehdr = infoReader.read(1) + filedata = infoReader.read() + + IDStrings = GetIDStrings() + for IDString in IDStrings: + DB = {} + #print "trying IDString:",IDString + try: + hdr = filehdr + data = filedata + if data.find('[') != -1 : + # older style kindle-info file + cud = CryptUnprotectData(IDString) + items = data.split('[') + for item in items: + if item != '': + keyhash, rawdata = item.split(':') + keyname = 'unknown' + for name in names: + if encodeHash(name,charMap2) == keyhash: + keyname = name + break + if keyname == 'unknown': + keyname = keyhash + encryptedValue = decode(rawdata,charMap2) + cleartext = cud.decrypt(encryptedValue) + if len(cleartext) > 0: + DB[keyname] = cleartext + if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB: + break + elif hdr == '/': + # else newer style .kinf file used by K4Mac >= 1.6.0 + # the .kinf file uses '/' to separate it into records + # so remove the trailing '/' to make it easy to use split + data = data[:-1] + items = data.split('/') + cud = CryptUnprotectDataV2(IDString) + + # loop through the item records until all are processed + while len(items) > 0: + + # get the first item record + item = items.pop(0) + + # the first 32 chars of the first record of a group + # is the MD5 hash of the key name encoded by charMap5 + keyhash = item[0:32] + keyname = 'unknown' + + # the raw keyhash string is also used to create entropy for the actual + # CryptProtectData Blob that represents that keys contents + # 'entropy' not used for K4Mac only K4PC + # entropy = SHA1(keyhash) + + # the remainder of the first record when decoded with charMap5 + # has the ':' split char followed by the string representation + # of the number of records that follow + # and make up the contents + srcnt = decode(item[34:],charMap5) + rcnt = int(srcnt) + + # read and store in rcnt records of data + # that make up the contents value + edlst = [] + for i in xrange(rcnt): + item = items.pop(0) + edlst.append(item) + + keyname = 'unknown' + for name in names: + if encodeHash(name,charMap5) == keyhash: + keyname = name + break + if keyname == 'unknown': + keyname = keyhash + + # the charMap5 encoded contents data has had a length + # of chars (always odd) cut off of the front and moved + # to the end to prevent decoding using charMap5 from + # working properly, and thereby preventing the ensuing + # CryptUnprotectData call from succeeding. + + # The offset into the charMap5 encoded contents seems to be: + # len(contents) - largest prime number less than or equal to int(len(content)/3) + # (in other words split 'about' 2/3rds of the way through) + + # move first offsets chars to end to align for decode by charMap5 + encdata = ''.join(edlst) + contlen = len(encdata) + + # now properly split and recombine + # by moving noffset chars from the start of the + # string to the end of the string + noffset = contlen - primes(int(contlen/3))[-1] + pfx = encdata[0:noffset] + encdata = encdata[noffset:] + encdata = encdata + pfx + + # decode using charMap5 to get the CryptProtect Data + encryptedValue = decode(encdata,charMap5) + cleartext = cud.decrypt(encryptedValue) + if len(cleartext) > 0: + DB[keyname] = cleartext + + if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB: + break + else: + # the latest .kinf2011 version for K4M 1.9.1 + # put back the hdr char, it is needed + data = hdr + data + data = data[:-1] + items = data.split('/') + + # the headerblob is the encrypted information needed to build the entropy string + headerblob = items.pop(0) + encryptedValue = decode(headerblob, charMap1) + cleartext = UnprotectHeaderData(encryptedValue) + + # now extract the pieces in the same way + # this version is different from K4PC it scales the build number by multipying by 735 + pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) + for m in re.finditer(pattern, cleartext): + entropy = str(int(m.group(2)) * 0x2df) + m.group(4) + + cud = CryptUnprotectDataV3(entropy,IDString) + + # loop through the item records until all are processed + while len(items) > 0: + + # get the first item record + item = items.pop(0) + + # the first 32 chars of the first record of a group + # is the MD5 hash of the key name encoded by charMap5 + keyhash = item[0:32] + keyname = 'unknown' + + # unlike K4PC the keyhash is not used in generating entropy + # entropy = SHA1(keyhash) + added_entropy + # entropy = added_entropy + + # the remainder of the first record when decoded with charMap5 + # has the ':' split char followed by the string representation + # of the number of records that follow + # and make up the contents + srcnt = decode(item[34:],charMap5) + rcnt = int(srcnt) + + # read and store in rcnt records of data + # that make up the contents value + edlst = [] + for i in xrange(rcnt): + item = items.pop(0) + edlst.append(item) + + keyname = 'unknown' + for name in names: + if encodeHash(name,testMap8) == keyhash: + keyname = name + break + if keyname == 'unknown': + keyname = keyhash + + # the testMap8 encoded contents data has had a length + # of chars (always odd) cut off of the front and moved + # to the end to prevent decoding using testMap8 from + # working properly, and thereby preventing the ensuing + # CryptUnprotectData call from succeeding. + + # The offset into the testMap8 encoded contents seems to be: + # len(contents) - largest prime number less than or equal to int(len(content)/3) + # (in other words split 'about' 2/3rds of the way through) + + # move first offsets chars to end to align for decode by testMap8 + encdata = ''.join(edlst) + contlen = len(encdata) + + # now properly split and recombine + # by moving noffset chars from the start of the + # string to the end of the string + noffset = contlen - primes(int(contlen/3))[-1] + pfx = encdata[0:noffset] + encdata = encdata[noffset:] + encdata = encdata + pfx + + # decode using testMap8 to get the CryptProtect Data + encryptedValue = decode(encdata,testMap8) + cleartext = cud.decrypt(encryptedValue) + # print keyname + # print cleartext + if len(cleartext) > 0: + DB[keyname] = cleartext + + if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB: + break + except: + pass + if 'kindle.account.tokens' in DB: + # store values used in decryption + print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString, GetUserName()) + DB['IDString'] = IDString + DB['UserName'] = GetUserName() + else: + print u"Couldn't decrypt file." + DB = {} + return DB +else: + def getDBfromFile(kInfoFile): + raise DrmException(u"This script only runs under Windows or Mac OS X.") + return {} + +def kindlekeys(files = []): + keys = [] + if files == []: + files = getKindleInfoFiles() + for file in files: + key = getDBfromFile(file) + if key: + # convert all values to hex, just in case. + for keyname in key: + key[keyname]=key[keyname].encode('hex') + keys.append(key) + return keys + +# interface for Python DeDRM +# returns single key or multiple keys, depending on path or file passed in +def getkey(outpath, files=[]): + keys = kindlekeys(files) + if len(keys) > 0: + if not os.path.isdir(outpath): + outfile = outpath + with file(outfile, 'w') as keyfileout: + keyfileout.write(json.dumps(keys[0])) + print u"Saved a key to {0}".format(outfile) + else: + keycount = 0 + for key in keys: + while True: + keycount += 1 + outfile = os.path.join(outpath,u"kindlekey{0:d}.k4i".format(keycount)) + if not os.path.exists(outfile): + break + with file(outfile, 'w') as keyfileout: + keyfileout.write(json.dumps(key)) + print u"Saved a key to {0}".format(outfile) + return True + return False + +def usage(progname): + print u"Finds, decrypts and saves the default Kindle For Mac/PC encryption keys." + print u"Keys are saved to the current directory, or a specified output directory." + print u"If a file name is passed instead of a directory, only the first key is saved, in that file." + print u"Usage:" + print u" {0:s} [-h] [-k ] []".format(progname) - return pid def cli_main(): - print u"Mobipocket PID calculator for Amazon Kindle. Copyright © 2007, 2009 Igor Skochinsky" + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) argv=unicode_argv() - if len(argv)==2: - serial = argv[1] + progname = os.path.basename(argv[0]) + print u"{0} v{1}\nCopyright © 2010-2013 some_updates and Apprentice Alf".format(progname,__version__) + + try: + opts, args = getopt.getopt(argv[1:], "hk:") + except getopt.GetoptError, err: + print u"Error in options or arguments: {0}".format(err.args[0]) + usage(progname) + sys.exit(2) + + files = [] + for o, a in opts: + if o == "-h": + usage(progname) + sys.exit(0) + if o == "-k": + files = [a] + + if len(args) > 1: + usage(progname) + sys.exit(2) + + if len(args) == 1: + # save to the specified file or directory + outpath = args[0] + if not os.path.isabs(outpath): + outpath = os.path.abspath(outpath) else: - print u"Usage: kindlepid.py /" + # save to the same directory as the script + outpath = os.path.dirname(argv[0]) + + # make sure the outpath is the + outpath = os.path.realpath(os.path.normpath(outpath)) + + if not getkey(outpath, files): + print u"Could not retrieve Kindle for Mac/PC key." + return 0 + + +def gui_main(): + try: + import Tkinter + import Tkconstants + import tkMessageBox + import traceback + except: + return cli_main() + + class ExceptionDialog(Tkinter.Frame): + def __init__(self, root, text): + Tkinter.Frame.__init__(self, root, border=5) + label = Tkinter.Label(self, text=u"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) + + + argv=unicode_argv() + root = Tkinter.Tk() + root.withdraw() + progpath, progname = os.path.split(argv[0]) + success = False + try: + keys = kindlekeys() + keycount = 0 + for key in keys: + while True: + keycount += 1 + outfile = os.path.join(progpath,u"kindlekey{0:d}.k4i".format(keycount)) + if not os.path.exists(outfile): + break + + with file(outfile, 'w') as keyfileout: + keyfileout.write(json.dumps(key)) + success = True + tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile)) + except DrmException, e: + tkMessageBox.showerror(progname, u"Error: {0}".format(str(e))) + except Exception: + root.wm_state('normal') + root.title(progname) + text = traceback.format_exc() + ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1) + root.mainloop() + if not success: return 1 - if len(serial)==16: - if serial.startswith("B") or serial.startswith("9"): - print u"Kindle serial number detected" - else: - print u"Warning: unrecognized serial number. Please recheck input." - return 1 - pid = pidFromSerial(serial.encode("utf-8"),7)+'*' - print u"Mobipocket PID for Kindle serial#{0} is {1}".format(serial,checksumPid(pid)) - return 0 - elif len(serial)==40: - print u"iPhone serial number (UDID) detected" - pid = pidFromSerial(serial.encode("utf-8"),8) - print u"Mobipocket PID for iPhone serial#{0} is {1}".format(serial,checksumPid(pid)) - return 0 - print u"Warning: unrecognized serial number. Please recheck input." - return 1 - - -if __name__ == "__main__": - sys.stdout=SafeUnbuffered(sys.stdout) - sys.stderr=SafeUnbuffered(sys.stderr) - sys.exit(cli_main()) + return 0 + +if __name__ == '__main__': + if len(sys.argv) > 1: + sys.exit(cli_main()) + sys.exit(gui_main()) diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/kindlepid.py b/DeDRM_calibre_plugin/DeDRM_plugin/kindlepid.py index 01c348cc8a638e243754aea2cd2681ad30e629a4..8bbcf691bb2802ac73050af51c79d880181dbc85 100644 GIT binary patch literal 4738 zcmbtY+j84R5`E`a^imZQuqlY7WJ`9L+9+0>Xk&Z1mK5!-N@0P(kenrM1q>uIspNn1 z8~bH@dS-B;M5neYc#t4WPxtB5w;ApI<)AF`gPBYYvW;4%NoSAtpYKzUhB8^ap;FEF zPxwm*3m?;&%+g>b6iqJ9De$A9jC_^mbU*NpXpWb&*#9|A=m(jEk?^qRU7BrjxmYT4 z0+&XE!Ev8|zNU)>#4lHAu#`!$+R)pge*Zf5W#r}MDfWLa3mDkE!7B~CAzdU=Nk5`o zM8YqG`8=YtFccvbWtOG6(nuB}5xKA6g$B#ujFyf z*)LT(kIG_6g$mPB^=U5GSjvSe^MuI}R2Kfinm?p63w;Fvwo)dN1&rFG9mooivj<5T z&qQ9pgr94e4^l0u_qtbEvx;a|E()d+{=cL+y@wsUH1+evJ;xy;kSKu)%q1Gd4I+>} z6m%}mKYs+`I0F|m8A-M2bY!e)DmF#+iUjbBKuQktP3~vp16MBmn2MCZmm<)Rm5LTZ z6{NWV3>n8Fl<+*-V4t6aTPR^b zf(xWX5(tME`!>`qA-2Iw0Ls4%Orx-d+^@-WYxFG&JcB`7-?^?M?3lfVUPR1SDz6#l zKU$z{X;>-kou#`^S`=AVJlLcJ!<)iGuodDQLk<;yMYVK#5b3!PO^Y=L;!9$opBu>z zqidIgV?k*ut-&Tfdvsk0)Z9`;(c#GZPN;Wj93yi-$V8l>nr^uM;e|G>%gZlN3=!H8 zrE|Kr#mzC`XRpd=uT%55M?a#jWttQ;^47d-qFXD4Y0`sY+oY^2RBmGn{nzMa%{PXv zKIz7QB7mQv$fFG>sa>SA$pzQG5l)bP$K|~@2XoKzWX1Cdca^f%Ey&si=PHE886r0+k&w@u&mFi+Gf>QajNa(Q;7*j~B zIc4xnB&z0)x9bi7s4Wxw6p6&)2EcF|AS`a1%`LCo$~!&qB+6h*Ckyhk&Uvmo+5~!0 z?L>?IJ;wDQY=JI)_`yR9vSkdYXaEY64XOX15^W&kIW+Tw6%|1)GewCXx9dzTC3l## zj6zDpy~t_lXPIcwRq)I1ifc$@GW4=d*QA4piqE5(^DG0GFgv$fq0?iBD`^3SU-$_KHGu{EKePd4)Wf-prlzgir~rgb!$?l~Zcv?$OsSRKFJI{x^7d`6b(j0^?|*QW zGbdec3ghjAnMQ_2z(M6@cRU_7W5iyI^boke9q#Ie73yogd%dA{_oTPx&PgWm=##!( zogTb3m%alzNkh6y%V0^@=NIRv24t)04cq7p7T_DcY5;Nz(^=g9*9ef5;&ZN4Y2J=&{lBc=LwE^xSl7u2+~C&f5t6pOJCg?A;2vN z?E%*}uhIU*b!|W=?V}PlKITcwA{s)SjrfL8H literal 87160 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 diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/libalfcrypto.dylib b/DeDRM_calibre_plugin/DeDRM_plugin/libalfcrypto.dylib index 9a5a442617a2046fb9b7050d339b18bca8993e7b..01c348cc8a638e243754aea2cd2681ad30e629a4 100644 GIT binary patch literal 87160 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 delta 11439 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 N%?V7taWo%w{x1d9X7vC7 diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/libalfcrypto32.so b/DeDRM_calibre_plugin/DeDRM_plugin/libalfcrypto32.so index a08ac28930fdd54e0eb4e8973a398e5e951d3825..9a5a442617a2046fb9b7050d339b18bca8993e7b 100644 GIT binary patch literal 23859 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 literal 33417 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 Ko^Mvd(mwtG diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/libalfcrypto64.so b/DeDRM_calibre_plugin/DeDRM_plugin/libalfcrypto64.so index 7b69edcf4d72a59bb1c2a9b779ffe0eab75f2c1e..a08ac28930fdd54e0eb4e8973a398e5e951d3825 100644 GIT binary patch literal 33417 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 Ko^Mvd(mwtG literal 21895 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 0xE4. +# 0.15 - Now outputs 'heartbeat', and is also quicker for long files. +# 0.16 - And reverts to 'done' not 'done.' at the end for unswindle compatibility. +# 0.17 - added modifications to support its use as an imported python module +# both inside calibre and also in other places (ie K4DeDRM tools) +# 0.17a- disabled the standalone plugin feature since a plugin can not import +# a plugin +# 0.18 - It seems that multibyte entries aren't encrypted in a v7 file... +# 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 - 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 +# 0.23 - fixed problem with older files with no EXTH section +# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well +# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption +# 0.26 - Now enables Text-To-Speech flag and sets clipping limit to 100% +# 0.27 - Correct pid metadata token generation to match that used by skindle (Thank You Bart!) +# 0.28 - slight additional changes to metadata token generation (None -> '') +# 0.29 - It seems that the ideas about when multibyte trailing characters were +# included in the encryption were wrong. They are for DOC compressed +# files, but they are not for HUFF/CDIC compress files! +# 0.30 - Modified interface slightly to work better with new calibre plugin style +# 0.31 - The multibyte encrytion info is true for version 7 files too. +# 0.32 - Added support for "Print Replica" Kindle ebooks +# 0.33 - Performance improvements for large files (concatenation) +# 0.34 - Performance improvements in decryption (libalfcrypto) +# 0.35 - add interface to get mobi_version +# 0.36 - fixed problem with TEXtREAd and getBookTitle interface +# 0.37 - Fixed double announcement for stand-alone operation +# 0.38 - Unicode used wherever possible, cope with absent alfcrypto +# 0.39 - Fixed problem with TEXtREAd and getBookType interface +# 0.40 - moved unicode_argv call inside main for Windows DeDRM compatibility +# 0.41 - Fixed potential unicode problem in command line calls - if sys.platform.startswith('win'): - libcrypto = find_library('libeay32') + +__version__ = u"0.41" + +import sys +import os +import struct +import binascii +try: + from alfcrypto import Pukall_Cipher +except: + print u"AlfCrypto not found. Using python PC1 implementation." + +# Wrap a stream so that output gets flushed immediately +# and also make sure that any unicode strings get +# encoded using "replace" before writing them. +class SafeUnbuffered: + def __init__(self, stream): + self.stream = stream + self.encoding = stream.encoding + if self.encoding == None: + self.encoding = "utf-8" + def write(self, data): + if isinstance(data,unicode): + data = data.encode(self.encoding,"replace") + self.stream.write(data) + self.stream.flush() + def __getattr__(self, attr): + return getattr(self.stream, attr) + +iswindows = sys.platform.startswith('win') +isosx = sys.platform.startswith('darwin') + +def unicode_argv(): + if iswindows: + # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode + # strings. + + # Versions 2.x of Python don't support Unicode in sys.argv on + # Windows, with the underlying Windows API instead replacing multi-byte + # characters with '?'. + + + from ctypes import POINTER, byref, cdll, c_int, windll + from ctypes.wintypes import LPCWSTR, LPWSTR + + GetCommandLineW = cdll.kernel32.GetCommandLineW + GetCommandLineW.argtypes = [] + GetCommandLineW.restype = LPCWSTR + + CommandLineToArgvW = windll.shell32.CommandLineToArgvW + CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] + CommandLineToArgvW.restype = POINTER(LPWSTR) + + cmd = GetCommandLineW() + argc = c_int(0) + argv = CommandLineToArgvW(cmd, byref(argc)) + if argc.value > 0: + # Remove Python executable and commands if present + start = argc.value - len(sys.argv) + return [argv[i] for i in + xrange(start, argc.value)] + # if we don't have any arguments at all, just pass back script name + # this should never happen + return [u"mobidedrm.py"] + else: + argvencoding = sys.stdin.encoding + if argvencoding == None: + argvencoding = 'utf-8' + return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv] + + +class DrmException(Exception): + pass + + +# +# MobiBook Utility Routines +# + +# Implementation of Pukall Cipher 1 +def PC1(key, src, decryption=True): + # if we can get it from alfcrypto, use that + try: + return Pukall_Cipher().PC1(key,src,decryption) + except NameError: + pass + except TypeError: + pass + + # use slow python version, since Pukall_Cipher didn't load + sum1 = 0; + sum2 = 0; + keyXorVal = 0; + if len(key)!=16: + DrmException (u"PC1: Bad key length") + wkey = [] + for i in xrange(8): + wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1])) + dst = "" + for i in xrange(len(src)): + temp1 = 0; + byteXorVal = 0; + for j in xrange(8): + temp1 ^= wkey[j] + sum2 = (sum2+j)*20021 + sum1 + sum1 = (temp1*346)&0xFFFF + sum2 = (sum2+sum1)&0xFFFF + temp1 = (temp1*20021+1)&0xFFFF + byteXorVal ^= temp1 ^ sum2 + curByte = ord(src[i]) + if not decryption: + keyXorVal = curByte * 257; + curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF + if decryption: + keyXorVal = curByte * 257; + for j in xrange(8): + wkey[j] ^= keyXorVal; + dst+=chr(curByte) + return dst + +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 + +def getSizeOfTrailingDataEntries(ptr, size, flags): + def getSizeOfTrailingDataEntry(ptr, size): + bitpos, result = 0, 0 + if size <= 0: + return result + while True: + v = ord(ptr[size-1]) + result |= (v & 0x7F) << bitpos + bitpos += 7 + size -= 1 + if (v & 0x80) != 0 or (bitpos >= 28) or (size == 0): + return result + num = 0 + testflags = flags >> 1 + while testflags: + if testflags & 1: + num += getSizeOfTrailingDataEntry(ptr, size - num) + testflags >>= 1 + # Check the low bit to see if there's multibyte data present. + # if multibyte data is included in the encryped data, we'll + # have already cleared this flag. + if flags & 1: + num += (ord(ptr[size - num - 1]) & 0x3) + 1 + return num + + + +class MobiBook: + def loadSection(self, section): + if (section + 1 == self.num_sections): + endoff = len(self.data_file) + else: + endoff = self.sections[section + 1][0] + off = self.sections[section][0] + return self.data_file[off:endoff] + + def cleanup(self): + # to match function in Topaz book + pass + + def __init__(self, infile): + print u"MobiDeDrm v{0:s}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__) + + try: + from alfcrypto import Pukall_Cipher + except: + print u"AlfCrypto not found. Using python PC1 implementation." + + # initial sanity check on file + self.data_file = file(infile, 'rb').read() + self.mobi_data = '' + self.header = self.data_file[0:78] + if self.header[0x3C:0x3C+8] != 'BOOKMOBI' and self.header[0x3C:0x3C+8] != 'TEXtREAd': + raise DrmException(u"Invalid file format") + self.magic = self.header[0x3C:0x3C+8] + self.crypto_type = -1 + + # 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.compression, = struct.unpack('>H', self.sect[0x0:0x0+2]) + + # det default values before PalmDoc test + self.print_replica = False + self.extra_data_flags = 0 + self.meta_array = {} + self.mobi_length = 0 + self.mobi_codepage = 1252 + self.mobi_version = -1 + + if self.magic == 'TEXtREAd': + print u"PalmDoc format book detected." + return + + self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18]) + self.mobi_codepage, = struct.unpack('>L',self.sect[0x1c:0x20]) + self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C]) + print u"MOBI header version {0:d}, header length {1:d}".format(self.mobi_version, self.mobi_length) + if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5): + self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4]) + print u"Extra Data Flags: {0:d}".format(self.extra_data_flags) + if (self.compression != 17480): + # multibyte utf8 data is included in the encryption for PalmDoc compression + # 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 + try: + exth_flag, = struct.unpack('>L', self.sect[0x80:0x84]) + exth = '' + if exth_flag & 0x40: + exth = self.sect[16 + self.mobi_length:] + if (len(exth) >= 12) and (exth[:4] == 'EXTH'): + 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 + # reset the text to speech flag and clipping limit, if present + if type == 401 and size == 9: + # set clipping limit to 100% + self.patchSection(0, '\144', 16 + self.mobi_length + pos + 8) + elif type == 404 and size == 9: + # make sure text to speech is enabled + self.patchSection(0, '\0', 16 + self.mobi_length + pos + 8) + # print type, size, content, content.encode('hex') + pos += size + except: + pass + + def getBookTitle(self): + codec_map = { + 1252 : 'windows-1252', + 65001 : 'utf-8', + } + title = '' + codec = 'windows-1252' + if self.magic == 'BOOKMOBI': + 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 self.mobi_codepage in codec_map.keys(): + codec = codec_map[self.mobi_codepage] + if title == '': + title = self.header[:32] + title = title.split('\0')[0] + return unicode(title, codec) + + def getPIDMetaInfo(self): + rec209 = '' + token = '' + if 209 in self.meta_array: + rec209 = self.meta_array[209] + data = rec209 + # The 209 data comes in five byte groups. Interpret the last four bytes + # of each group as a big endian unsigned integer to get a key value + # if that key exists in the meta_array, append its contents to the token + for i in xrange(0,len(data),5): + val, = struct.unpack('>I',data[i+1:i+5]) + sval = self.meta_array.get(val,'') + token += sval + return rec209, token + + def patch(self, off, new): + self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):] + + def patchSection(self, section, new, in_off = 0): + if (section + 1 == self.num_sections): + endoff = len(self.data_file) + else: + endoff = self.sections[section + 1][0] + off = self.sections[section][0] + assert off + in_off + len(new) <= endoff + self.patch(off + in_off, new) + + def parseDRM(self, data, count, pidlist): + found_key = None + 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]) + 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 getFile(self, outpath): + file(outpath,'wb').write(self.mobi_data) + + def getBookType(self): + if self.print_replica: + return u"Print Replica" + if self.mobi_version >= 8: + return u"Kindle Format 8" + if self.mobi_version >= 0: + return u"Mobipocket {0:d}".format(self.mobi_version) + return u"PalmDoc" + + def getBookExtension(self): + if self.print_replica: + return u".azw4" + if self.mobi_version >= 8: + return u".azw3" + return u".mobi" + + def processBook(self, pidlist): + crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2]) + print u"Crypto Type is: {0:d}".format(crypto_type) + self.crypto_type = crypto_type + if crypto_type == 0: + print u"This book is not encrypted." + # we must still check for Print Replica + self.print_replica = (self.loadSection(1)[0:4] == '%MOP') + self.mobi_data = self.data_file + return + if crypto_type != 2 and crypto_type != 1: + raise DrmException(u"Cannot decode unknown Mobipocket encryption type {0:d}".format(crypto_type)) + if 406 in self.meta_array: + data406 = self.meta_array[406] + val406, = struct.unpack('>Q',data406) + if val406 != 0: + raise DrmException(u"Cannot decode library or rented ebooks.") + + goodpids = [] + for pid in pidlist: + if len(pid)==10: + if checksumPid(pid[0:-2]) != pid: + print u"Warning: PID {0} has incorrect checksum, should have been {1}".format(pid,checksumPid(pid[0:-2])) + goodpids.append(pid[0:-2]) + elif len(pid)==8: + goodpids.append(pid) + + if self.crypto_type == 1: + t1_keyvec = 'QDCVEPMU675RUBSZ' + if self.magic == 'TEXtREAd': + bookkey_data = self.sect[0x0E:0x0E+16] + elif self.mobi_version < 0: + bookkey_data = self.sect[0x90:0x90+16] + else: + bookkey_data = self.sect[self.mobi_length+16:self.mobi_length+32] + pid = '00000000' + found_key = PC1(t1_keyvec, bookkey_data) + else : + # 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(u"Encryption not initialised. Must be opened with Mobipocket Reader first.") + found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids) + if not found_key: + raise DrmException(u"No key found in {0:d} keys tried.".format(len(goodpids))) + # kill the drm keys + self.patchSection(0, '\0' * drm_size, drm_ptr) + # kill the drm pointers + self.patchSection(0, '\xff' * 4 + '\0' * 12, 0xA8) + + if pid=='00000000': + print u"File has default encryption, no specific key needed." + else: + print u"File is encoded with PID {0}.".format(checksumPid(pid)) + + # clear the crypto type + self.patchSection(0, "\0" * 2, 0xC) + + # decrypt sections + print u"Decrypting. Please wait . . .", + mobidataList = [] + mobidataList.append(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 u".", + # print "record %d, extra_size %d" %(i,extra_size) + decoded_data = PC1(found_key, data[0:len(data) - extra_size]) + if i==1: + self.print_replica = (decoded_data[0:4] == '%MOP') + mobidataList.append(decoded_data) + if extra_size > 0: + mobidataList.append(data[-extra_size:]) + if self.num_sections > self.records+1: + mobidataList.append(self.data_file[self.sections[self.records+1][0]:]) + self.mobi_data = "".join(mobidataList) + print u"done" + return + +def getUnencryptedBook(infile,pidlist): + if not os.path.isfile(infile): + raise DrmException(u"Input File Not Found.") + book = MobiBook(infile) + book.processBook(pidlist) + return book.mobi_data + + +def cli_main(): + argv=unicode_argv() + progname = os.path.basename(argv[0]) + if len(argv)<3 or len(argv)>4: + print u"MobiDeDrm v{0}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__) + print u"Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks" + print u"Usage:" + print u" {0} []".format(progname) + return 1 else: - libcrypto = find_library('crypto') - - if libcrypto is None: - return None - - libcrypto = CDLL(libcrypto) - - # typedef struct DES_ks - # { - # union - # { - # DES_cblock cblock; - # /* make sure things are correct size on machines with - # * 8 byte longs */ - # DES_LONG deslong[2]; - # } ks[16]; - # } DES_key_schedule; - - # just create a big enough place to hold everything - # it will have alignment of structure so we should be okay (16 byte aligned?) - class DES_KEY_SCHEDULE(Structure): - _fields_ = [('DES_cblock1', c_char * 16), - ('DES_cblock2', c_char * 16), - ('DES_cblock3', c_char * 16), - ('DES_cblock4', c_char * 16), - ('DES_cblock5', c_char * 16), - ('DES_cblock6', c_char * 16), - ('DES_cblock7', c_char * 16), - ('DES_cblock8', c_char * 16), - ('DES_cblock9', c_char * 16), - ('DES_cblock10', c_char * 16), - ('DES_cblock11', c_char * 16), - ('DES_cblock12', c_char * 16), - ('DES_cblock13', c_char * 16), - ('DES_cblock14', c_char * 16), - ('DES_cblock15', c_char * 16), - ('DES_cblock16', c_char * 16)] - - DES_KEY_SCHEDULE_p = POINTER(DES_KEY_SCHEDULE) - - def F(restype, name, argtypes): - func = getattr(libcrypto, name) - func.restype = restype - func.argtypes = argtypes - return func - - DES_set_key = F(None, 'DES_set_key',[c_char_p, DES_KEY_SCHEDULE_p]) - DES_ecb_encrypt = F(None, 'DES_ecb_encrypt',[c_char_p, c_char_p, DES_KEY_SCHEDULE_p, c_int]) - - - class DES(object): - def __init__(self, key): - if len(key) != 8 : - raise Exception('DES improper key used') - return - self.key = key - self.keyschedule = DES_KEY_SCHEDULE() - DES_set_key(self.key, self.keyschedule) - def desdecrypt(self, data): - ob = create_string_buffer(len(data)) - DES_ecb_encrypt(data, ob, self.keyschedule, 0) - return ob.raw - def decrypt(self, data): - if not data: - return '' - i = 0 - result = [] - while i < len(data): - block = data[i:i+8] - processed_block = self.desdecrypt(block) - result.append(processed_block) - i += 8 - return ''.join(result) - - return DES + infile = argv[1] + outfile = argv[2] + if len(argv) is 4: + pidlist = argv[3].split(',') + else: + pidlist = [] + try: + stripped_file = getUnencryptedBook(infile, pidlist) + file(outfile, 'wb').write(stripped_file) + except DrmException, e: + print u"MobiDeDRM v{0} Error: {0:s}".format(__version__,e.args[0]) + return 1 + return 0 + + +if __name__ == '__main__': + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) + sys.exit(cli_main()) diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/openssl_des.py b/DeDRM_calibre_plugin/DeDRM_plugin/openssl_des.py index e69de29..9a84e58 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/openssl_des.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/openssl_des.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab + +# implement just enough of des from openssl to make erdr2pml.py happy + +def load_libcrypto(): + from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_char, c_int, c_long, \ + Structure, c_ulong, create_string_buffer, cast + from ctypes.util import find_library + import sys + + if sys.platform.startswith('win'): + libcrypto = find_library('libeay32') + else: + libcrypto = find_library('crypto') + + if libcrypto is None: + return None + + libcrypto = CDLL(libcrypto) + + # typedef struct DES_ks + # { + # union + # { + # DES_cblock cblock; + # /* make sure things are correct size on machines with + # * 8 byte longs */ + # DES_LONG deslong[2]; + # } ks[16]; + # } DES_key_schedule; + + # just create a big enough place to hold everything + # it will have alignment of structure so we should be okay (16 byte aligned?) + class DES_KEY_SCHEDULE(Structure): + _fields_ = [('DES_cblock1', c_char * 16), + ('DES_cblock2', c_char * 16), + ('DES_cblock3', c_char * 16), + ('DES_cblock4', c_char * 16), + ('DES_cblock5', c_char * 16), + ('DES_cblock6', c_char * 16), + ('DES_cblock7', c_char * 16), + ('DES_cblock8', c_char * 16), + ('DES_cblock9', c_char * 16), + ('DES_cblock10', c_char * 16), + ('DES_cblock11', c_char * 16), + ('DES_cblock12', c_char * 16), + ('DES_cblock13', c_char * 16), + ('DES_cblock14', c_char * 16), + ('DES_cblock15', c_char * 16), + ('DES_cblock16', c_char * 16)] + + DES_KEY_SCHEDULE_p = POINTER(DES_KEY_SCHEDULE) + + def F(restype, name, argtypes): + func = getattr(libcrypto, name) + func.restype = restype + func.argtypes = argtypes + return func + + DES_set_key = F(None, 'DES_set_key',[c_char_p, DES_KEY_SCHEDULE_p]) + DES_ecb_encrypt = F(None, 'DES_ecb_encrypt',[c_char_p, c_char_p, DES_KEY_SCHEDULE_p, c_int]) + + + class DES(object): + def __init__(self, key): + if len(key) != 8 : + raise Exception('DES improper key used') + return + self.key = key + self.keyschedule = DES_KEY_SCHEDULE() + DES_set_key(self.key, self.keyschedule) + def desdecrypt(self, data): + ob = create_string_buffer(len(data)) + DES_ecb_encrypt(data, ob, self.keyschedule, 0) + return ob.raw + def decrypt(self, data): + if not data: + return '' + i = 0 + result = [] + while i < len(data): + block = data[i:i+8] + processed_block = self.desdecrypt(block) + result.append(processed_block) + i += 8 + return ''.join(result) + + return DES diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/plugin-import-name-dedrm.txt b/DeDRM_calibre_plugin/DeDRM_plugin/plugin-import-name-dedrm.txt index 05065ac..e69de29 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/plugin-import-name-dedrm.txt +++ b/DeDRM_calibre_plugin/DeDRM_plugin/plugin-import-name-dedrm.txt @@ -1,292 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai - -from __future__ import with_statement -__license__ = 'GPL v3' - -# Standard Python modules. -import os, sys, re, hashlib -import json -import traceback - -from calibre.utils.config import dynamic, config_dir, JSONConfig -from calibre_plugins.dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION -from calibre.constants import iswindows, isosx - -class DeDRM_Prefs(): - def __init__(self): - JSON_PATH = os.path.join(u"plugins", PLUGIN_NAME.strip().lower().replace(' ', '_') + '.json') - self.dedrmprefs = JSONConfig(JSON_PATH) - - self.dedrmprefs.defaults['configured'] = False - self.dedrmprefs.defaults['bandnkeys'] = {} - self.dedrmprefs.defaults['adeptkeys'] = {} - self.dedrmprefs.defaults['ereaderkeys'] = {} - self.dedrmprefs.defaults['kindlekeys'] = {} - self.dedrmprefs.defaults['pids'] = [] - self.dedrmprefs.defaults['serials'] = [] - self.dedrmprefs.defaults['adobewineprefix'] = "" - self.dedrmprefs.defaults['kindlewineprefix'] = "" - - # initialise - # we must actually set the prefs that are dictionaries and lists - # to empty dictionaries and lists, otherwise we are unable to add to them - # as then it just adds to the (memory only) dedrmprefs.defaults versions! - if self.dedrmprefs['bandnkeys'] == {}: - self.dedrmprefs['bandnkeys'] = {} - if self.dedrmprefs['adeptkeys'] == {}: - self.dedrmprefs['adeptkeys'] = {} - if self.dedrmprefs['ereaderkeys'] == {}: - self.dedrmprefs['ereaderkeys'] = {} - if self.dedrmprefs['kindlekeys'] == {}: - self.dedrmprefs['kindlekeys'] = {} - if self.dedrmprefs['pids'] == []: - self.dedrmprefs['pids'] = [] - if self.dedrmprefs['serials'] == []: - self.dedrmprefs['serials'] = [] - - def __getitem__(self,kind = None): - if kind is not None: - return self.dedrmprefs[kind] - return self.dedrmprefs - - def set(self, kind, value): - self.dedrmprefs[kind] = value - - def writeprefs(self,value = True): - self.dedrmprefs['configured'] = value - - def addnamedvaluetoprefs(self, prefkind, keyname, keyvalue): - try: - if keyvalue not in self.dedrmprefs[prefkind].values(): - # ensure that the keyname is unique - # by adding a number (starting with 2) to the name if it is not - namecount = 1 - newname = keyname - while newname in self.dedrmprefs[prefkind]: - namecount += 1 - newname = "{0:s}_{1:d}".format(keyname,namecount) - # add to the preferences - self.dedrmprefs[prefkind][newname] = keyvalue - return (True, newname) - except: - traceback.print_exc() - pass - return (False, keyname) - - def addvaluetoprefs(self, prefkind, prefsvalue): - # ensure the keyvalue isn't already in the preferences - try: - if prefsvalue not in self.dedrmprefs[prefkind]: - self.dedrmprefs[prefkind].append(prefsvalue) - return True - except: - traceback.print_exc() - return False - - -def convertprefs(always = False): - - def parseIgnobleString(keystuff): - from calibre_plugins.dedrm.ignoblekeygen import generate_key - userkeys = [] - ar = keystuff.split(':') - for keystring in ar: - try: - name, ccn = keystring.split(',') - # Generate Barnes & Noble EPUB user key from name and credit card number. - keyname = u"{0}_{1}".format(name.strip(),ccn.strip()[-4:]) - keyvalue = generate_key(name, ccn) - userkeys.append([keyname,keyvalue]) - except Exception, e: - traceback.print_exc() - print e.args[0] - pass - return userkeys - - def parseeReaderString(keystuff): - from calibre_plugins.dedrm.erdr2pml import getuser_key - userkeys = [] - ar = keystuff.split(':') - for keystring in ar: - try: - name, cc = keystring.split(',') - # Generate eReader user key from name and credit card number. - keyname = u"{0}_{1}".format(name.strip(),cc.strip()[-4:]) - keyvalue = getuser_key(name,cc).encode('hex') - userkeys.append([keyname,keyvalue]) - except Exception, e: - traceback.print_exc() - print e.args[0] - pass - return userkeys - - def parseKindleString(keystuff): - pids = [] - serials = [] - ar = keystuff.split(',') - for keystring in ar: - keystring = str(keystring).strip().replace(" ","") - if len(keystring) == 10 or len(keystring) == 8 and keystring not in pids: - pids.append(keystring) - elif len(keystring) == 16 and keystring[0] == 'B' and keystring not in serials: - serials.append(keystring) - return (pids,serials) - - def getConfigFiles(extension, encoding = None): - # get any files with extension 'extension' in the config dir - userkeys = [] - files = [f for f in os.listdir(config_dir) if f.endswith(extension)] - for filename in files: - try: - fpath = os.path.join(config_dir, filename) - key = os.path.splitext(filename)[0] - value = open(fpath, 'rb').read() - if encoding is not None: - value = value.encode(encoding) - userkeys.append([key,value]) - except: - traceback.print_exc() - pass - return userkeys - - dedrmprefs = DeDRM_Prefs() - - if (not always) and dedrmprefs['configured']: - # We've already converted old preferences, - # and we're not being forced to do it again, so just return - return - - - print u"{0} v{1}: Importing configuration data from old DeDRM plugins".format(PLUGIN_NAME, PLUGIN_VERSION) - - IGNOBLEPLUGINNAME = "Ignoble Epub DeDRM" - EREADERPLUGINNAME = "eReader PDB 2 PML" - OLDKINDLEPLUGINNAME = "K4PC, K4Mac, Kindle Mobi and Topaz DeDRM" - - # get prefs from older tools - kindleprefs = JSONConfig(os.path.join(u"plugins", u"K4MobiDeDRM")) - ignobleprefs = JSONConfig(os.path.join(u"plugins", u"ignoble_epub_dedrm")) - - # Handle the old ignoble plugin's customization string by converting the - # old string to stored keys... get that personal data out of plain sight. - from calibre.customize.ui import config - sc = config['plugin_customization'] - val = sc.pop(IGNOBLEPLUGINNAME, None) - if val is not None: - print u"{0} v{1}: Converting old Ignoble plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION) - priorkeycount = len(dedrmprefs['bandnkeys']) - userkeys = parseIgnobleString(str(val)) - for keypair in userkeys: - name = keypair[0] - value = keypair[1] - dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value) - addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount - print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from old Ignoble plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys") - # Make the json write all the prefs to disk - dedrmprefs.writeprefs(False) - - # Handle the old eReader plugin's customization string by converting the - # old string to stored keys... get that personal data out of plain sight. - val = sc.pop(EREADERPLUGINNAME, None) - if val is not None: - print u"{0} v{1}: Converting old eReader plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION) - priorkeycount = len(dedrmprefs['ereaderkeys']) - userkeys = parseeReaderString(str(val)) - for keypair in userkeys: - name = keypair[0] - value = keypair[1] - dedrmprefs.addnamedvaluetoprefs('ereaderkeys', name, value) - addedkeycount = len(dedrmprefs['ereaderkeys'])-priorkeycount - print u"{0} v{1}: {2:d} eReader {3} imported from old eReader plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys") - # Make the json write all the prefs to disk - dedrmprefs.writeprefs(False) - - # get old Kindle plugin configuration string - val = sc.pop(OLDKINDLEPLUGINNAME, None) - if val is not None: - print u"{0} v{1}: Converting old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION) - priorpidcount = len(dedrmprefs['pids']) - priorserialcount = len(dedrmprefs['serials']) - pids, serials = parseKindleString(val) - for pid in pids: - dedrmprefs.addvaluetoprefs('pids',pid) - for serial in serials: - dedrmprefs.addvaluetoprefs('serials',serial) - addedpidcount = len(dedrmprefs['pids']) - priorpidcount - addedserialcount = len(dedrmprefs['serials']) - priorserialcount - print u"{0} v{1}: {2:d} {3} and {4:d} {5} imported from old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, u"PID" if addedpidcount==1 else u"PIDs", addedserialcount, u"serial number" if addedserialcount==1 else u"serial numbers") - # Make the json write all the prefs to disk - dedrmprefs.writeprefs(False) - - # copy the customisations back into calibre preferences, as we've now removed the nasty plaintext - config['plugin_customization'] = sc - - # get any .b64 files in the config dir - priorkeycount = len(dedrmprefs['bandnkeys']) - bandnfilekeys = getConfigFiles('.b64') - for keypair in bandnfilekeys: - name = keypair[0] - value = keypair[1] - dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value) - addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount - if addedkeycount > 0: - print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key file" if addedkeycount==1 else u"key files") - # Make the json write all the prefs to disk - dedrmprefs.writeprefs(False) - - # get any .der files in the config dir - priorkeycount = len(dedrmprefs['adeptkeys']) - adeptfilekeys = getConfigFiles('.der','hex') - for keypair in adeptfilekeys: - name = keypair[0] - value = keypair[1] - dedrmprefs.addnamedvaluetoprefs('adeptkeys', name, value) - addedkeycount = len(dedrmprefs['adeptkeys'])-priorkeycount - if addedkeycount > 0: - print u"{0} v{1}: {2:d} Adobe Adept {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"keyfile" if addedkeycount==1 else u"keyfiles") - # Make the json write all the prefs to disk - dedrmprefs.writeprefs(False) - - # get ignoble json prefs - if 'keys' in ignobleprefs: - priorkeycount = len(dedrmprefs['bandnkeys']) - for name in ignobleprefs['keys']: - value = ignobleprefs['keys'][name] - dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value) - addedkeycount = len(dedrmprefs['bandnkeys']) - priorkeycount - # no need to delete old prefs, since they contain no recoverable private data - if addedkeycount > 0: - print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from Ignoble plugin preferences.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys") - # Make the json write all the prefs to disk - dedrmprefs.writeprefs(False) - - # get kindle json prefs - priorpidcount = len(dedrmprefs['pids']) - priorserialcount = len(dedrmprefs['serials']) - if 'pids' in kindleprefs: - pids, serials = parseKindleString(kindleprefs['pids']) - for pid in pids: - dedrmprefs.addvaluetoprefs('pids',pid) - if 'serials' in kindleprefs: - pids, serials = parseKindleString(kindleprefs['serials']) - for serial in serials: - dedrmprefs.addvaluetoprefs('serials',serial) - addedpidcount = len(dedrmprefs['pids']) - priorpidcount - if addedpidcount > 0: - print u"{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, u"PID" if addedpidcount==1 else u"PIDs") - addedserialcount = len(dedrmprefs['serials']) - priorserialcount - if addedserialcount > 0: - print u"{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedserialcount, u"serial number" if addedserialcount==1 else u"serial numbers") - try: - if 'wineprefix' in kindleprefs and kindleprefs['wineprefix'] != "": - dedrmprefs.set('adobewineprefix',kindleprefs['wineprefix']) - dedrmprefs.set('kindlewineprefix',kindleprefs['wineprefix']) - print u"{0} v{1}: WINEPREFIX ‘(2)’ imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, kindleprefs['wineprefix']) - except: - traceback.print_exc() - - - # Make the json write all the prefs to disk - dedrmprefs.writeprefs() - print u"{0} v{1}: Finished setting up configuration data.".format(PLUGIN_NAME, PLUGIN_VERSION) diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/prefs.py b/DeDRM_calibre_plugin/DeDRM_plugin/prefs.py new file mode 100644 index 0000000..05065ac --- /dev/null +++ b/DeDRM_calibre_plugin/DeDRM_plugin/prefs.py @@ -0,0 +1,292 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +from __future__ import with_statement +__license__ = 'GPL v3' + +# Standard Python modules. +import os, sys, re, hashlib +import json +import traceback + +from calibre.utils.config import dynamic, config_dir, JSONConfig +from calibre_plugins.dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION +from calibre.constants import iswindows, isosx + +class DeDRM_Prefs(): + def __init__(self): + JSON_PATH = os.path.join(u"plugins", PLUGIN_NAME.strip().lower().replace(' ', '_') + '.json') + self.dedrmprefs = JSONConfig(JSON_PATH) + + self.dedrmprefs.defaults['configured'] = False + self.dedrmprefs.defaults['bandnkeys'] = {} + self.dedrmprefs.defaults['adeptkeys'] = {} + self.dedrmprefs.defaults['ereaderkeys'] = {} + self.dedrmprefs.defaults['kindlekeys'] = {} + self.dedrmprefs.defaults['pids'] = [] + self.dedrmprefs.defaults['serials'] = [] + self.dedrmprefs.defaults['adobewineprefix'] = "" + self.dedrmprefs.defaults['kindlewineprefix'] = "" + + # initialise + # we must actually set the prefs that are dictionaries and lists + # to empty dictionaries and lists, otherwise we are unable to add to them + # as then it just adds to the (memory only) dedrmprefs.defaults versions! + if self.dedrmprefs['bandnkeys'] == {}: + self.dedrmprefs['bandnkeys'] = {} + if self.dedrmprefs['adeptkeys'] == {}: + self.dedrmprefs['adeptkeys'] = {} + if self.dedrmprefs['ereaderkeys'] == {}: + self.dedrmprefs['ereaderkeys'] = {} + if self.dedrmprefs['kindlekeys'] == {}: + self.dedrmprefs['kindlekeys'] = {} + if self.dedrmprefs['pids'] == []: + self.dedrmprefs['pids'] = [] + if self.dedrmprefs['serials'] == []: + self.dedrmprefs['serials'] = [] + + def __getitem__(self,kind = None): + if kind is not None: + return self.dedrmprefs[kind] + return self.dedrmprefs + + def set(self, kind, value): + self.dedrmprefs[kind] = value + + def writeprefs(self,value = True): + self.dedrmprefs['configured'] = value + + def addnamedvaluetoprefs(self, prefkind, keyname, keyvalue): + try: + if keyvalue not in self.dedrmprefs[prefkind].values(): + # ensure that the keyname is unique + # by adding a number (starting with 2) to the name if it is not + namecount = 1 + newname = keyname + while newname in self.dedrmprefs[prefkind]: + namecount += 1 + newname = "{0:s}_{1:d}".format(keyname,namecount) + # add to the preferences + self.dedrmprefs[prefkind][newname] = keyvalue + return (True, newname) + except: + traceback.print_exc() + pass + return (False, keyname) + + def addvaluetoprefs(self, prefkind, prefsvalue): + # ensure the keyvalue isn't already in the preferences + try: + if prefsvalue not in self.dedrmprefs[prefkind]: + self.dedrmprefs[prefkind].append(prefsvalue) + return True + except: + traceback.print_exc() + return False + + +def convertprefs(always = False): + + def parseIgnobleString(keystuff): + from calibre_plugins.dedrm.ignoblekeygen import generate_key + userkeys = [] + ar = keystuff.split(':') + for keystring in ar: + try: + name, ccn = keystring.split(',') + # Generate Barnes & Noble EPUB user key from name and credit card number. + keyname = u"{0}_{1}".format(name.strip(),ccn.strip()[-4:]) + keyvalue = generate_key(name, ccn) + userkeys.append([keyname,keyvalue]) + except Exception, e: + traceback.print_exc() + print e.args[0] + pass + return userkeys + + def parseeReaderString(keystuff): + from calibre_plugins.dedrm.erdr2pml import getuser_key + userkeys = [] + ar = keystuff.split(':') + for keystring in ar: + try: + name, cc = keystring.split(',') + # Generate eReader user key from name and credit card number. + keyname = u"{0}_{1}".format(name.strip(),cc.strip()[-4:]) + keyvalue = getuser_key(name,cc).encode('hex') + userkeys.append([keyname,keyvalue]) + except Exception, e: + traceback.print_exc() + print e.args[0] + pass + return userkeys + + def parseKindleString(keystuff): + pids = [] + serials = [] + ar = keystuff.split(',') + for keystring in ar: + keystring = str(keystring).strip().replace(" ","") + if len(keystring) == 10 or len(keystring) == 8 and keystring not in pids: + pids.append(keystring) + elif len(keystring) == 16 and keystring[0] == 'B' and keystring not in serials: + serials.append(keystring) + return (pids,serials) + + def getConfigFiles(extension, encoding = None): + # get any files with extension 'extension' in the config dir + userkeys = [] + files = [f for f in os.listdir(config_dir) if f.endswith(extension)] + for filename in files: + try: + fpath = os.path.join(config_dir, filename) + key = os.path.splitext(filename)[0] + value = open(fpath, 'rb').read() + if encoding is not None: + value = value.encode(encoding) + userkeys.append([key,value]) + except: + traceback.print_exc() + pass + return userkeys + + dedrmprefs = DeDRM_Prefs() + + if (not always) and dedrmprefs['configured']: + # We've already converted old preferences, + # and we're not being forced to do it again, so just return + return + + + print u"{0} v{1}: Importing configuration data from old DeDRM plugins".format(PLUGIN_NAME, PLUGIN_VERSION) + + IGNOBLEPLUGINNAME = "Ignoble Epub DeDRM" + EREADERPLUGINNAME = "eReader PDB 2 PML" + OLDKINDLEPLUGINNAME = "K4PC, K4Mac, Kindle Mobi and Topaz DeDRM" + + # get prefs from older tools + kindleprefs = JSONConfig(os.path.join(u"plugins", u"K4MobiDeDRM")) + ignobleprefs = JSONConfig(os.path.join(u"plugins", u"ignoble_epub_dedrm")) + + # Handle the old ignoble plugin's customization string by converting the + # old string to stored keys... get that personal data out of plain sight. + from calibre.customize.ui import config + sc = config['plugin_customization'] + val = sc.pop(IGNOBLEPLUGINNAME, None) + if val is not None: + print u"{0} v{1}: Converting old Ignoble plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION) + priorkeycount = len(dedrmprefs['bandnkeys']) + userkeys = parseIgnobleString(str(val)) + for keypair in userkeys: + name = keypair[0] + value = keypair[1] + dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value) + addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount + print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from old Ignoble plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys") + # Make the json write all the prefs to disk + dedrmprefs.writeprefs(False) + + # Handle the old eReader plugin's customization string by converting the + # old string to stored keys... get that personal data out of plain sight. + val = sc.pop(EREADERPLUGINNAME, None) + if val is not None: + print u"{0} v{1}: Converting old eReader plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION) + priorkeycount = len(dedrmprefs['ereaderkeys']) + userkeys = parseeReaderString(str(val)) + for keypair in userkeys: + name = keypair[0] + value = keypair[1] + dedrmprefs.addnamedvaluetoprefs('ereaderkeys', name, value) + addedkeycount = len(dedrmprefs['ereaderkeys'])-priorkeycount + print u"{0} v{1}: {2:d} eReader {3} imported from old eReader plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys") + # Make the json write all the prefs to disk + dedrmprefs.writeprefs(False) + + # get old Kindle plugin configuration string + val = sc.pop(OLDKINDLEPLUGINNAME, None) + if val is not None: + print u"{0} v{1}: Converting old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION) + priorpidcount = len(dedrmprefs['pids']) + priorserialcount = len(dedrmprefs['serials']) + pids, serials = parseKindleString(val) + for pid in pids: + dedrmprefs.addvaluetoprefs('pids',pid) + for serial in serials: + dedrmprefs.addvaluetoprefs('serials',serial) + addedpidcount = len(dedrmprefs['pids']) - priorpidcount + addedserialcount = len(dedrmprefs['serials']) - priorserialcount + print u"{0} v{1}: {2:d} {3} and {4:d} {5} imported from old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, u"PID" if addedpidcount==1 else u"PIDs", addedserialcount, u"serial number" if addedserialcount==1 else u"serial numbers") + # Make the json write all the prefs to disk + dedrmprefs.writeprefs(False) + + # copy the customisations back into calibre preferences, as we've now removed the nasty plaintext + config['plugin_customization'] = sc + + # get any .b64 files in the config dir + priorkeycount = len(dedrmprefs['bandnkeys']) + bandnfilekeys = getConfigFiles('.b64') + for keypair in bandnfilekeys: + name = keypair[0] + value = keypair[1] + dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value) + addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount + if addedkeycount > 0: + print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key file" if addedkeycount==1 else u"key files") + # Make the json write all the prefs to disk + dedrmprefs.writeprefs(False) + + # get any .der files in the config dir + priorkeycount = len(dedrmprefs['adeptkeys']) + adeptfilekeys = getConfigFiles('.der','hex') + for keypair in adeptfilekeys: + name = keypair[0] + value = keypair[1] + dedrmprefs.addnamedvaluetoprefs('adeptkeys', name, value) + addedkeycount = len(dedrmprefs['adeptkeys'])-priorkeycount + if addedkeycount > 0: + print u"{0} v{1}: {2:d} Adobe Adept {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"keyfile" if addedkeycount==1 else u"keyfiles") + # Make the json write all the prefs to disk + dedrmprefs.writeprefs(False) + + # get ignoble json prefs + if 'keys' in ignobleprefs: + priorkeycount = len(dedrmprefs['bandnkeys']) + for name in ignobleprefs['keys']: + value = ignobleprefs['keys'][name] + dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value) + addedkeycount = len(dedrmprefs['bandnkeys']) - priorkeycount + # no need to delete old prefs, since they contain no recoverable private data + if addedkeycount > 0: + print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from Ignoble plugin preferences.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys") + # Make the json write all the prefs to disk + dedrmprefs.writeprefs(False) + + # get kindle json prefs + priorpidcount = len(dedrmprefs['pids']) + priorserialcount = len(dedrmprefs['serials']) + if 'pids' in kindleprefs: + pids, serials = parseKindleString(kindleprefs['pids']) + for pid in pids: + dedrmprefs.addvaluetoprefs('pids',pid) + if 'serials' in kindleprefs: + pids, serials = parseKindleString(kindleprefs['serials']) + for serial in serials: + dedrmprefs.addvaluetoprefs('serials',serial) + addedpidcount = len(dedrmprefs['pids']) - priorpidcount + if addedpidcount > 0: + print u"{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, u"PID" if addedpidcount==1 else u"PIDs") + addedserialcount = len(dedrmprefs['serials']) - priorserialcount + if addedserialcount > 0: + print u"{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedserialcount, u"serial number" if addedserialcount==1 else u"serial numbers") + try: + if 'wineprefix' in kindleprefs and kindleprefs['wineprefix'] != "": + dedrmprefs.set('adobewineprefix',kindleprefs['wineprefix']) + dedrmprefs.set('kindlewineprefix',kindleprefs['wineprefix']) + print u"{0} v{1}: WINEPREFIX ‘(2)’ imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, kindleprefs['wineprefix']) + except: + traceback.print_exc() + + + # Make the json write all the prefs to disk + dedrmprefs.writeprefs() + print u"{0} v{1}: Finished setting up configuration data.".format(PLUGIN_NAME, PLUGIN_VERSION) diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/stylexml2css.py b/DeDRM_calibre_plugin/DeDRM_plugin/stylexml2css.py index c111850..daa108a 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/stylexml2css.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/stylexml2css.py @@ -178,7 +178,12 @@ class DocParser(object): if val == "": val = 0 - if not ((attr == 'hang') and (int(val) == 0)) : + if not ((attr == 'hang') and (int(val) == 0)): + try: + f = float(val) + except: + print "Warning: unrecognised val, ignoring" + val = 0 pv = float(val)/scale cssargs[attr] = (self.attr_val_map[attr], pv) keep = True diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/subasyncio.py b/DeDRM_calibre_plugin/DeDRM_plugin/subasyncio.py new file mode 100644 index 0000000..de084d3 --- /dev/null +++ b/DeDRM_calibre_plugin/DeDRM_plugin/subasyncio.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python +# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab + +import os, sys +import signal +import threading +import subprocess +from subprocess import Popen, PIPE, STDOUT + +# **heavily** chopped up and modfied version of asyncproc.py +# to make it actually work on Windows as well as Mac/Linux +# For the original see: +# "http://www.lysator.liu.se/~bellman/download/" +# author is "Thomas Bellman " +# 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() diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/topazextract.py b/DeDRM_calibre_plugin/DeDRM_plugin/topazextract.py index de084d3..fb5eb7a 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/topazextract.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/topazextract.py @@ -1,148 +1,538 @@ #!/usr/bin/env python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab - -import os, sys -import signal -import threading -import subprocess -from subprocess import Popen, PIPE, STDOUT - -# **heavily** chopped up and modfied version of asyncproc.py -# to make it actually work on Windows as well as Mac/Linux -# For the original see: -# "http://www.lysator.liu.se/~bellman/download/" -# author is "Thomas Bellman " -# 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() +# -*- coding: utf-8 -*- + +# topazextract.py +# Mostly written by some_updates based on code from many others + +# Changelog +# 4.9 - moved unicode_argv call inside main for Windows DeDRM compatibility +# 5.0 - Fixed potential unicode problem with command line interface + +__version__ = '5.0' + +import sys +import os, csv, getopt +import zlib, zipfile, tempfile, shutil +import traceback +from struct import pack +from struct import unpack +from alfcrypto import Topaz_Cipher + +class SafeUnbuffered: + def __init__(self, stream): + self.stream = stream + self.encoding = stream.encoding + if self.encoding == None: + self.encoding = "utf-8" + def write(self, data): + if isinstance(data,unicode): + data = data.encode(self.encoding,"replace") + self.stream.write(data) + self.stream.flush() + def __getattr__(self, attr): + return getattr(self.stream, attr) + +iswindows = sys.platform.startswith('win') +isosx = sys.platform.startswith('darwin') + +def unicode_argv(): + if iswindows: + # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode + # strings. + + # Versions 2.x of Python don't support Unicode in sys.argv on + # Windows, with the underlying Windows API instead replacing multi-byte + # characters with '?'. + + + from ctypes import POINTER, byref, cdll, c_int, windll + from ctypes.wintypes import LPCWSTR, LPWSTR + + GetCommandLineW = cdll.kernel32.GetCommandLineW + GetCommandLineW.argtypes = [] + GetCommandLineW.restype = LPCWSTR + + CommandLineToArgvW = windll.shell32.CommandLineToArgvW + CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] + CommandLineToArgvW.restype = POINTER(LPWSTR) + + cmd = GetCommandLineW() + argc = c_int(0) + argv = CommandLineToArgvW(cmd, byref(argc)) + if argc.value > 0: + # Remove Python executable and commands if present + start = argc.value - len(sys.argv) + return [argv[i] for i in + xrange(start, argc.value)] + # if we don't have any arguments at all, just pass back script name + # this should never happen + return [u"mobidedrm.py"] + else: + argvencoding = sys.stdin.encoding + if argvencoding == None: + argvencoding = 'utf-8' + return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv] + +#global switch +debug = False + +if 'calibre' in sys.modules: + inCalibre = True + from calibre_plugins.dedrm import kgenpids +else: + inCalibre = False + import kgenpids + + +class DrmException(Exception): + pass + + +# recursive zip creation support routine +def zipUpDir(myzip, tdir, localname): + currentdir = tdir + if localname != u"": + currentdir = os.path.join(currentdir,localname) + list = os.listdir(currentdir) + for file in list: + afilename = file + localfilePath = os.path.join(localname, afilename) + realfilePath = os.path.join(currentdir,file) + if os.path.isfile(realfilePath): + myzip.write(realfilePath, localfilePath) + elif os.path.isdir(realfilePath): + zipUpDir(myzip, tdir, localfilePath) + +# +# Utility routines +# + +# Get a 7 bit encoded number from file +def bookReadEncodedNumber(fo): + flag = False + data = ord(fo.read(1)) + if data == 0xFF: + flag = True + data = ord(fo.read(1)) + if data >= 0x80: + datax = (data & 0x7F) + while data >= 0x80 : + data = ord(fo.read(1)) + datax = (datax <<7) + (data & 0x7F) + data = datax + if flag: + data = -data + return data + +# Get a length prefixed string from file +def bookReadString(fo): + stringLength = bookReadEncodedNumber(fo) + return unpack(str(stringLength)+'s',fo.read(stringLength))[0] + +# +# crypto routines +# + +# Context initialisation for the Topaz Crypto +def topazCryptoInit(key): + return Topaz_Cipher().ctx_init(key) + +# ctx1 = 0x0CAFFE19E +# for keyChar in key: +# keyByte = ord(keyChar) +# ctx2 = ctx1 +# ctx1 = ((((ctx1 >>2) * (ctx1 >>7))&0xFFFFFFFF) ^ (keyByte * keyByte * 0x0F902007)& 0xFFFFFFFF ) +# return [ctx1,ctx2] + +# decrypt data with the context prepared by topazCryptoInit() +def topazCryptoDecrypt(data, ctx): + return Topaz_Cipher().decrypt(data, ctx) +# ctx1 = ctx[0] +# ctx2 = ctx[1] +# plainText = "" +# for dataChar in data: +# dataByte = ord(dataChar) +# m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF +# ctx2 = ctx1 +# ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF) +# plainText += chr(m) +# return plainText + +# Decrypt data with the PID +def decryptRecord(data,PID): + ctx = topazCryptoInit(PID) + return topazCryptoDecrypt(data, ctx) + +# Try to decrypt a dkey record (contains the bookPID) +def decryptDkeyRecord(data,PID): + record = decryptRecord(data,PID) + fields = unpack('3sB8sB8s3s',record) + if fields[0] != 'PID' or fields[5] != 'pid' : + raise DrmException(u"Didn't find PID magic numbers in record") + elif fields[1] != 8 or fields[3] != 8 : + raise DrmException(u"Record didn't contain correct length fields") + elif fields[2] != PID : + raise DrmException(u"Record didn't contain PID") + return fields[4] + +# Decrypt all dkey records (contain the book PID) +def decryptDkeyRecords(data,PID): + nbKeyRecords = ord(data[0]) + records = [] + data = data[1:] + for i in range (0,nbKeyRecords): + length = ord(data[0]) + try: + key = decryptDkeyRecord(data[1:length+1],PID) + records.append(key) + except DrmException: + pass + data = data[1+length:] + if len(records) == 0: + raise DrmException(u"BookKey Not Found") + return records + + +class TopazBook: + def __init__(self, filename): + self.fo = file(filename, 'rb') + self.outdir = tempfile.mkdtemp() + # self.outdir = 'rawdat' + self.bookPayloadOffset = 0 + self.bookHeaderRecords = {} + self.bookMetadata = {} + self.bookKey = None + magic = unpack('4s',self.fo.read(4))[0] + if magic != 'TPZ0': + raise DrmException(u"Parse Error : Invalid Header, not a Topaz file") + self.parseTopazHeaders() + self.parseMetadata() + + def parseTopazHeaders(self): + def bookReadHeaderRecordData(): + # Read and return the data of one header record at the current book file position + # [[offset,decompressedLength,compressedLength],...] + nbValues = bookReadEncodedNumber(self.fo) + if debug: print "%d records in header " % nbValues, + values = [] + for i in range (0,nbValues): + values.append([bookReadEncodedNumber(self.fo),bookReadEncodedNumber(self.fo),bookReadEncodedNumber(self.fo)]) + return values + def parseTopazHeaderRecord(): + # Read and parse one header record at the current book file position and return the associated data + # [[offset,decompressedLength,compressedLength],...] + if ord(self.fo.read(1)) != 0x63: + raise DrmException(u"Parse Error : Invalid Header") + tag = bookReadString(self.fo) + record = bookReadHeaderRecordData() + return [tag,record] + nbRecords = bookReadEncodedNumber(self.fo) + if debug: print "Headers: %d" % nbRecords + for i in range (0,nbRecords): + result = parseTopazHeaderRecord() + if debug: print result[0], ": ", result[1] + self.bookHeaderRecords[result[0]] = result[1] + if ord(self.fo.read(1)) != 0x64 : + raise DrmException(u"Parse Error : Invalid Header") + self.bookPayloadOffset = self.fo.tell() + + def parseMetadata(self): + # Parse the metadata record from the book payload and return a list of [key,values] + self.fo.seek(self.bookPayloadOffset + self.bookHeaderRecords['metadata'][0][0]) + tag = bookReadString(self.fo) + if tag != 'metadata' : + raise DrmException(u"Parse Error : Record Names Don't Match") + flags = ord(self.fo.read(1)) + nbRecords = ord(self.fo.read(1)) + if debug: print "Metadata Records: %d" % nbRecords + for i in range (0,nbRecords) : + keyval = bookReadString(self.fo) + content = bookReadString(self.fo) + if debug: print keyval + if debug: print content + self.bookMetadata[keyval] = content + return self.bookMetadata + + def getPIDMetaInfo(self): + keysRecord = self.bookMetadata.get('keys','') + keysRecordRecord = '' + if keysRecord != '': + keylst = keysRecord.split(',') + for keyval in keylst: + keysRecordRecord += self.bookMetadata.get(keyval,'') + return keysRecord, keysRecordRecord + + def getBookTitle(self): + title = '' + if 'Title' in self.bookMetadata: + title = self.bookMetadata['Title'] + return title.decode('utf-8') + + def setBookKey(self, key): + self.bookKey = key + + def getBookPayloadRecord(self, name, index): + # Get a record in the book payload, given its name and index. + # decrypted and decompressed if necessary + encrypted = False + compressed = False + try: + recordOffset = self.bookHeaderRecords[name][index][0] + except: + raise DrmException("Parse Error : Invalid Record, record not found") + + self.fo.seek(self.bookPayloadOffset + recordOffset) + + tag = bookReadString(self.fo) + if tag != name : + raise DrmException("Parse Error : Invalid Record, record name doesn't match") + + recordIndex = bookReadEncodedNumber(self.fo) + if recordIndex < 0 : + encrypted = True + recordIndex = -recordIndex -1 + + if recordIndex != index : + raise DrmException("Parse Error : Invalid Record, index doesn't match") + + if (self.bookHeaderRecords[name][index][2] > 0): + compressed = True + record = self.fo.read(self.bookHeaderRecords[name][index][2]) else: - 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() + record = self.fo.read(self.bookHeaderRecords[name][index][1]) + + if encrypted: + if self.bookKey: + ctx = topazCryptoInit(self.bookKey) + record = topazCryptoDecrypt(record,ctx) + else : + raise DrmException("Error: Attempt to decrypt without bookKey") + + if compressed: + record = zlib.decompress(record) + + return record + + def processBook(self, pidlst): + raw = 0 + fixedimage=True + try: + keydata = self.getBookPayloadRecord('dkey', 0) + except DrmException, e: + print u"no dkey record found, book may not be encrypted" + print u"attempting to extrct files without a book key" + self.createBookDirectory() + self.extractFiles() + print u"Successfully Extracted Topaz contents" + if inCalibre: + from calibre_plugins.dedrm import genbook + else: + import genbook + + rv = genbook.generateBook(self.outdir, raw, fixedimage) + if rv == 0: + print u"Book Successfully generated." + return rv + + # try each pid to decode the file + bookKey = None + for pid in pidlst: + # use 8 digit pids here + pid = pid[0:8] + print u"Trying: {0}".format(pid) + bookKeys = [] + data = keydata + try: + bookKeys+=decryptDkeyRecords(data,pid) + except DrmException, e: + pass + else: + bookKey = bookKeys[0] + print u"Book Key Found! ({0})".format(bookKey.encode('hex')) break - 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() + + if not bookKey: + raise DrmException(u"No key found in {0:d} keys tried. Read the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(len(pidlst))) + + self.setBookKey(bookKey) + self.createBookDirectory() + self.extractFiles() + print u"Successfully Extracted Topaz contents" + if inCalibre: + from calibre_plugins.dedrm import genbook + else: + import genbook + + rv = genbook.generateBook(self.outdir, raw, fixedimage) + if rv == 0: + print u"Book Successfully generated" + return rv + + def createBookDirectory(self): + outdir = self.outdir + # create output directory structure + if not os.path.exists(outdir): + os.makedirs(outdir) + destdir = os.path.join(outdir,u"img") + if not os.path.exists(destdir): + os.makedirs(destdir) + destdir = os.path.join(outdir,u"color_img") + if not os.path.exists(destdir): + os.makedirs(destdir) + destdir = os.path.join(outdir,u"page") + if not os.path.exists(destdir): + os.makedirs(destdir) + destdir = os.path.join(outdir,u"glyphs") + if not os.path.exists(destdir): + os.makedirs(destdir) + + def extractFiles(self): + outdir = self.outdir + for headerRecord in self.bookHeaderRecords: + name = headerRecord + if name != 'dkey': + ext = u".dat" + if name == 'img': ext = u".jpg" + if name == 'color' : ext = u".jpg" + print u"Processing Section: {0}\n. . .".format(name), + for index in range (0,len(self.bookHeaderRecords[name])) : + fname = u"{0}{1:04d}{2}".format(name,index,ext) + destdir = outdir + if name == 'img': + destdir = os.path.join(outdir,u"img") + if name == 'color': + destdir = os.path.join(outdir,u"color_img") + if name == 'page': + destdir = os.path.join(outdir,u"page") + if name == 'glyphs': + destdir = os.path.join(outdir,u"glyphs") + outputFile = os.path.join(destdir,fname) + print u".", + record = self.getBookPayloadRecord(name,index) + if record != '': + file(outputFile, 'wb').write(record) + print u" " + + def getFile(self, zipname): + htmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False) + htmlzip.write(os.path.join(self.outdir,u"book.html"),u"book.html") + htmlzip.write(os.path.join(self.outdir,u"book.opf"),u"book.opf") + if os.path.isfile(os.path.join(self.outdir,u"cover.jpg")): + htmlzip.write(os.path.join(self.outdir,u"cover.jpg"),u"cover.jpg") + htmlzip.write(os.path.join(self.outdir,u"style.css"),u"style.css") + zipUpDir(htmlzip, self.outdir, u"img") + htmlzip.close() + + def getBookType(self): + return u"Topaz" + + def getBookExtension(self): + return u".htmlz" + + def getSVGZip(self, zipname): + svgzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False) + svgzip.write(os.path.join(self.outdir,u"index_svg.xhtml"),u"index_svg.xhtml") + zipUpDir(svgzip, self.outdir, u"svg") + zipUpDir(svgzip, self.outdir, u"img") + svgzip.close() + + def cleanup(self): + if os.path.isdir(self.outdir): + shutil.rmtree(self.outdir, True) + +def usage(progname): + print u"Removes DRM protection from Topaz ebooks and extracts the contents" + print u"Usage:" + print u" {0} [-k ] [-p ] [-s ] ".format(progname) + +# Main +def cli_main(): + argv=unicode_argv() + progname = os.path.basename(argv[0]) + print u"TopazExtract v{0}.".format(__version__) + + try: + opts, args = getopt.getopt(argv[1:], "k:p:s:x") + except getopt.GetoptError, err: + print u"Error in options or arguments: {0}".format(err.args[0]) + usage(progname) + return 1 + if len(args)<2: + usage(progname) + return 1 + + infile = args[0] + outdir = args[1] + if not os.path.isfile(infile): + print u"Input File {0} Does Not Exist.".format(infile) + return 1 + + if not os.path.exists(outdir): + print u"Output Directory {0} Does Not Exist.".format(outdir) + return 1 + + kDatabaseFiles = [] + serials = [] + pids = [] + + for o, a in opts: + if o == '-k': + if a == None : + raise DrmException("Invalid parameter for -k") + kDatabaseFiles.append(a) + if o == '-p': + if a == None : + raise DrmException("Invalid parameter for -p") + pids = a.split(',') + if o == '-s': + if a == None : + raise DrmException("Invalid parameter for -s") + serials = [serial.replace(" ","") for serial in a.split(',')] + + bookname = os.path.splitext(os.path.basename(infile))[0] + + tb = TopazBook(infile) + title = tb.getBookTitle() + print u"Processing Book: {0}".format(title) + md1, md2 = tb.getPIDMetaInfo() + pids.extend(kgenpids.getPidList(md1, md2, serials, kDatabaseFiles)) + + try: + print u"Decrypting Book" + tb.processBook(pids) + + print u" Creating HTML ZIP Archive" + zipname = os.path.join(outdir, bookname + u"_nodrm.htmlz") + tb.getFile(zipname) + + print u" Creating SVG ZIP Archive" + zipname = os.path.join(outdir, bookname + u"_SVG.zip") + tb.getSVGZip(zipname) + + # removing internal temporary directory of pieces + tb.cleanup() + + except DrmException, e: + print u"Decryption failed\n{0}".format(traceback.format_exc()) + + try: + tb.cleanup() + except: + pass + return 1 + + except Exception, e: + print u"Decryption failed\m{0}".format(traceback.format_exc()) + try: + tb.cleanup() + except: + pass + return 1 + + return 0 + + +if __name__ == '__main__': + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) + sys.exit(cli_main()) diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/utilities.py b/DeDRM_calibre_plugin/DeDRM_plugin/utilities.py index 97f6583..c730607 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/utilities.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/utilities.py @@ -1,538 +1,39 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# topazextract.py -# Mostly written by some_updates based on code from many others - -# Changelog -# 4.9 - moved unicode_argv call inside main for Windows DeDRM compatibility -# 5.0 - Fixed potential unicode problem with command line interface - -__version__ = '5.0' - -import sys -import os, csv, getopt -import zlib, zipfile, tempfile, shutil -import traceback -from struct import pack -from struct import unpack -from alfcrypto import Topaz_Cipher - -class SafeUnbuffered: - def __init__(self, stream): - self.stream = stream - self.encoding = stream.encoding - if self.encoding == None: - self.encoding = "utf-8" - def write(self, data): - if isinstance(data,unicode): - data = data.encode(self.encoding,"replace") - self.stream.write(data) - self.stream.flush() - def __getattr__(self, attr): - return getattr(self.stream, attr) - -iswindows = sys.platform.startswith('win') -isosx = sys.platform.startswith('darwin') - -def unicode_argv(): - if iswindows: - # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode - # strings. - - # Versions 2.x of Python don't support Unicode in sys.argv on - # Windows, with the underlying Windows API instead replacing multi-byte - # characters with '?'. - - - from ctypes import POINTER, byref, cdll, c_int, windll - from ctypes.wintypes import LPCWSTR, LPWSTR - - GetCommandLineW = cdll.kernel32.GetCommandLineW - GetCommandLineW.argtypes = [] - GetCommandLineW.restype = LPCWSTR - - CommandLineToArgvW = windll.shell32.CommandLineToArgvW - CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] - CommandLineToArgvW.restype = POINTER(LPWSTR) - - cmd = GetCommandLineW() - argc = c_int(0) - argv = CommandLineToArgvW(cmd, byref(argc)) - if argc.value > 0: - # Remove Python executable and commands if present - start = argc.value - len(sys.argv) - return [argv[i] for i in - xrange(start, argc.value)] - # if we don't have any arguments at all, just pass back script name - # this should never happen - return [u"mobidedrm.py"] +from __future__ import with_statement + +__license__ = 'GPL v3' + +DETAILED_MESSAGE = \ +'You have personal information stored in this plugin\'s customization '+ \ +'string from a previous version of this plugin.\n\n'+ \ +'This new version of the plugin can convert that info '+ \ +'into key data that the new plugin can then use (which doesn\'t '+ \ +'require personal information to be stored/displayed in an insecure '+ \ +'manner like the old plugin did).\n\nIf you choose NOT to migrate this data at this time '+ \ +'you will be prompted to save that personal data to a file elsewhere; and you\'ll have '+ \ +'to manually re-configure this plugin with your information.\n\nEither way... ' + \ +'this new version of the plugin will not be responsible for storing that personal '+ \ +'info in plain sight any longer.' + +def uStrCmp (s1, s2, caseless=False): + import unicodedata as ud + str1 = s1 if isinstance(s1, unicode) else unicode(s1) + str2 = s2 if isinstance(s2, unicode) else unicode(s2) + if caseless: + return ud.normalize('NFC', str1.lower()) == ud.normalize('NFC', str2.lower()) else: - argvencoding = sys.stdin.encoding - if argvencoding == None: - argvencoding = 'utf-8' - return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv] - -#global switch -debug = False - -if 'calibre' in sys.modules: - inCalibre = True - from calibre_plugins.dedrm import kgenpids -else: - inCalibre = False - import kgenpids - - -class DrmException(Exception): - pass - - -# recursive zip creation support routine -def zipUpDir(myzip, tdir, localname): - currentdir = tdir - if localname != u"": - currentdir = os.path.join(currentdir,localname) - list = os.listdir(currentdir) - for file in list: - afilename = file - localfilePath = os.path.join(localname, afilename) - realfilePath = os.path.join(currentdir,file) - if os.path.isfile(realfilePath): - myzip.write(realfilePath, localfilePath) - elif os.path.isdir(realfilePath): - zipUpDir(myzip, tdir, localfilePath) - -# -# Utility routines -# - -# Get a 7 bit encoded number from file -def bookReadEncodedNumber(fo): - flag = False - data = ord(fo.read(1)) - if data == 0xFF: - flag = True - data = ord(fo.read(1)) - if data >= 0x80: - datax = (data & 0x7F) - while data >= 0x80 : - data = ord(fo.read(1)) - datax = (datax <<7) + (data & 0x7F) - data = datax - if flag: - data = -data - return data - -# Get a length prefixed string from file -def bookReadString(fo): - stringLength = bookReadEncodedNumber(fo) - return unpack(str(stringLength)+'s',fo.read(stringLength))[0] + return ud.normalize('NFC', str1) == ud.normalize('NFC', str2) -# -# crypto routines -# - -# Context initialisation for the Topaz Crypto -def topazCryptoInit(key): - return Topaz_Cipher().ctx_init(key) - -# ctx1 = 0x0CAFFE19E -# for keyChar in key: -# keyByte = ord(keyChar) -# ctx2 = ctx1 -# ctx1 = ((((ctx1 >>2) * (ctx1 >>7))&0xFFFFFFFF) ^ (keyByte * keyByte * 0x0F902007)& 0xFFFFFFFF ) -# return [ctx1,ctx2] - -# decrypt data with the context prepared by topazCryptoInit() -def topazCryptoDecrypt(data, ctx): - return Topaz_Cipher().decrypt(data, ctx) -# ctx1 = ctx[0] -# ctx2 = ctx[1] -# plainText = "" -# for dataChar in data: -# dataByte = ord(dataChar) -# m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF -# ctx2 = ctx1 -# ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF) -# plainText += chr(m) -# return plainText - -# Decrypt data with the PID -def decryptRecord(data,PID): - ctx = topazCryptoInit(PID) - return topazCryptoDecrypt(data, ctx) - -# Try to decrypt a dkey record (contains the bookPID) -def decryptDkeyRecord(data,PID): - record = decryptRecord(data,PID) - fields = unpack('3sB8sB8s3s',record) - if fields[0] != 'PID' or fields[5] != 'pid' : - raise DrmException(u"Didn't find PID magic numbers in record") - elif fields[1] != 8 or fields[3] != 8 : - raise DrmException(u"Record didn't contain correct length fields") - elif fields[2] != PID : - raise DrmException(u"Record didn't contain PID") - return fields[4] - -# Decrypt all dkey records (contain the book PID) -def decryptDkeyRecords(data,PID): - nbKeyRecords = ord(data[0]) - records = [] - data = data[1:] - for i in range (0,nbKeyRecords): - length = ord(data[0]) +def parseCustString(keystuff): + userkeys = [] + ar = keystuff.split(':') + for i in ar: try: - key = decryptDkeyRecord(data[1:length+1],PID) - records.append(key) - except DrmException: - pass - data = data[1+length:] - if len(records) == 0: - raise DrmException(u"BookKey Not Found") - return records - - -class TopazBook: - def __init__(self, filename): - self.fo = file(filename, 'rb') - self.outdir = tempfile.mkdtemp() - # self.outdir = 'rawdat' - self.bookPayloadOffset = 0 - self.bookHeaderRecords = {} - self.bookMetadata = {} - self.bookKey = None - magic = unpack('4s',self.fo.read(4))[0] - if magic != 'TPZ0': - raise DrmException(u"Parse Error : Invalid Header, not a Topaz file") - self.parseTopazHeaders() - self.parseMetadata() - - def parseTopazHeaders(self): - def bookReadHeaderRecordData(): - # Read and return the data of one header record at the current book file position - # [[offset,decompressedLength,compressedLength],...] - nbValues = bookReadEncodedNumber(self.fo) - if debug: print "%d records in header " % nbValues, - values = [] - for i in range (0,nbValues): - values.append([bookReadEncodedNumber(self.fo),bookReadEncodedNumber(self.fo),bookReadEncodedNumber(self.fo)]) - return values - def parseTopazHeaderRecord(): - # Read and parse one header record at the current book file position and return the associated data - # [[offset,decompressedLength,compressedLength],...] - if ord(self.fo.read(1)) != 0x63: - raise DrmException(u"Parse Error : Invalid Header") - tag = bookReadString(self.fo) - record = bookReadHeaderRecordData() - return [tag,record] - nbRecords = bookReadEncodedNumber(self.fo) - if debug: print "Headers: %d" % nbRecords - for i in range (0,nbRecords): - result = parseTopazHeaderRecord() - if debug: print result[0], ": ", result[1] - self.bookHeaderRecords[result[0]] = result[1] - if ord(self.fo.read(1)) != 0x64 : - raise DrmException(u"Parse Error : Invalid Header") - self.bookPayloadOffset = self.fo.tell() - - def parseMetadata(self): - # Parse the metadata record from the book payload and return a list of [key,values] - self.fo.seek(self.bookPayloadOffset + self.bookHeaderRecords['metadata'][0][0]) - tag = bookReadString(self.fo) - if tag != 'metadata' : - raise DrmException(u"Parse Error : Record Names Don't Match") - flags = ord(self.fo.read(1)) - nbRecords = ord(self.fo.read(1)) - if debug: print "Metadata Records: %d" % nbRecords - for i in range (0,nbRecords) : - keyval = bookReadString(self.fo) - content = bookReadString(self.fo) - if debug: print keyval - if debug: print content - self.bookMetadata[keyval] = content - return self.bookMetadata - - def getPIDMetaInfo(self): - keysRecord = self.bookMetadata.get('keys','') - keysRecordRecord = '' - if keysRecord != '': - keylst = keysRecord.split(',') - for keyval in keylst: - keysRecordRecord += self.bookMetadata.get(keyval,'') - return keysRecord, keysRecordRecord - - def getBookTitle(self): - title = '' - if 'Title' in self.bookMetadata: - title = self.bookMetadata['Title'] - return title.decode('utf-8') - - def setBookKey(self, key): - self.bookKey = key - - def getBookPayloadRecord(self, name, index): - # Get a record in the book payload, given its name and index. - # decrypted and decompressed if necessary - encrypted = False - compressed = False - try: - recordOffset = self.bookHeaderRecords[name][index][0] - except: - raise DrmException("Parse Error : Invalid Record, record not found") - - self.fo.seek(self.bookPayloadOffset + recordOffset) - - tag = bookReadString(self.fo) - if tag != name : - raise DrmException("Parse Error : Invalid Record, record name doesn't match") - - recordIndex = bookReadEncodedNumber(self.fo) - if recordIndex < 0 : - encrypted = True - recordIndex = -recordIndex -1 - - if recordIndex != index : - raise DrmException("Parse Error : Invalid Record, index doesn't match") - - if (self.bookHeaderRecords[name][index][2] > 0): - compressed = True - record = self.fo.read(self.bookHeaderRecords[name][index][2]) - else: - record = self.fo.read(self.bookHeaderRecords[name][index][1]) - - if encrypted: - if self.bookKey: - ctx = topazCryptoInit(self.bookKey) - record = topazCryptoDecrypt(record,ctx) - else : - raise DrmException("Error: Attempt to decrypt without bookKey") - - if compressed: - record = zlib.decompress(record) - - return record - - def processBook(self, pidlst): - raw = 0 - fixedimage=True - try: - keydata = self.getBookPayloadRecord('dkey', 0) - except DrmException, e: - print u"no dkey record found, book may not be encrypted" - print u"attempting to extrct files without a book key" - self.createBookDirectory() - self.extractFiles() - print u"Successfully Extracted Topaz contents" - if inCalibre: - from calibre_plugins.dedrm import genbook - else: - import genbook - - rv = genbook.generateBook(self.outdir, raw, fixedimage) - if rv == 0: - print u"Book Successfully generated." - return rv - - # try each pid to decode the file - bookKey = None - for pid in pidlst: - # use 8 digit pids here - pid = pid[0:8] - print u"Trying: {0}".format(pid) - bookKeys = [] - data = keydata - try: - bookKeys+=decryptDkeyRecords(data,pid) - except DrmException, e: - pass - else: - bookKey = bookKeys[0] - print u"Book Key Found! ({0})".format(bookKey.encode('hex')) - break - - if not bookKey: - raise DrmException(u"No key found in {0:d} keys tried. Read the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(len(pidlst))) - - self.setBookKey(bookKey) - self.createBookDirectory() - self.extractFiles() - print u"Successfully Extracted Topaz contents" - if inCalibre: - from calibre_plugins.dedrm import genbook - else: - import genbook - - rv = genbook.generateBook(self.outdir, raw, fixedimage) - if rv == 0: - print u"Book Successfully generated" - return rv - - def createBookDirectory(self): - outdir = self.outdir - # create output directory structure - if not os.path.exists(outdir): - os.makedirs(outdir) - destdir = os.path.join(outdir,u"img") - if not os.path.exists(destdir): - os.makedirs(destdir) - destdir = os.path.join(outdir,u"color_img") - if not os.path.exists(destdir): - os.makedirs(destdir) - destdir = os.path.join(outdir,u"page") - if not os.path.exists(destdir): - os.makedirs(destdir) - destdir = os.path.join(outdir,u"glyphs") - if not os.path.exists(destdir): - os.makedirs(destdir) - - def extractFiles(self): - outdir = self.outdir - for headerRecord in self.bookHeaderRecords: - name = headerRecord - if name != 'dkey': - ext = u".dat" - if name == 'img': ext = u".jpg" - if name == 'color' : ext = u".jpg" - print u"Processing Section: {0}\n. . .".format(name), - for index in range (0,len(self.bookHeaderRecords[name])) : - fname = u"{0}{1:04d}{2}".format(name,index,ext) - destdir = outdir - if name == 'img': - destdir = os.path.join(outdir,u"img") - if name == 'color': - destdir = os.path.join(outdir,u"color_img") - if name == 'page': - destdir = os.path.join(outdir,u"page") - if name == 'glyphs': - destdir = os.path.join(outdir,u"glyphs") - outputFile = os.path.join(destdir,fname) - print u".", - record = self.getBookPayloadRecord(name,index) - if record != '': - file(outputFile, 'wb').write(record) - print u" " - - def getFile(self, zipname): - htmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False) - htmlzip.write(os.path.join(self.outdir,u"book.html"),u"book.html") - htmlzip.write(os.path.join(self.outdir,u"book.opf"),u"book.opf") - if os.path.isfile(os.path.join(self.outdir,u"cover.jpg")): - htmlzip.write(os.path.join(self.outdir,u"cover.jpg"),u"cover.jpg") - htmlzip.write(os.path.join(self.outdir,u"style.css"),u"style.css") - zipUpDir(htmlzip, self.outdir, u"img") - htmlzip.close() - - def getBookType(self): - return u"Topaz" - - def getBookExtension(self): - return u".htmlz" - - def getSVGZip(self, zipname): - svgzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False) - svgzip.write(os.path.join(self.outdir,u"index_svg.xhtml"),u"index_svg.xhtml") - zipUpDir(svgzip, self.outdir, u"svg") - zipUpDir(svgzip, self.outdir, u"img") - svgzip.close() - - def cleanup(self): - if os.path.isdir(self.outdir): - shutil.rmtree(self.outdir, True) - -def usage(progname): - print u"Removes DRM protection from Topaz ebooks and extracts the contents" - print u"Usage:" - print u" {0} [-k ] [-p ] [-s ] ".format(progname) - -# Main -def cli_main(): - argv=unicode_argv() - progname = os.path.basename(argv[0]) - print u"TopazExtract v{0}.".format(__version__) - - try: - opts, args = getopt.getopt(argv[1:], "k:p:s:x") - except getopt.GetoptError, err: - print u"Error in options or arguments: {0}".format(err.args[0]) - usage(progname) - return 1 - if len(args)<2: - usage(progname) - return 1 - - infile = args[0] - outdir = args[1] - if not os.path.isfile(infile): - print u"Input File {0} Does Not Exist.".format(infile) - return 1 - - if not os.path.exists(outdir): - print u"Output Directory {0} Does Not Exist.".format(outdir) - return 1 - - kDatabaseFiles = [] - serials = [] - pids = [] - - for o, a in opts: - if o == '-k': - if a == None : - raise DrmException("Invalid parameter for -k") - kDatabaseFiles.append(a) - if o == '-p': - if a == None : - raise DrmException("Invalid parameter for -p") - pids = a.split(',') - if o == '-s': - if a == None : - raise DrmException("Invalid parameter for -s") - serials = [serial.replace(" ","") for serial in a.split(',')] - - bookname = os.path.splitext(os.path.basename(infile))[0] - - tb = TopazBook(infile) - title = tb.getBookTitle() - print u"Processing Book: {0}".format(title) - md1, md2 = tb.getPIDMetaInfo() - pids.extend(kgenpids.getPidList(md1, md2, serials, kDatabaseFiles)) - - try: - print u"Decrypting Book" - tb.processBook(pids) - - print u" Creating HTML ZIP Archive" - zipname = os.path.join(outdir, bookname + u"_nodrm.htmlz") - tb.getFile(zipname) - - print u" Creating SVG ZIP Archive" - zipname = os.path.join(outdir, bookname + u"_SVG.zip") - tb.getSVGZip(zipname) - - # removing internal temporary directory of pieces - tb.cleanup() - - except DrmException, e: - print u"Decryption failed\n{0}".format(traceback.format_exc()) - - try: - tb.cleanup() - except: - pass - return 1 - - except Exception, e: - print u"Decryption failed\m{0}".format(traceback.format_exc()) - try: - tb.cleanup() + name, ccn = i.split(',') + # Generate Barnes & Noble EPUB user key from name and credit card number. + userkeys.append(generate_key(name, ccn)) except: pass - return 1 - - return 0 - - -if __name__ == '__main__': - sys.stdout=SafeUnbuffered(sys.stdout) - sys.stderr=SafeUnbuffered(sys.stderr) - sys.exit(cli_main()) + return userkeys diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/wineutils.py b/DeDRM_calibre_plugin/DeDRM_plugin/wineutils.py new file mode 100644 index 0000000..b54db80 --- /dev/null +++ b/DeDRM_calibre_plugin/DeDRM_plugin/wineutils.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from __future__ import with_statement + +__license__ = 'GPL v3' + +# Standard Python modules. +import os, sys, re, hashlib +from calibre_plugins.dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION + +def WineGetKeys(scriptpath, extension, wineprefix=""): + import subprocess + from subprocess import Popen, PIPE, STDOUT + + import subasyncio + from subasyncio import Process + + if extension == u".k4i": + import json + + basepath, script = os.path.split(scriptpath) + print u"{0} v{1}: Running {2} under Wine".format(PLUGIN_NAME, PLUGIN_VERSION, script) + + outdirpath = os.path.join(basepath, u"winekeysdir") + if not os.path.exists(outdirpath): + os.makedirs(outdirpath) + + if wineprefix != "" and os.path.exists(wineprefix): + cmdline = u"WINEPREFIX=\"{2}\" wine python.exe \"{0}\" \"{1}\"".format(scriptpath,outdirpath,wineprefix) + else: + cmdline = u"wine python.exe \"{0}\" \"{1}\"".format(scriptpath,outdirpath) + print u"{0} v{1}: Command line: “{2}”".format(PLUGIN_NAME, PLUGIN_VERSION, cmdline) + + try: + cmdline = cmdline.encode(sys.getfilesystemencoding()) + p2 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=sys.stdout, stderr=STDOUT, close_fds=False) + result = p2.wait("wait") + except Exception, e: + print u"{0} v{1}: Wine subprocess call error: {2}".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0]) + return [] + + winekeys = [] + # get any files with extension in the output dir + files = [f for f in os.listdir(outdirpath) if f.endswith(extension)] + for filename in files: + try: + fpath = os.path.join(outdirpath, filename) + with open(fpath, 'rb') as keyfile: + if extension == u".k4i": + new_key_value = json.loads(keyfile.read()) + else: + new_key_value = keyfile.read() + winekeys.append(new_key_value) + except: + print u"{0} v{1}: Error loading file {2}".format(PLUGIN_NAME, PLUGIN_VERSION, filename) + traceback.print_exc() + os.remove(fpath) + print u"{0} v{1}: Found and decrypted {2} {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(winekeys), u"key file" if len(winekeys) == 1 else u"key files") + return winekeys diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/zipfilerugged.py b/DeDRM_calibre_plugin/DeDRM_plugin/zipfilerugged.py index c730607..4a55a69 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/zipfilerugged.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/zipfilerugged.py @@ -1,39 +1,1400 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import with_statement - -__license__ = 'GPL v3' - -DETAILED_MESSAGE = \ -'You have personal information stored in this plugin\'s customization '+ \ -'string from a previous version of this plugin.\n\n'+ \ -'This new version of the plugin can convert that info '+ \ -'into key data that the new plugin can then use (which doesn\'t '+ \ -'require personal information to be stored/displayed in an insecure '+ \ -'manner like the old plugin did).\n\nIf you choose NOT to migrate this data at this time '+ \ -'you will be prompted to save that personal data to a file elsewhere; and you\'ll have '+ \ -'to manually re-configure this plugin with your information.\n\nEither way... ' + \ -'this new version of the plugin will not be responsible for storing that personal '+ \ -'info in plain sight any longer.' - -def uStrCmp (s1, s2, caseless=False): - import unicodedata as ud - str1 = s1 if isinstance(s1, unicode) else unicode(s1) - str2 = s2 if isinstance(s2, unicode) else unicode(s2) - if caseless: - return ud.normalize('NFC', str1.lower()) == ud.normalize('NFC', str2.lower()) - else: - return ud.normalize('NFC', str1) == ud.normalize('NFC', str2) - -def parseCustString(keystuff): - userkeys = [] - ar = keystuff.split(':') - for i in ar: - try: - name, ccn = i.split(',') - # Generate Barnes & Noble EPUB user key from name and credit card number. - userkeys.append(generate_key(name, ccn)) - except: +""" +Read and write ZIP files. +""" +import struct, os, time, sys, shutil +import binascii, cStringIO, stat +import io +import re + +try: + import zlib # We may need its compression method + crc32 = zlib.crc32 +except ImportError: + zlib = None + crc32 = binascii.crc32 + +__all__ = ["BadZipfile", "error", "ZIP_STORED", "ZIP_DEFLATED", "is_zipfile", + "ZipInfo", "ZipFile", "PyZipFile", "LargeZipFile" ] + +class BadZipfile(Exception): + pass + + +class LargeZipFile(Exception): + """ + Raised when writing a zipfile, the zipfile requires ZIP64 extensions + and those extensions are disabled. + """ + +error = BadZipfile # The exception raised by this module + +ZIP64_LIMIT = (1 << 31) - 1 +ZIP_FILECOUNT_LIMIT = 1 << 16 +ZIP_MAX_COMMENT = (1 << 16) - 1 + +# constants for Zip file compression methods +ZIP_STORED = 0 +ZIP_DEFLATED = 8 +# Other ZIP compression methods not supported + +# Below are some formats and associated data for reading/writing headers using +# the struct module. The names and structures of headers/records are those used +# in the PKWARE description of the ZIP file format: +# http://www.pkware.com/documents/casestudies/APPNOTE.TXT +# (URL valid as of January 2008) + +# The "end of central directory" structure, magic number, size, and indices +# (section V.I in the format document) +structEndArchive = "<4s4H2LH" +stringEndArchive = "PK\005\006" +sizeEndCentDir = struct.calcsize(structEndArchive) + +_ECD_SIGNATURE = 0 +_ECD_DISK_NUMBER = 1 +_ECD_DISK_START = 2 +_ECD_ENTRIES_THIS_DISK = 3 +_ECD_ENTRIES_TOTAL = 4 +_ECD_SIZE = 5 +_ECD_OFFSET = 6 +_ECD_COMMENT_SIZE = 7 +# These last two indices are not part of the structure as defined in the +# spec, but they are used internally by this module as a convenience +_ECD_COMMENT = 8 +_ECD_LOCATION = 9 + +# The "central directory" structure, magic number, size, and indices +# of entries in the structure (section V.F in the format document) +structCentralDir = "<4s4B4HL2L5H2L" +stringCentralDir = "PK\001\002" +sizeCentralDir = struct.calcsize(structCentralDir) + +# indexes of entries in the central directory structure +_CD_SIGNATURE = 0 +_CD_CREATE_VERSION = 1 +_CD_CREATE_SYSTEM = 2 +_CD_EXTRACT_VERSION = 3 +_CD_EXTRACT_SYSTEM = 4 +_CD_FLAG_BITS = 5 +_CD_COMPRESS_TYPE = 6 +_CD_TIME = 7 +_CD_DATE = 8 +_CD_CRC = 9 +_CD_COMPRESSED_SIZE = 10 +_CD_UNCOMPRESSED_SIZE = 11 +_CD_FILENAME_LENGTH = 12 +_CD_EXTRA_FIELD_LENGTH = 13 +_CD_COMMENT_LENGTH = 14 +_CD_DISK_NUMBER_START = 15 +_CD_INTERNAL_FILE_ATTRIBUTES = 16 +_CD_EXTERNAL_FILE_ATTRIBUTES = 17 +_CD_LOCAL_HEADER_OFFSET = 18 + +# The "local file header" structure, magic number, size, and indices +# (section V.A in the format document) +structFileHeader = "<4s2B4HL2L2H" +stringFileHeader = "PK\003\004" +sizeFileHeader = struct.calcsize(structFileHeader) + +_FH_SIGNATURE = 0 +_FH_EXTRACT_VERSION = 1 +_FH_EXTRACT_SYSTEM = 2 +_FH_GENERAL_PURPOSE_FLAG_BITS = 3 +_FH_COMPRESSION_METHOD = 4 +_FH_LAST_MOD_TIME = 5 +_FH_LAST_MOD_DATE = 6 +_FH_CRC = 7 +_FH_COMPRESSED_SIZE = 8 +_FH_UNCOMPRESSED_SIZE = 9 +_FH_FILENAME_LENGTH = 10 +_FH_EXTRA_FIELD_LENGTH = 11 + +# The "Zip64 end of central directory locator" structure, magic number, and size +structEndArchive64Locator = "<4sLQL" +stringEndArchive64Locator = "PK\x06\x07" +sizeEndCentDir64Locator = struct.calcsize(structEndArchive64Locator) + +# The "Zip64 end of central directory" record, magic number, size, and indices +# (section V.G in the format document) +structEndArchive64 = "<4sQ2H2L4Q" +stringEndArchive64 = "PK\x06\x06" +sizeEndCentDir64 = struct.calcsize(structEndArchive64) + +_CD64_SIGNATURE = 0 +_CD64_DIRECTORY_RECSIZE = 1 +_CD64_CREATE_VERSION = 2 +_CD64_EXTRACT_VERSION = 3 +_CD64_DISK_NUMBER = 4 +_CD64_DISK_NUMBER_START = 5 +_CD64_NUMBER_ENTRIES_THIS_DISK = 6 +_CD64_NUMBER_ENTRIES_TOTAL = 7 +_CD64_DIRECTORY_SIZE = 8 +_CD64_OFFSET_START_CENTDIR = 9 + +def _check_zipfile(fp): + try: + if _EndRecData(fp): + return True # file has correct magic number + except IOError: + pass + return False + +def is_zipfile(filename): + """Quickly see if a file is a ZIP file by checking the magic number. + + The filename argument may be a file or file-like object too. + """ + result = False + try: + if hasattr(filename, "read"): + result = _check_zipfile(fp=filename) + else: + with open(filename, "rb") as fp: + result = _check_zipfile(fp) + except IOError: + pass + return result + +def _EndRecData64(fpin, offset, endrec): + """ + Read the ZIP64 end-of-archive records and use that to update endrec + """ + fpin.seek(offset - sizeEndCentDir64Locator, 2) + data = fpin.read(sizeEndCentDir64Locator) + sig, diskno, reloff, disks = struct.unpack(structEndArchive64Locator, data) + if sig != stringEndArchive64Locator: + return endrec + + if diskno != 0 or disks != 1: + raise BadZipfile("zipfiles that span multiple disks are not supported") + + # Assume no 'zip64 extensible data' + fpin.seek(offset - sizeEndCentDir64Locator - sizeEndCentDir64, 2) + data = fpin.read(sizeEndCentDir64) + sig, sz, create_version, read_version, disk_num, disk_dir, \ + dircount, dircount2, dirsize, diroffset = \ + struct.unpack(structEndArchive64, data) + if sig != stringEndArchive64: + return endrec + + # Update the original endrec using data from the ZIP64 record + endrec[_ECD_SIGNATURE] = sig + endrec[_ECD_DISK_NUMBER] = disk_num + endrec[_ECD_DISK_START] = disk_dir + endrec[_ECD_ENTRIES_THIS_DISK] = dircount + endrec[_ECD_ENTRIES_TOTAL] = dircount2 + endrec[_ECD_SIZE] = dirsize + endrec[_ECD_OFFSET] = diroffset + return endrec + + +def _EndRecData(fpin): + """Return data from the "End of Central Directory" record, or None. + + The data is a list of the nine items in the ZIP "End of central dir" + record followed by a tenth item, the file seek offset of this record.""" + + # Determine file size + fpin.seek(0, 2) + filesize = fpin.tell() + + # Check to see if this is ZIP file with no archive comment (the + # "end of central directory" structure should be the last item in the + # file if this is the case). + try: + fpin.seek(-sizeEndCentDir, 2) + except IOError: + return None + data = fpin.read() + if data[0:4] == stringEndArchive and data[-2:] == "\000\000": + # the signature is correct and there's no comment, unpack structure + endrec = struct.unpack(structEndArchive, data) + endrec=list(endrec) + + # Append a blank comment and record start offset + endrec.append("") + endrec.append(filesize - sizeEndCentDir) + + # Try to read the "Zip64 end of central directory" structure + return _EndRecData64(fpin, -sizeEndCentDir, endrec) + + # Either this is not a ZIP file, or it is a ZIP file with an archive + # comment. Search the end of the file for the "end of central directory" + # record signature. The comment is the last item in the ZIP file and may be + # up to 64K long. It is assumed that the "end of central directory" magic + # number does not appear in the comment. + maxCommentStart = max(filesize - (1 << 16) - sizeEndCentDir, 0) + fpin.seek(maxCommentStart, 0) + data = fpin.read() + start = data.rfind(stringEndArchive) + if start >= 0: + # found the magic number; attempt to unpack and interpret + recData = data[start:start+sizeEndCentDir] + endrec = list(struct.unpack(structEndArchive, recData)) + comment = data[start+sizeEndCentDir:] + # check that comment length is correct + if endrec[_ECD_COMMENT_SIZE] == len(comment): + # Append the archive comment and start offset + endrec.append(comment) + endrec.append(maxCommentStart + start) + + # Try to read the "Zip64 end of central directory" structure + return _EndRecData64(fpin, maxCommentStart + start - filesize, + endrec) + + # Unable to find a valid end of central directory structure + return + + +class ZipInfo (object): + """Class with attributes describing each file in the ZIP archive.""" + + __slots__ = ( + 'orig_filename', + 'filename', + 'date_time', + 'compress_type', + 'comment', + 'extra', + 'create_system', + 'create_version', + 'extract_version', + 'reserved', + 'flag_bits', + 'volume', + 'internal_attr', + 'external_attr', + 'header_offset', + 'CRC', + 'compress_size', + 'file_size', + '_raw_time', + ) + + def __init__(self, filename="NoName", date_time=(1980,1,1,0,0,0)): + self.orig_filename = filename # Original file name in archive + + # Terminate the file name at the first null byte. Null bytes in file + # names are used as tricks by viruses in archives. + null_byte = filename.find(chr(0)) + if null_byte >= 0: + filename = filename[0:null_byte] + # This is used to ensure paths in generated ZIP files always use + # forward slashes as the directory separator, as required by the + # ZIP format specification. + if os.sep != "/" and os.sep in filename: + filename = filename.replace(os.sep, "/") + + self.filename = filename # Normalized file name + self.date_time = date_time # year, month, day, hour, min, sec + # Standard values: + self.compress_type = ZIP_STORED # Type of compression for the file + self.comment = "" # Comment for each file + self.extra = "" # ZIP extra data + if sys.platform == 'win32': + self.create_system = 0 # System which created ZIP archive + else: + # Assume everything else is unix-y + self.create_system = 3 # System which created ZIP archive + self.create_version = 20 # Version which created ZIP archive + self.extract_version = 20 # Version needed to extract archive + self.reserved = 0 # Must be zero + self.flag_bits = 0 # ZIP flag bits + self.volume = 0 # Volume number of file header + self.internal_attr = 0 # Internal attributes + self.external_attr = 0 # External file attributes + # Other attributes are set by class ZipFile: + # header_offset Byte offset to the file header + # CRC CRC-32 of the uncompressed file + # compress_size Size of the compressed file + # file_size Size of the uncompressed file + + def FileHeader(self): + """Return the per-file header as a string.""" + dt = self.date_time + dosdate = (dt[0] - 1980) << 9 | dt[1] << 5 | dt[2] + dostime = dt[3] << 11 | dt[4] << 5 | (dt[5] // 2) + if self.flag_bits & 0x08: + # Set these to zero because we write them after the file data + CRC = compress_size = file_size = 0 + else: + CRC = self.CRC + compress_size = self.compress_size + file_size = self.file_size + + extra = self.extra + + if file_size > ZIP64_LIMIT or compress_size > ZIP64_LIMIT: + # File is larger than what fits into a 4 byte integer, + # fall back to the ZIP64 extension + fmt = '= 24: + counts = unpack('> 1) & 0x7FFFFFFF) ^ poly + else: + crc = ((crc >> 1) & 0x7FFFFFFF) + table[i] = crc + return table + crctable = _GenerateCRCTable() + + def _crc32(self, ch, crc): + """Compute the CRC32 primitive on one byte.""" + return ((crc >> 8) & 0xffffff) ^ self.crctable[(crc ^ ord(ch)) & 0xff] + + def __init__(self, pwd): + self.key0 = 305419896 + self.key1 = 591751049 + self.key2 = 878082192 + for p in pwd: + self._UpdateKeys(p) + + def _UpdateKeys(self, c): + self.key0 = self._crc32(c, self.key0) + self.key1 = (self.key1 + (self.key0 & 255)) & 4294967295 + self.key1 = (self.key1 * 134775813 + 1) & 4294967295 + self.key2 = self._crc32(chr((self.key1 >> 24) & 255), self.key2) + + def __call__(self, c): + """Decrypt a single character.""" + c = ord(c) + k = self.key2 | 2 + c = c ^ (((k * (k^1)) >> 8) & 255) + c = chr(c) + self._UpdateKeys(c) + return c + +class ZipExtFile(io.BufferedIOBase): + """File-like object for reading an archive member. + Is returned by ZipFile.open(). + """ + + # Max size supported by decompressor. + MAX_N = 1 << 31 - 1 + + # Read from compressed files in 4k blocks. + MIN_READ_SIZE = 4096 + + # Search for universal newlines or line chunks. + PATTERN = re.compile(r'^(?P[^\r\n]+)|(?P\n|\r\n?)') + + def __init__(self, fileobj, mode, zipinfo, decrypter=None): + self._fileobj = fileobj + self._decrypter = decrypter + + self._compress_type = zipinfo.compress_type + self._compress_size = zipinfo.compress_size + self._compress_left = zipinfo.compress_size + + if self._compress_type == ZIP_DEFLATED: + self._decompressor = zlib.decompressobj(-15) + self._unconsumed = '' + + self._readbuffer = '' + self._offset = 0 + + self._universal = 'U' in mode + self.newlines = None + + # Adjust read size for encrypted files since the first 12 bytes + # are for the encryption/password information. + if self._decrypter is not None: + self._compress_left -= 12 + + self.mode = mode + self.name = zipinfo.filename + + def readline(self, limit=-1): + """Read and return a line from the stream. + + If limit is specified, at most limit bytes will be read. + """ + + if not self._universal and limit < 0: + # Shortcut common case - newline found in buffer. + i = self._readbuffer.find('\n', self._offset) + 1 + if i > 0: + line = self._readbuffer[self._offset: i] + self._offset = i + return line + + if not self._universal: + return io.BufferedIOBase.readline(self, limit) + + line = '' + while limit < 0 or len(line) < limit: + readahead = self.peek(2) + if readahead == '': + return line + + # + # Search for universal newlines or line chunks. + # + # The pattern returns either a line chunk or a newline, but not + # both. Combined with peek(2), we are assured that the sequence + # '\r\n' is always retrieved completely and never split into + # separate newlines - '\r', '\n' due to coincidental readaheads. + # + match = self.PATTERN.search(readahead) + newline = match.group('newline') + if newline is not None: + if self.newlines is None: + self.newlines = [] + if newline not in self.newlines: + self.newlines.append(newline) + self._offset += len(newline) + return line + '\n' + + chunk = match.group('chunk') + if limit >= 0: + chunk = chunk[: limit - len(line)] + + self._offset += len(chunk) + line += chunk + + return line + + def peek(self, n=1): + """Returns buffered bytes without advancing the position.""" + if n > len(self._readbuffer) - self._offset: + chunk = self.read(n) + self._offset -= len(chunk) + + # Return up to 512 bytes to reduce allocation overhead for tight loops. + return self._readbuffer[self._offset: self._offset + 512] + + def readable(self): + return True + + def read(self, n=-1): + """Read and return up to n bytes. + If the argument is omitted, None, or negative, data is read and returned until EOF is reached.. + """ + + buf = '' + while n < 0 or n is None or n > len(buf): + data = self.read1(n) + if len(data) == 0: + return buf + + buf += data + + return buf + + def read1(self, n): + """Read up to n bytes with at most one read() system call.""" + + # Simplify algorithm (branching) by transforming negative n to large n. + if n < 0 or n is None: + n = self.MAX_N + + # Bytes available in read buffer. + len_readbuffer = len(self._readbuffer) - self._offset + + # Read from file. + if self._compress_left > 0 and n > len_readbuffer + len(self._unconsumed): + nbytes = n - len_readbuffer - len(self._unconsumed) + nbytes = max(nbytes, self.MIN_READ_SIZE) + nbytes = min(nbytes, self._compress_left) + + data = self._fileobj.read(nbytes) + self._compress_left -= len(data) + + if data and self._decrypter is not None: + data = ''.join(map(self._decrypter, data)) + + if self._compress_type == ZIP_STORED: + self._readbuffer = self._readbuffer[self._offset:] + data + self._offset = 0 + else: + # Prepare deflated bytes for decompression. + self._unconsumed += data + + # Handle unconsumed data. + if (len(self._unconsumed) > 0 and n > len_readbuffer and + self._compress_type == ZIP_DEFLATED): + data = self._decompressor.decompress( + self._unconsumed, + max(n - len_readbuffer, self.MIN_READ_SIZE) + ) + + self._unconsumed = self._decompressor.unconsumed_tail + if len(self._unconsumed) == 0 and self._compress_left == 0: + data += self._decompressor.flush() + + self._readbuffer = self._readbuffer[self._offset:] + data + self._offset = 0 + + # Read from buffer. + data = self._readbuffer[self._offset: self._offset + n] + self._offset += len(data) + return data + + + +class ZipFile: + """ Class with methods to open, read, write, close, list zip files. + + z = ZipFile(file, mode="r", compression=ZIP_STORED, allowZip64=False) + + file: Either the path to the file, or a file-like object. + If it is a path, the file will be opened and closed by ZipFile. + mode: The mode can be either read "r", write "w" or append "a". + compression: ZIP_STORED (no compression) or ZIP_DEFLATED (requires zlib). + allowZip64: if True ZipFile will create files with ZIP64 extensions when + needed, otherwise it will raise an exception when this would + be necessary. + + """ + + fp = None # Set here since __del__ checks it + + def __init__(self, file, mode="r", compression=ZIP_STORED, allowZip64=False): + """Open the ZIP file with mode read "r", write "w" or append "a".""" + if mode not in ("r", "w", "a"): + raise RuntimeError('ZipFile() requires mode "r", "w", or "a"') + + if compression == ZIP_STORED: pass - return userkeys + elif compression == ZIP_DEFLATED: + if not zlib: + raise RuntimeError,\ + "Compression requires the (missing) zlib module" + else: + raise RuntimeError, "That compression method is not supported" + + self._allowZip64 = allowZip64 + self._didModify = False + self.debug = 0 # Level of printing: 0 through 3 + self.NameToInfo = {} # Find file info given name + self.filelist = [] # List of ZipInfo instances for archive + self.compression = compression # Method of compression + self.mode = key = mode.replace('b', '')[0] + self.pwd = None + self.comment = '' + + # Check if we were passed a file-like object + if isinstance(file, basestring): + self._filePassed = 0 + self.filename = file + modeDict = {'r' : 'rb', 'w': 'wb', 'a' : 'r+b'} + try: + self.fp = open(file, modeDict[mode]) + except IOError: + if mode == 'a': + mode = key = 'w' + self.fp = open(file, modeDict[mode]) + else: + raise + else: + self._filePassed = 1 + self.fp = file + self.filename = getattr(file, 'name', None) + + if key == 'r': + self._GetContents() + elif key == 'w': + pass + elif key == 'a': + try: # See if file is a zip file + self._RealGetContents() + # seek to start of directory and overwrite + self.fp.seek(self.start_dir, 0) + except BadZipfile: # file is not a zip file, just append + self.fp.seek(0, 2) + else: + if not self._filePassed: + self.fp.close() + self.fp = None + raise RuntimeError, 'Mode must be "r", "w" or "a"' + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() + + def _GetContents(self): + """Read the directory, making sure we close the file if the format + is bad.""" + try: + self._RealGetContents() + except BadZipfile: + if not self._filePassed: + self.fp.close() + self.fp = None + raise + + def _RealGetContents(self): + """Read in the table of contents for the ZIP file.""" + fp = self.fp + endrec = _EndRecData(fp) + if not endrec: + raise BadZipfile, "File is not a zip file" + if self.debug > 1: + print endrec + size_cd = endrec[_ECD_SIZE] # bytes in central directory + offset_cd = endrec[_ECD_OFFSET] # offset of central directory + self.comment = endrec[_ECD_COMMENT] # archive comment + + # "concat" is zero, unless zip was concatenated to another file + concat = endrec[_ECD_LOCATION] - size_cd - offset_cd + if endrec[_ECD_SIGNATURE] == stringEndArchive64: + # If Zip64 extension structures are present, account for them + concat -= (sizeEndCentDir64 + sizeEndCentDir64Locator) + + if self.debug > 2: + inferred = concat + offset_cd + print "given, inferred, offset", offset_cd, inferred, concat + # self.start_dir: Position of start of central directory + self.start_dir = offset_cd + concat + fp.seek(self.start_dir, 0) + data = fp.read(size_cd) + fp = cStringIO.StringIO(data) + total = 0 + while total < size_cd: + centdir = fp.read(sizeCentralDir) + if centdir[0:4] != stringCentralDir: + raise BadZipfile, "Bad magic number for central directory" + centdir = struct.unpack(structCentralDir, centdir) + if self.debug > 2: + print centdir + filename = fp.read(centdir[_CD_FILENAME_LENGTH]) + # Create ZipInfo instance to store file information + x = ZipInfo(filename) + x.extra = fp.read(centdir[_CD_EXTRA_FIELD_LENGTH]) + x.comment = fp.read(centdir[_CD_COMMENT_LENGTH]) + x.header_offset = centdir[_CD_LOCAL_HEADER_OFFSET] + (x.create_version, x.create_system, x.extract_version, x.reserved, + x.flag_bits, x.compress_type, t, d, + x.CRC, x.compress_size, x.file_size) = centdir[1:12] + x.volume, x.internal_attr, x.external_attr = centdir[15:18] + # Convert date/time code to (year, month, day, hour, min, sec) + x._raw_time = t + x.date_time = ( (d>>9)+1980, (d>>5)&0xF, d&0x1F, + t>>11, (t>>5)&0x3F, (t&0x1F) * 2 ) + + x._decodeExtra() + x.header_offset = x.header_offset + concat + x.filename = x._decodeFilename() + self.filelist.append(x) + self.NameToInfo[x.filename] = x + + # update total bytes read from central directory + total = (total + sizeCentralDir + centdir[_CD_FILENAME_LENGTH] + + centdir[_CD_EXTRA_FIELD_LENGTH] + + centdir[_CD_COMMENT_LENGTH]) + + if self.debug > 2: + print "total", total + + + def namelist(self): + """Return a list of file names in the archive.""" + l = [] + for data in self.filelist: + l.append(data.filename) + return l + + def infolist(self): + """Return a list of class ZipInfo instances for files in the + archive.""" + return self.filelist + + def printdir(self): + """Print a table of contents for the zip file.""" + print "%-46s %19s %12s" % ("File Name", "Modified ", "Size") + for zinfo in self.filelist: + date = "%d-%02d-%02d %02d:%02d:%02d" % zinfo.date_time[:6] + print "%-46s %s %12d" % (zinfo.filename, date, zinfo.file_size) + + def testzip(self): + """Read all the files and check the CRC.""" + chunk_size = 2 ** 20 + for zinfo in self.filelist: + try: + # Read by chunks, to avoid an OverflowError or a + # MemoryError with very large embedded files. + f = self.open(zinfo.filename, "r") + while f.read(chunk_size): # Check CRC-32 + pass + except BadZipfile: + return zinfo.filename + + def getinfo(self, name): + """Return the instance of ZipInfo given 'name'.""" + info = self.NameToInfo.get(name) + if info is None: + raise KeyError( + 'There is no item named %r in the archive' % name) + + return info + + def setpassword(self, pwd): + """Set default password for encrypted files.""" + self.pwd = pwd + + def read(self, name, pwd=None): + """Return file bytes (as a string) for name.""" + return self.open(name, "r", pwd).read() + + def open(self, name, mode="r", pwd=None): + """Return file-like object for 'name'.""" + if mode not in ("r", "U", "rU"): + raise RuntimeError, 'open() requires mode "r", "U", or "rU"' + if not self.fp: + raise RuntimeError, \ + "Attempt to read ZIP archive that was already closed" + + # Only open a new file for instances where we were not + # given a file object in the constructor + if self._filePassed: + zef_file = self.fp + else: + zef_file = open(self.filename, 'rb') + + # Make sure we have an info object + if isinstance(name, ZipInfo): + # 'name' is already an info object + zinfo = name + else: + # Get info object for name + zinfo = self.getinfo(name) + + zef_file.seek(zinfo.header_offset, 0) + + # Skip the file header: + fheader = zef_file.read(sizeFileHeader) + if fheader[0:4] != stringFileHeader: + raise BadZipfile, "Bad magic number for file header" + + fheader = struct.unpack(structFileHeader, fheader) + fname = zef_file.read(fheader[_FH_FILENAME_LENGTH]) + if fheader[_FH_EXTRA_FIELD_LENGTH]: + zef_file.read(fheader[_FH_EXTRA_FIELD_LENGTH]) + + if fname != zinfo.orig_filename: + raise BadZipfile, \ + 'File name in directory "%s" and header "%s" differ.' % ( + zinfo.orig_filename, fname) + + # check for encrypted flag & handle password + is_encrypted = zinfo.flag_bits & 0x1 + zd = None + if is_encrypted: + if not pwd: + pwd = self.pwd + if not pwd: + raise RuntimeError, "File %s is encrypted, " \ + "password required for extraction" % name + + zd = _ZipDecrypter(pwd) + # The first 12 bytes in the cypher stream is an encryption header + # used to strengthen the algorithm. The first 11 bytes are + # completely random, while the 12th contains the MSB of the CRC, + # or the MSB of the file time depending on the header type + # and is used to check the correctness of the password. + bytes = zef_file.read(12) + h = map(zd, bytes[0:12]) + if zinfo.flag_bits & 0x8: + # compare against the file type from extended local headers + check_byte = (zinfo._raw_time >> 8) & 0xff + else: + # compare against the CRC otherwise + check_byte = (zinfo.CRC >> 24) & 0xff + if ord(h[11]) != check_byte: + raise RuntimeError("Bad password for file", name) + + return ZipExtFile(zef_file, mode, zinfo, zd) + + def extract(self, member, path=None, pwd=None): + """Extract a member from the archive to the current working directory, + using its full name. Its file information is extracted as accurately + as possible. `member' may be a filename or a ZipInfo object. You can + specify a different directory using `path'. + """ + if not isinstance(member, ZipInfo): + member = self.getinfo(member) + + if path is None: + path = os.getcwd() + + return self._extract_member(member, path, pwd) + + def extractall(self, path=None, members=None, pwd=None): + """Extract all members from the archive to the current working + directory. `path' specifies a different directory to extract to. + `members' is optional and must be a subset of the list returned + by namelist(). + """ + if members is None: + members = self.namelist() + + for zipinfo in members: + self.extract(zipinfo, path, pwd) + + def _extract_member(self, member, targetpath, pwd): + """Extract the ZipInfo object 'member' to a physical + file on the path targetpath. + """ + # build the destination pathname, replacing + # forward slashes to platform specific separators. + # Strip trailing path separator, unless it represents the root. + if (targetpath[-1:] in (os.path.sep, os.path.altsep) + and len(os.path.splitdrive(targetpath)[1]) > 1): + targetpath = targetpath[:-1] + + # don't include leading "/" from file name if present + if member.filename[0] == '/': + targetpath = os.path.join(targetpath, member.filename[1:]) + else: + targetpath = os.path.join(targetpath, member.filename) + + targetpath = os.path.normpath(targetpath) + + # Create all upper directories if necessary. + upperdirs = os.path.dirname(targetpath) + if upperdirs and not os.path.exists(upperdirs): + os.makedirs(upperdirs) + + if member.filename[-1] == '/': + if not os.path.isdir(targetpath): + os.mkdir(targetpath) + return targetpath + + source = self.open(member, pwd=pwd) + target = file(targetpath, "wb") + shutil.copyfileobj(source, target) + source.close() + target.close() + + return targetpath + + def _writecheck(self, zinfo): + """Check for errors before writing a file to the archive.""" + if zinfo.filename in self.NameToInfo: + if self.debug: # Warning for duplicate names + print "Duplicate name:", zinfo.filename + if self.mode not in ("w", "a"): + raise RuntimeError, 'write() requires mode "w" or "a"' + if not self.fp: + raise RuntimeError, \ + "Attempt to write ZIP archive that was already closed" + if zinfo.compress_type == ZIP_DEFLATED and not zlib: + raise RuntimeError, \ + "Compression requires the (missing) zlib module" + if zinfo.compress_type not in (ZIP_STORED, ZIP_DEFLATED): + raise RuntimeError, \ + "That compression method is not supported" + if zinfo.file_size > ZIP64_LIMIT: + if not self._allowZip64: + raise LargeZipFile("Filesize would require ZIP64 extensions") + if zinfo.header_offset > ZIP64_LIMIT: + if not self._allowZip64: + raise LargeZipFile("Zipfile size would require ZIP64 extensions") + + def write(self, filename, arcname=None, compress_type=None): + """Put the bytes from filename into the archive under the name + arcname.""" + if not self.fp: + raise RuntimeError( + "Attempt to write to ZIP archive that was already closed") + + st = os.stat(filename) + isdir = stat.S_ISDIR(st.st_mode) + mtime = time.localtime(st.st_mtime) + date_time = mtime[0:6] + # Create ZipInfo instance to store file information + if arcname is None: + arcname = filename + arcname = os.path.normpath(os.path.splitdrive(arcname)[1]) + while arcname[0] in (os.sep, os.altsep): + arcname = arcname[1:] + if isdir: + arcname += '/' + zinfo = ZipInfo(arcname, date_time) + zinfo.external_attr = (st[0] & 0xFFFF) << 16L # Unix attributes + if compress_type is None: + zinfo.compress_type = self.compression + else: + zinfo.compress_type = compress_type + + zinfo.file_size = st.st_size + zinfo.flag_bits = 0x00 + zinfo.header_offset = self.fp.tell() # Start of header bytes + + self._writecheck(zinfo) + self._didModify = True + + if isdir: + zinfo.file_size = 0 + zinfo.compress_size = 0 + zinfo.CRC = 0 + self.filelist.append(zinfo) + self.NameToInfo[zinfo.filename] = zinfo + self.fp.write(zinfo.FileHeader()) + return + + with open(filename, "rb") as fp: + # Must overwrite CRC and sizes with correct data later + zinfo.CRC = CRC = 0 + zinfo.compress_size = compress_size = 0 + zinfo.file_size = file_size = 0 + self.fp.write(zinfo.FileHeader()) + if zinfo.compress_type == ZIP_DEFLATED: + cmpr = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, + zlib.DEFLATED, -15) + else: + cmpr = None + while 1: + buf = fp.read(1024 * 8) + if not buf: + break + file_size = file_size + len(buf) + CRC = crc32(buf, CRC) & 0xffffffff + if cmpr: + buf = cmpr.compress(buf) + compress_size = compress_size + len(buf) + self.fp.write(buf) + if cmpr: + buf = cmpr.flush() + compress_size = compress_size + len(buf) + self.fp.write(buf) + zinfo.compress_size = compress_size + else: + zinfo.compress_size = file_size + zinfo.CRC = CRC + zinfo.file_size = file_size + # Seek backwards and write CRC and file sizes + position = self.fp.tell() # Preserve current position in file + self.fp.seek(zinfo.header_offset + 14, 0) + self.fp.write(struct.pack(" ZIP64_LIMIT \ + or zinfo.compress_size > ZIP64_LIMIT: + extra.append(zinfo.file_size) + extra.append(zinfo.compress_size) + file_size = 0xffffffff + compress_size = 0xffffffff + else: + file_size = zinfo.file_size + compress_size = zinfo.compress_size + + if zinfo.header_offset > ZIP64_LIMIT: + extra.append(zinfo.header_offset) + header_offset = 0xffffffffL + else: + header_offset = zinfo.header_offset + + extra_data = zinfo.extra + if extra: + # Append a ZIP64 field to the extra's + extra_data = struct.pack( + '>sys.stderr, (structCentralDir, + stringCentralDir, create_version, + zinfo.create_system, extract_version, zinfo.reserved, + zinfo.flag_bits, zinfo.compress_type, dostime, dosdate, + zinfo.CRC, compress_size, file_size, + len(zinfo.filename), len(extra_data), len(zinfo.comment), + 0, zinfo.internal_attr, zinfo.external_attr, + header_offset) + raise + self.fp.write(centdir) + self.fp.write(filename) + self.fp.write(extra_data) + self.fp.write(zinfo.comment) + + pos2 = self.fp.tell() + # Write end-of-zip-archive record + centDirCount = count + centDirSize = pos2 - pos1 + centDirOffset = pos1 + if (centDirCount >= ZIP_FILECOUNT_LIMIT or + centDirOffset > ZIP64_LIMIT or + centDirSize > ZIP64_LIMIT): + # Need to write the ZIP64 end-of-archive records + zip64endrec = struct.pack( + structEndArchive64, stringEndArchive64, + 44, 45, 45, 0, 0, centDirCount, centDirCount, + centDirSize, centDirOffset) + self.fp.write(zip64endrec) + + zip64locrec = struct.pack( + structEndArchive64Locator, + stringEndArchive64Locator, 0, pos2, 1) + self.fp.write(zip64locrec) + centDirCount = min(centDirCount, 0xFFFF) + centDirSize = min(centDirSize, 0xFFFFFFFF) + centDirOffset = min(centDirOffset, 0xFFFFFFFF) + + # check for valid comment length + if len(self.comment) >= ZIP_MAX_COMMENT: + if self.debug > 0: + msg = 'Archive comment is too long; truncating to %d bytes' \ + % ZIP_MAX_COMMENT + self.comment = self.comment[:ZIP_MAX_COMMENT] + + endrec = struct.pack(structEndArchive, stringEndArchive, + 0, 0, centDirCount, centDirCount, + centDirSize, centDirOffset, len(self.comment)) + self.fp.write(endrec) + self.fp.write(self.comment) + self.fp.flush() + + if not self._filePassed: + self.fp.close() + self.fp = None + + +class PyZipFile(ZipFile): + """Class to create ZIP archives with Python library files and packages.""" + + def writepy(self, pathname, basename = ""): + """Add all files from "pathname" to the ZIP archive. + + If pathname is a package directory, search the directory and + all package subdirectories recursively for all *.py and enter + the modules into the archive. If pathname is a plain + directory, listdir *.py and enter all modules. Else, pathname + must be a Python *.py file and the module will be put into the + archive. Added modules are always module.pyo or module.pyc. + This method will compile the module.py into module.pyc if + necessary. + """ + dir, name = os.path.split(pathname) + if os.path.isdir(pathname): + initname = os.path.join(pathname, "__init__.py") + if os.path.isfile(initname): + # This is a package directory, add it + if basename: + basename = "%s/%s" % (basename, name) + else: + basename = name + if self.debug: + print "Adding package in", pathname, "as", basename + fname, arcname = self._get_codename(initname[0:-3], basename) + if self.debug: + print "Adding", arcname + self.write(fname, arcname) + dirlist = os.listdir(pathname) + dirlist.remove("__init__.py") + # Add all *.py files and package subdirectories + for filename in dirlist: + path = os.path.join(pathname, filename) + root, ext = os.path.splitext(filename) + if os.path.isdir(path): + if os.path.isfile(os.path.join(path, "__init__.py")): + # This is a package directory, add it + self.writepy(path, basename) # Recursive call + elif ext == ".py": + fname, arcname = self._get_codename(path[0:-3], + basename) + if self.debug: + print "Adding", arcname + self.write(fname, arcname) + else: + # This is NOT a package directory, add its files at top level + if self.debug: + print "Adding files from directory", pathname + for filename in os.listdir(pathname): + path = os.path.join(pathname, filename) + root, ext = os.path.splitext(filename) + if ext == ".py": + fname, arcname = self._get_codename(path[0:-3], + basename) + if self.debug: + print "Adding", arcname + self.write(fname, arcname) + else: + if pathname[-3:] != ".py": + raise RuntimeError, \ + 'Files added with writepy() must end with ".py"' + fname, arcname = self._get_codename(pathname[0:-3], basename) + if self.debug: + print "Adding file", arcname + self.write(fname, arcname) + + def _get_codename(self, pathname, basename): + """Return (filename, archivename) for the path. + + Given a module name path, return the correct file path and + archive name, compiling if necessary. For example, given + /python/lib/string, return (/python/lib/string.pyc, string). + """ + file_py = pathname + ".py" + file_pyc = pathname + ".pyc" + file_pyo = pathname + ".pyo" + if os.path.isfile(file_pyo) and \ + os.stat(file_pyo).st_mtime >= os.stat(file_py).st_mtime: + fname = file_pyo # Use .pyo file + elif not os.path.isfile(file_pyc) or \ + os.stat(file_pyc).st_mtime < os.stat(file_py).st_mtime: + import py_compile + if self.debug: + print "Compiling", file_py + try: + py_compile.compile(file_py, file_pyc, None, True) + except py_compile.PyCompileError,err: + print err.msg + fname = file_pyc + else: + fname = file_pyc + archivename = os.path.split(fname)[1] + if basename: + archivename = "%s/%s" % (basename, archivename) + return (fname, archivename) + + +def main(args = None): + import textwrap + USAGE=textwrap.dedent("""\ + Usage: + zipfile.py -l zipfile.zip # Show listing of a zipfile + zipfile.py -t zipfile.zip # Test if a zipfile is valid + zipfile.py -e zipfile.zip target # Extract zipfile into target dir + zipfile.py -c zipfile.zip src ... # Create zipfile from sources + """) + if args is None: + args = sys.argv[1:] + + if not args or args[0] not in ('-l', '-c', '-e', '-t'): + print USAGE + sys.exit(1) + + if args[0] == '-l': + if len(args) != 2: + print USAGE + sys.exit(1) + zf = ZipFile(args[1], 'r') + zf.printdir() + zf.close() + + elif args[0] == '-t': + if len(args) != 2: + print USAGE + sys.exit(1) + zf = ZipFile(args[1], 'r') + zf.testzip() + print "Done testing" + + elif args[0] == '-e': + if len(args) != 3: + print USAGE + sys.exit(1) + + zf = ZipFile(args[1], 'r') + out = args[2] + for path in zf.namelist(): + if path.startswith('./'): + tgt = os.path.join(out, path[2:]) + else: + tgt = os.path.join(out, path) + + tgtdir = os.path.dirname(tgt) + if not os.path.exists(tgtdir): + os.makedirs(tgtdir) + with open(tgt, 'wb') as fp: + fp.write(zf.read(path)) + zf.close() + + elif args[0] == '-c': + if len(args) < 3: + print USAGE + sys.exit(1) + + def addToZip(zf, path, zippath): + if os.path.isfile(path): + zf.write(path, zippath, ZIP_DEFLATED) + elif os.path.isdir(path): + for nm in os.listdir(path): + addToZip(zf, + os.path.join(path, nm), os.path.join(zippath, nm)) + # else: ignore + + zf = ZipFile(args[1], 'w', allowZip64=True) + for src in args[2:]: + addToZip(zf, src, os.path.basename(src)) + + zf.close() + +if __name__ == "__main__": + main() diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/zipfix.py b/DeDRM_calibre_plugin/DeDRM_plugin/zipfix.py index b54db80..8ddfae3 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/zipfix.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/zipfix.py @@ -1,60 +1,188 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from __future__ import with_statement +# zipfix.py, version 1.1 +# Copyright © 2010-2013 by some_updates, DiapDealer and Apprentice Alf + +# Released under the terms of the GNU General Public Licence, version 3 +# + +# Revision history: +# 1.0 - Initial release +# 1.1 - Updated to handle zip file metadata correctly + +""" +Re-write zip (or ePub) fixing problems with file names (and mimetype entry). +""" __license__ = 'GPL v3' +__version__ = "1.1" + +import sys +import zlib +import zipfilerugged +import os +import os.path +import getopt +from struct import unpack + + +_FILENAME_LEN_OFFSET = 26 +_EXTRA_LEN_OFFSET = 28 +_FILENAME_OFFSET = 30 +_MAX_SIZE = 64 * 1024 +_MIMETYPE = 'application/epub+zip' + +class ZipInfo(zipfilerugged.ZipInfo): + def __init__(self, *args, **kwargs): + if 'compress_type' in kwargs: + compress_type = kwargs.pop('compress_type') + super(ZipInfo, self).__init__(*args, **kwargs) + self.compress_type = compress_type + +class fixZip: + def __init__(self, zinput, zoutput): + self.ztype = 'zip' + if zinput.lower().find('.epub') >= 0 : + self.ztype = 'epub' + self.inzip = zipfilerugged.ZipFile(zinput,'r') + self.outzip = zipfilerugged.ZipFile(zoutput,'w') + # open the input zip for reading only as a raw file + self.bzf = file(zinput,'rb') + + def getlocalname(self, zi): + local_header_offset = zi.header_offset + self.bzf.seek(local_header_offset + _FILENAME_LEN_OFFSET) + leninfo = self.bzf.read(2) + local_name_length, = unpack(' 0: + if len(cmpdata) > _MAX_SIZE : + newdata = cmpdata[0:_MAX_SIZE] + cmpdata = cmpdata[_MAX_SIZE:] + else: + newdata = cmpdata + cmpdata = '' + newdata = dc.decompress(newdata) + unprocessed = dc.unconsumed_tail + if len(unprocessed) == 0: + newdata += dc.flush() + data += newdata + cmpdata += unprocessed + unprocessed = '' + return data + + def getfiledata(self, zi): + # get file name length and exta data length to find start of file data + local_header_offset = zi.header_offset + + self.bzf.seek(local_header_offset + _FILENAME_LEN_OFFSET) + leninfo = self.bzf.read(2) + local_name_length, = unpack('