From 07e532f59c584b62a1fe34020a2ccbcd7ad12223 Mon Sep 17 00:00:00 2001 From: Apprentice Alf Date: Tue, 6 Mar 2012 18:24:28 +0000 Subject: [PATCH] tools v5.0 Introduction of alfcrypto library for speed Reorganisation of archive plugins,apps,other --- .../K4MobiDeDRM_plugin/__init__.py | 64 +- .../K4MobiDeDRM_plugin/convert2xml.py | 1324 +++++++--------- .../K4MobiDeDRM_plugin/flatxml2html.py | Bin 29346 -> 70144 bytes .../K4MobiDeDRM_plugin/flatxml2svg.py | 522 ++++--- Calibre_Plugins/K4MobiDeDRM_plugin/genbook.py | Bin 24295 -> 52224 bytes .../K4MobiDeDRM_plugin/k4mobidedrm_orig.py | Bin 6315 -> 17393 bytes .../K4MobiDeDRM_plugin/kgenpids.py | 1009 ++++++++++--- .../plugin-import-name-k4mobidedrm.txt | 726 +++++++++ .../K4MobiDeDRM_plugin/stylexml2css.py | 894 ++++++++--- .../K4MobiDeDRM_plugin/topazextract.py | 527 ++----- Calibre_Plugins/k4mobidedrm_plugin.zip | Bin 51886 -> 215853 bytes .../k4mobidedrm_plugin/k4mutils.py | 1340 +++++++++++------ .../k4mobidedrm_plugin/k4pcutils.py | 1057 +++++++++---- .../k4mobidedrm_plugin/mobidedrm.py | 643 +++----- .../DeDRM.app/Contents/Info.plist | 8 +- .../Contents/Resources/Scripts/main.scpt | Bin 236044 -> 236148 bytes .../DeDRM.app/Contents/Resources/aescbc.py | 568 +++++++ .../DeDRM.app/Contents/Resources/alfcrypto.py | 290 ++++ .../Contents/Resources/convert2xml.py | 116 +- .../Contents/Resources/encodebase64.py | 7 +- .../DeDRM.app/Contents/Resources/erdr2pml.py | 35 +- .../Contents/Resources/flatxml2html.py | 77 +- .../Contents/Resources/flatxml2svg.py | 11 +- .../DeDRM.app/Contents/Resources/genbook.py | 38 +- .../Contents/Resources/ignobleepub.py | 14 +- .../Contents/Resources/ignoblekeygen.py | 12 +- .../DeDRM.app/Contents/Resources/ineptepub.py | 44 +- .../DeDRM.app/Contents/Resources/ineptkey.py | 8 +- .../DeDRM.app/Contents/Resources/ineptpdf.py | 190 +-- .../Contents/Resources/k4mdumpkinfo.py | 50 +- .../Contents/Resources/k4mobidedrm.py | 35 +- .../DeDRM.app/Contents/Resources/k4mutils.py | 343 +++-- .../DeDRM.app/Contents/Resources/k4pcutils.py | 183 ++- .../DeDRM.app/Contents/Resources/kgenpids.py | 30 +- .../Contents/Resources/libalfcrypto src.zip | Bin 0 -> 19299 bytes .../Contents/Resources/libalfcrypto.dylib | Bin 0 -> 87160 bytes .../DeDRM.app/Contents/Resources/mobidedrm.py | 92 +- .../Contents/Resources/mobidedrm_orig.py | 444 ++++++ .../Contents/Resources/openssl_des.py | 7 +- .../DeDRM.app/Contents/Resources/pbkdf2.py | 68 + .../Contents/Resources/stylexml2css.py | 38 +- .../Contents/Resources/subasyncio.py | 5 +- .../Contents/Resources/topazextract.py | 79 +- .../DeDRM.app/Contents/Resources/zipfix.py | 12 +- .../ReadMe_DeDRM.app.rtf | 48 + .../DeDRM_WinApp/DeDRM_lib/DeDRM_app.pyw | 18 +- .../DeDRM_WinApp/DeDRM_lib/lib/activitybar.py | 6 +- .../DeDRM_WinApp/DeDRM_lib/lib/aescbc.py | 568 +++++++ .../DeDRM_WinApp/DeDRM_lib/lib/alfcrypto.dll | Bin 0 -> 70144 bytes .../DeDRM_WinApp/DeDRM_lib/lib/alfcrypto.exp | Bin 0 -> 1227 bytes .../DeDRM_WinApp/DeDRM_lib/lib/alfcrypto.py | 290 ++++ .../DeDRM_lib/lib/alfcrypto64.dll | Bin 0 -> 52224 bytes .../DeDRM_lib/lib/alfcrypto_src.zip | Bin 0 -> 17393 bytes .../DeDRM_WinApp/DeDRM_lib/lib/convert2xml.py | 167 +- .../DeDRM_WinApp/DeDRM_lib/lib/decryptepub.py | 1 - .../DeDRM_WinApp/DeDRM_lib/lib/decryptpdb.py | 1 - .../DeDRM_WinApp/DeDRM_lib/lib/decryptpdf.py | 5 +- .../DeDRM_WinApp/DeDRM_lib/lib/erdr2pml.py | 35 +- .../DeDRM_lib/lib/flatxml2html.py | 124 +- .../DeDRM_WinApp/DeDRM_lib/lib/flatxml2svg.py | 97 +- .../DeDRM_WinApp/DeDRM_lib/lib/genbook.py | 239 +-- .../DeDRM_WinApp/DeDRM_lib/lib/ignobleepub.py | 14 +- .../DeDRM_lib/lib/ignoblekeygen.py | 12 +- .../DeDRM_WinApp/DeDRM_lib/lib/ineptepub.py | 44 +- .../DeDRM_WinApp/DeDRM_lib/lib/ineptkey.py | 8 +- .../DeDRM_WinApp/DeDRM_lib/lib/ineptpdf.py | 191 +-- .../DeDRM_WinApp/DeDRM_lib/lib/k4mobidedrm.py | 46 +- .../DeDRM_WinApp/DeDRM_lib/lib/k4mutils.py | 343 +++-- .../DeDRM_WinApp/DeDRM_lib/lib/k4pcutils.py | 179 ++- .../DeDRM_WinApp/DeDRM_lib/lib/kgenpids.py | 30 +- .../DeDRM_WinApp/DeDRM_lib/lib/mobidedrm.py | 92 +- .../DeDRM_WinApp/DeDRM_lib/lib/openssl_des.py | 7 +- .../DeDRM_WinApp/DeDRM_lib/lib/pbkdf2.py | 68 + .../DeDRM_lib/lib/pycrypto_des.py | 1 - .../DeDRM_WinApp/DeDRM_lib/lib/python_des.py | 24 +- .../DeDRM_WinApp/DeDRM_lib/lib/simpleprefs.py | 3 +- .../DeDRM_lib/lib/stylexml2css.py | 38 +- .../DeDRM_WinApp/DeDRM_lib/lib/subasyncio.py | 5 +- .../DeDRM_lib/lib/topazextract.py | 94 +- .../DeDRM_WinApp/DeDRM_lib/lib/zipfix.py | 12 +- .../ReadMe_DeDRM_WinApp.txt | 17 +- .../Additional_Tools/FindTopazEbooks.pyw | 30 +- Other_Tools/Additional_Tools/lib/mobidedrm.py | 13 +- Other_Tools/Adobe_PDF_Tools/ineptkey.pyw | 8 +- Other_Tools/Adobe_PDF_Tools/ineptpdf.pyw | 191 +-- Other_Tools/Adobe_ePub_Tools/ineptepub.pyw | 44 +- Other_Tools/Adobe_ePub_Tools/ineptkey.pyw | 8 +- .../ignobleepub.pyw | 14 +- .../ignoblekeygen.pyw | 12 +- Other_Tools/KindleBooks/lib/aescbc.py | 568 +++++++ Other_Tools/KindleBooks/lib/alfcrypto.dll | Bin 0 -> 70144 bytes Other_Tools/KindleBooks/lib/alfcrypto.py | 290 ++++ Other_Tools/KindleBooks/lib/alfcrypto64.dll | Bin 0 -> 52224 bytes Other_Tools/KindleBooks/lib/alfcrypto_src.zip | Bin 0 -> 17393 bytes Other_Tools/KindleBooks/lib/convert2xml.py | 167 +- Other_Tools/KindleBooks/lib/flatxml2html.py | 124 +- Other_Tools/KindleBooks/lib/flatxml2svg.py | 97 +- Other_Tools/KindleBooks/lib/genbook.py | 239 +-- Other_Tools/KindleBooks/lib/k4mobidedrm.py | 46 +- Other_Tools/KindleBooks/lib/k4mutils.py | 343 +++-- Other_Tools/KindleBooks/lib/k4pcutils.py | 179 ++- .../KindleBooks/lib/libalfcrypto.dylib | Bin 0 -> 87160 bytes Other_Tools/KindleBooks/lib/mobidedrm.py | 92 +- Other_Tools/KindleBooks/lib/stylexml2css.py | 38 +- Other_Tools/KindleBooks/lib/subasyncio.py | 5 +- Other_Tools/KindleBooks/lib/topazextract.py | 94 +- ...EADME_Kindle_for_iPad_iPhone_iPodTouch.txt | 33 +- Other_Tools/ePub_Fixer/lib/zipfix.py | 12 +- Other_Tools/eReader_PDB_Tools/lib/erdr2pml.py | 35 +- .../eReader_PDB_Tools/lib/openssl_des.py | 7 +- .../eReader_PDB_Tools/lib/pycrypto_des.py | 1 - ReadMe_First.txt | 222 ++- 112 files changed, 11472 insertions(+), 5177 deletions(-) create mode 100644 DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/aescbc.py create mode 100644 DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/alfcrypto.py create mode 100644 DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/libalfcrypto src.zip create mode 100644 DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/libalfcrypto.dylib create mode 100644 DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm_orig.py create mode 100644 DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/pbkdf2.py create mode 100644 DeDRM_Macintosh_Application/ReadMe_DeDRM.app.rtf create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/aescbc.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/alfcrypto.dll create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/alfcrypto.exp create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/alfcrypto.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/alfcrypto64.dll create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/alfcrypto_src.zip create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/pbkdf2.py create mode 100644 Other_Tools/KindleBooks/lib/aescbc.py create mode 100644 Other_Tools/KindleBooks/lib/alfcrypto.dll create mode 100644 Other_Tools/KindleBooks/lib/alfcrypto.py create mode 100644 Other_Tools/KindleBooks/lib/alfcrypto64.dll create mode 100644 Other_Tools/KindleBooks/lib/alfcrypto_src.zip create mode 100644 Other_Tools/KindleBooks/lib/libalfcrypto.dylib diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/__init__.py b/Calibre_Plugins/K4MobiDeDRM_plugin/__init__.py index 30c1e13..233b462 100644 --- a/Calibre_Plugins/K4MobiDeDRM_plugin/__init__.py +++ b/Calibre_Plugins/K4MobiDeDRM_plugin/__init__.py @@ -4,28 +4,62 @@ from __future__ import with_statement from calibre.customize import FileTypePlugin from calibre.gui2 import is_ok_to_use_qt +from calibre.utils.config import config_dir +from calibre.constants import iswindows, isosx # from calibre.ptempfile import PersistentTemporaryDirectory -from calibre_plugins.k4mobidedrm import kgenpids -from calibre_plugins.k4mobidedrm import topazextract -from calibre_plugins.k4mobidedrm import mobidedrm import sys import os import re +from zipfile import ZipFile class K4DeDRM(FileTypePlugin): name = 'K4PC, K4Mac, Kindle Mobi and Topaz DeDRM' # Name of the plugin description = 'Removes DRM from Mobipocket, Kindle/Mobi, Kindle/Topaz and Kindle/Print Replica files. Provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc.' supported_platforms = ['osx', 'windows', 'linux'] # Platforms this plugin will run on author = 'DiapDealer, SomeUpdates' # The author of this plugin - version = (0, 3, 8) # The version number of this plugin + version = (0, 4, 1) # The version number of this plugin file_types = set(['prc','mobi','azw','azw1','azw4','tpz']) # The file types that this plugin will be applied to on_import = True # Run this plugin during the import priority = 210 # run this plugin before mobidedrm, k4pcdedrm, k4dedrm minimum_calibre_version = (0, 7, 55) + + def initialize(self): + """ + Dynamic modules can't be imported/loaded from a zipfile... so this routine + runs whenever the plugin gets initialized. This will extract the appropriate + library for the target OS and copy it to the 'alfcrypto' subdirectory of + calibre's configuration directory. That 'alfcrypto' directory is then + inserted into the syspath (as the very first entry) in the run function + so the CDLL stuff will work in the alfcrypto.py script. + """ + if iswindows: + names = ['alfcrypto.dll','alfcrypto64.dll'] + elif isosx: + names = ['libalfcrypto.dylib'] + else: + names = ['libalfcrypto32.so','libalfcrypto64.so'] + lib_dict = self.load_resources(names) + self.alfdir = os.path.join(config_dir, 'alfcrypto') + if not os.path.exists(self.alfdir): + os.mkdir(self.alfdir) + for entry, data in lib_dict.items(): + file_path = os.path.join(self.alfdir, entry) + with open(file_path,'wb') as f: + f.write(data) def run(self, path_to_ebook): + # add the alfcrypto directory to sys.path so alfcrypto.py + # will be able to locate the custom lib(s) for CDLL import. + sys.path.insert(0, self.alfdir) + # Had to move these imports here so the custom libs can be + # extracted to the appropriate places beforehand these routines + # look for them. + from calibre_plugins.k4mobidedrm import kgenpids + from calibre_plugins.k4mobidedrm import topazextract + from calibre_plugins.k4mobidedrm import mobidedrm + plug_ver = '.'.join(str(self.version).strip('()').replace(' ', '').split(',')) k4 = True if sys.platform.startswith('linux'): @@ -45,7 +79,7 @@ class K4DeDRM(FileTypePlugin): serials.append(customvalue) else: print "%s is not a valid Kindle serial number or PID." % str(customvalue) - + # Load any kindle info files (*.info) included Calibre's config directory. try: # Find Calibre's configuration directory. @@ -77,7 +111,7 @@ class K4DeDRM(FileTypePlugin): title = mb.getBookTitle() md1, md2 = mb.getPIDMetaInfo() - pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles) + pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles) try: mb.processBook(pidlst) @@ -94,11 +128,11 @@ class K4DeDRM(FileTypePlugin): except topazextract.TpzDRMError, e: #if you reached here then no luck raise and exception if is_ok_to_use_qt(): - from PyQt4.Qt import QMessageBox - d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM v%s Plugin" % plug_ver, "Error: " + str(e) + "... %s\n" % path_to_ebook) - d.show() - d.raise_() - d.exec_() + from PyQt4.Qt import QMessageBox + d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM v%s Plugin" % plug_ver, "Error: " + str(e) + "... %s\n" % path_to_ebook) + d.show() + d.raise_() + d.exec_() raise Exception("K4MobiDeDRM plugin v%s Error: %s" % (plug_ver, str(e))) print "Success!" @@ -117,3 +151,11 @@ class K4DeDRM(FileTypePlugin): def customization_help(self, gui=False): return 'Enter 10 character PIDs and/or Kindle serial numbers, use a comma (no spaces) to separate each PID or SerialNumber from the next.' + + def load_resources(self, names): + ans = {} + with ZipFile(self.plugin_path, 'r') as zf: + for candidate in zf.namelist(): + if candidate in names: + ans[candidate] = zf.read(candidate) + return ans \ No newline at end of file diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/convert2xml.py b/Calibre_Plugins/K4MobiDeDRM_plugin/convert2xml.py index 0328206..5667511 100644 --- a/Calibre_Plugins/K4MobiDeDRM_plugin/convert2xml.py +++ b/Calibre_Plugins/K4MobiDeDRM_plugin/convert2xml.py @@ -1,832 +1,568 @@ -#! /usr/bin/python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab -# For use with Topaz Scripts Version 2.6 +#! /usr/bin/env python -class Unbuffered: - def __init__(self, stream): - self.stream = stream - def write(self, data): - self.stream.write(data) - self.stream.flush() - def __getattr__(self, attr): - return getattr(self.stream, attr) +""" + Routines for doing AES CBC in one file -import sys -sys.stdout=Unbuffered(sys.stdout) + Modified by some_updates to extract + and combine only those parts needed for AES CBC + into one simple to add python file -import csv -import os -import getopt -from struct import pack -from struct import unpack + 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 TpzDRMError(Exception): - pass +class CryptoError(Exception): + """ Base class for crypto exceptions """ + def __init__(self,errorMessage='Error!'): + self.message = errorMessage + def __str__(self): + return self.message -# Get a 7 bit encoded number from string. The most -# significant byte comes first and has the high bit (8th) set +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 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 - +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) -# returns a binary string that encodes a number into 7 bits -# most significant byte first which has the high bit set +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) -def encodeNumber(number): - result = "" - negative = False - flag = 0 - - if number < 0 : - number = -number + 1 - negative = True - - while True: - byte = number & 0x7F - number = number >> 7 - byte += flag - result += chr(byte) - flag = 0x80 - if number == 0 : - if (byte == 0xFF and negative == False) : - result += chr(0x80) - break - - if negative: - result += chr(0xFF) - - return result[::-1] - +""" + 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() -# create / read a length prefixed string from the file + def reset(self): + self.resetEncrypt() + self.resetDecrypt() + def resetEncrypt(self): + self.encryptBlockCount = 0 + self.bytesToEncrypt = '' + def resetDecrypt(self): + self.decryptBlockCount = 0 + self.bytesToDecrypt = '' -def lengthPrefixString(data): - return encodeNumber(len(data))+data - -def readString(file): - stringLength = readEncodedNumber(file) - if (stringLength == None): - return "" - sv = file.read(stringLength) - if (len(sv) != stringLength): - return "" - return unpack(str(stringLength)+"s",sv)[0] - - -# convert a binary string generated by encodeNumber (7 bit encoded number) -# to the value you would find inside the page*.dat files to be processed - -def convert(i): - result = '' - val = encodeNumber(i) - for j in xrange(len(val)): - c = ord(val[j:j+1]) - result += '%02x' % c - return result - - - -# the complete string table used to store all book text content -# as well as the xml tokens and values that make sense out of it - -class Dictionary(object): - def __init__(self, dictFile): - self.filename = dictFile - self.size = 0 - self.fo = file(dictFile,'rb') - self.stable = [] - self.size = readEncodedNumber(self.fo) - for i in xrange(self.size): - self.stable.append(self.escapestr(readString(self.fo))) - self.pos = 0 - - def escapestr(self, str): - str = str.replace('&','&') - 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] + 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: - print "Error - %d outside of string table limits" % val - raise TpzDRMError('outside of string table limits') - # sys.exit(-1) + self.bytesToEncrypt = '' - def getSize(self): - return self.size + 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 getPos(self): - return self.pos + def decrypt(self, cipherText, more = None): + """ Decrypt a string and return a string """ + self.bytesToDecrypt += cipherText # append to any bytes from prior decrypt - def dumpDict(self): - for i in xrange(self.size): - print "%d %s %s" % (i, convert(i), self.stable[i]) - return + 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' -# parses the xml snippets that are represented by each page*.dat file. -# also parses the other0.dat file - the main stylesheet -# and information used to inject the xml snippets into page*.dat files + # 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 -class PageParser(object): - def __init__(self, filename, dict, debug, flat_xml): - self.fo = file(filename,'rb') - self.id = os.path.basename(filename).replace('.dat','') - self.dict = dict - self.debug = debug - self.flat_xml = flat_xml - self.tagpath = [] - self.doc = [] - self.snippetList = [] + plainText = '' + for i in range(numBlocks): + bStart = i*self.blockSize + ptBlock = self.decryptBlock(self.bytesToDecrypt[bStart : bStart+self.blockSize]) + self.decryptBlockCount += 1 + plainText += ptBlock - - # hash table used to enable the decoding process - # This has all been developed by trial and error so it may still have omissions or - # contain errors - # Format: - # tag : (number of arguments, argument type, subtags present, special case of subtags presents when escaped) - - token_tags = { - 'x' : (1, 'scalar_number', 0, 0), - 'y' : (1, 'scalar_number', 0, 0), - 'h' : (1, 'scalar_number', 0, 0), - 'w' : (1, 'scalar_number', 0, 0), - 'firstWord' : (1, 'scalar_number', 0, 0), - 'lastWord' : (1, 'scalar_number', 0, 0), - 'rootID' : (1, 'scalar_number', 0, 0), - 'stemID' : (1, 'scalar_number', 0, 0), - 'type' : (1, 'scalar_text', 0, 0), - - 'info' : (0, 'number', 1, 0), - - 'info.word' : (0, 'number', 1, 1), - 'info.word.ocrText' : (1, 'text', 0, 0), - 'info.word.firstGlyph' : (1, 'raw', 0, 0), - 'info.word.lastGlyph' : (1, 'raw', 0, 0), - 'info.word.bl' : (1, 'raw', 0, 0), - 'info.word.link_id' : (1, 'number', 0, 0), - - 'glyph' : (0, 'number', 1, 1), - 'glyph.x' : (1, 'number', 0, 0), - 'glyph.y' : (1, 'number', 0, 0), - 'glyph.glyphID' : (1, 'number', 0, 0), - - 'dehyphen' : (0, 'number', 1, 1), - 'dehyphen.rootID' : (1, 'number', 0, 0), - 'dehyphen.stemID' : (1, 'number', 0, 0), - 'dehyphen.stemPage' : (1, 'number', 0, 0), - 'dehyphen.sh' : (1, 'number', 0, 0), - - 'links' : (0, 'number', 1, 1), - 'links.page' : (1, 'number', 0, 0), - 'links.rel' : (1, 'number', 0, 0), - 'links.row' : (1, 'number', 0, 0), - 'links.title' : (1, 'text', 0, 0), - 'links.href' : (1, 'text', 0, 0), - 'links.type' : (1, 'text', 0, 0), - - 'paraCont' : (0, 'number', 1, 1), - 'paraCont.rootID' : (1, 'number', 0, 0), - 'paraCont.stemID' : (1, 'number', 0, 0), - 'paraCont.stemPage' : (1, 'number', 0, 0), - - 'paraStems' : (0, 'number', 1, 1), - 'paraStems.stemID' : (1, 'number', 0, 0), - - 'wordStems' : (0, 'number', 1, 1), - 'wordStems.stemID' : (1, 'number', 0, 0), - - 'empty' : (1, 'snippets', 1, 0), - - 'page' : (1, 'snippets', 1, 0), - 'page.pageid' : (1, 'scalar_text', 0, 0), - 'page.pagelabel' : (1, 'scalar_text', 0, 0), - 'page.type' : (1, 'scalar_text', 0, 0), - 'page.h' : (1, 'scalar_number', 0, 0), - 'page.w' : (1, 'scalar_number', 0, 0), - 'page.startID' : (1, 'scalar_number', 0, 0), - - 'group' : (1, 'snippets', 1, 0), - 'group.type' : (1, 'scalar_text', 0, 0), - 'group._tag' : (1, 'scalar_text', 0, 0), - - 'region' : (1, 'snippets', 1, 0), - 'region.type' : (1, 'scalar_text', 0, 0), - 'region.x' : (1, 'scalar_number', 0, 0), - 'region.y' : (1, 'scalar_number', 0, 0), - 'region.h' : (1, 'scalar_number', 0, 0), - 'region.w' : (1, 'scalar_number', 0, 0), - - 'empty_text_region' : (1, 'snippets', 1, 0), - - 'img' : (1, 'snippets', 1, 0), - 'img.x' : (1, 'scalar_number', 0, 0), - 'img.y' : (1, 'scalar_number', 0, 0), - 'img.h' : (1, 'scalar_number', 0, 0), - 'img.w' : (1, 'scalar_number', 0, 0), - 'img.src' : (1, 'scalar_number', 0, 0), - 'img.color_src' : (1, 'scalar_number', 0, 0), - - 'paragraph' : (1, 'snippets', 1, 0), - 'paragraph.class' : (1, 'scalar_text', 0, 0), - 'paragraph.firstWord' : (1, 'scalar_number', 0, 0), - 'paragraph.lastWord' : (1, 'scalar_number', 0, 0), - 'paragraph.lastWord' : (1, 'scalar_number', 0, 0), - 'paragraph.gridSize' : (1, 'scalar_number', 0, 0), - 'paragraph.gridBottomCenter' : (1, 'scalar_number', 0, 0), - 'paragraph.gridTopCenter' : (1, 'scalar_number', 0, 0), - - - 'word_semantic' : (1, 'snippets', 1, 1), - 'word_semantic.type' : (1, 'scalar_text', 0, 0), - 'word_semantic.firstWord' : (1, 'scalar_number', 0, 0), - 'word_semantic.lastWord' : (1, 'scalar_number', 0, 0), - - 'word' : (1, 'snippets', 1, 0), - 'word.type' : (1, 'scalar_text', 0, 0), - 'word.class' : (1, 'scalar_text', 0, 0), - 'word.firstGlyph' : (1, 'scalar_number', 0, 0), - 'word.lastGlyph' : (1, 'scalar_number', 0, 0), - - '_span' : (1, 'snippets', 1, 0), - '_span.firstWord' : (1, 'scalar_number', 0, 0), - '_span.lastWord' : (1, 'scalar_number', 0, 0), - '_span.gridSize' : (1, 'scalar_number', 0, 0), - '_span.gridBottomCenter' : (1, 'scalar_number', 0, 0), - '_span.gridTopCenter' : (1, 'scalar_number', 0, 0), - - 'span' : (1, 'snippets', 1, 0), - 'span.firstWord' : (1, 'scalar_number', 0, 0), - 'span.lastWord' : (1, 'scalar_number', 0, 0), - 'span.gridSize' : (1, 'scalar_number', 0, 0), - 'span.gridBottomCenter' : (1, 'scalar_number', 0, 0), - 'span.gridTopCenter' : (1, 'scalar_number', 0, 0), - - 'extratokens' : (1, 'snippets', 1, 0), - 'extratokens.type' : (1, 'scalar_text', 0, 0), - 'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0), - 'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0), - - 'glyph.h' : (1, 'number', 0, 0), - 'glyph.w' : (1, 'number', 0, 0), - 'glyph.use' : (1, 'number', 0, 0), - 'glyph.vtx' : (1, 'number', 0, 1), - 'glyph.len' : (1, 'number', 0, 1), - 'glyph.dpi' : (1, 'number', 0, 0), - 'vtx' : (0, 'number', 1, 1), - 'vtx.x' : (1, 'number', 0, 0), - 'vtx.y' : (1, 'number', 0, 0), - 'len' : (0, 'number', 1, 1), - 'len.n' : (1, 'number', 0, 0), - - 'book' : (1, 'snippets', 1, 0), - 'version' : (1, 'snippets', 1, 0), - 'version.FlowEdit_1_id' : (1, 'scalar_text', 0, 0), - 'version.FlowEdit_1_version' : (1, 'scalar_text', 0, 0), - 'version.Schema_id' : (1, 'scalar_text', 0, 0), - 'version.Schema_version' : (1, 'scalar_text', 0, 0), - 'version.Topaz_version' : (1, 'scalar_text', 0, 0), - 'version.WordDetailEdit_1_id' : (1, 'scalar_text', 0, 0), - 'version.WordDetailEdit_1_version' : (1, 'scalar_text', 0, 0), - 'version.ZoneEdit_1_id' : (1, 'scalar_text', 0, 0), - 'version.ZoneEdit_1_version' : (1, 'scalar_text', 0, 0), - 'version.chapterheaders' : (1, 'scalar_text', 0, 0), - 'version.creation_date' : (1, 'scalar_text', 0, 0), - 'version.header_footer' : (1, 'scalar_text', 0, 0), - 'version.init_from_ocr' : (1, 'scalar_text', 0, 0), - 'version.letter_insertion' : (1, 'scalar_text', 0, 0), - 'version.xmlinj_convert' : (1, 'scalar_text', 0, 0), - 'version.xmlinj_reflow' : (1, 'scalar_text', 0, 0), - 'version.xmlinj_transform' : (1, 'scalar_text', 0, 0), - 'version.findlists' : (1, 'scalar_text', 0, 0), - 'version.page_num' : (1, 'scalar_text', 0, 0), - 'version.page_type' : (1, 'scalar_text', 0, 0), - 'version.bad_text' : (1, 'scalar_text', 0, 0), - 'version.glyph_mismatch' : (1, 'scalar_text', 0, 0), - 'version.margins' : (1, 'scalar_text', 0, 0), - 'version.staggered_lines' : (1, 'scalar_text', 0, 0), - 'version.paragraph_continuation' : (1, 'scalar_text', 0, 0), - 'version.toc' : (1, 'scalar_text', 0, 0), - - 'stylesheet' : (1, 'snippets', 1, 0), - 'style' : (1, 'snippets', 1, 0), - 'style._tag' : (1, 'scalar_text', 0, 0), - 'style.type' : (1, 'scalar_text', 0, 0), - 'style._parent_type' : (1, 'scalar_text', 0, 0), - 'style.class' : (1, 'scalar_text', 0, 0), - 'style._after_class' : (1, 'scalar_text', 0, 0), - 'rule' : (1, 'snippets', 1, 0), - 'rule.attr' : (1, 'scalar_text', 0, 0), - 'rule.value' : (1, 'scalar_text', 0, 0), - - 'original' : (0, 'number', 1, 1), - 'original.pnum' : (1, 'number', 0, 0), - 'original.pid' : (1, 'text', 0, 0), - 'pages' : (0, 'number', 1, 1), - 'pages.ref' : (1, 'number', 0, 0), - 'pages.id' : (1, 'number', 0, 0), - 'startID' : (0, 'number', 1, 1), - 'startID.page' : (1, 'number', 0, 0), - 'startID.id' : (1, 'number', 0, 0), - - } - - - # full tag path record keeping routines - def tag_push(self, token): - self.tagpath.append(token) - def tag_pop(self): - if len(self.tagpath) > 0 : - self.tagpath.pop() - def tagpath_len(self): - return len(self.tagpath) - def get_tagpath(self, i): - cnt = len(self.tagpath) - if i < cnt : result = self.tagpath[i] - for j in xrange(i+1, cnt) : - result += '.' + self.tagpath[j] - return result - - - # list of absolute command byte values values that indicate - # various types of loop meachanisms typically used to generate vectors - - cmd_list = (0x76, 0x76) - - # peek at and return 1 byte that is ahead by i bytes - def peek(self, aheadi): - c = self.fo.read(aheadi) - if (len(c) == 0): - return None - self.fo.seek(-aheadi,1) - c = c[-1:] - return ord(c) - - - # get the next value from the file being processed - def getNext(self): - nbyte = self.peek(1); - if (nbyte == None): - return None - val = readEncodedNumber(self.fo) - return val - - - # format an arg by argtype - def formatArg(self, arg, argtype): - if (argtype == 'text') or (argtype == 'scalar_text') : - result = self.dict.lookup(arg) - elif (argtype == 'raw') or (argtype == 'number') or (argtype == 'scalar_number') : - result = arg - elif (argtype == 'snippets') : - result = arg - else : - print "Error Unknown argtype %s" % argtype - sys.exit(-2) - return result - - - # process the next tag token, recursively handling subtags, - # arguments, and commands - def procToken(self, token): - - known_token = False - self.tag_push(token) - - if self.debug : print 'Processing: ', self.get_tagpath(0) - cnt = self.tagpath_len() - for j in xrange(cnt): - tkn = self.get_tagpath(j) - if tkn in self.token_tags : - num_args = self.token_tags[tkn][0] - argtype = self.token_tags[tkn][1] - subtags = self.token_tags[tkn][2] - splcase = self.token_tags[tkn][3] - ntags = -1 - known_token = True - break - - if known_token : - - # handle subtags if present - subtagres = [] - if (splcase == 1): - # this type of tag uses of escape marker 0x74 indicate subtag count - if self.peek(1) == 0x74: - skip = readEncodedNumber(self.fo) - subtags = 1 - num_args = 0 - - if (subtags == 1): - ntags = readEncodedNumber(self.fo) - if self.debug : print 'subtags: ' + token + ' has ' + str(ntags) - for j in xrange(ntags): - val = readEncodedNumber(self.fo) - subtagres.append(self.procToken(self.dict.lookup(val))) - - # arguments can be scalars or vectors of text or numbers - argres = [] - if num_args > 0 : - firstarg = self.peek(1) - if (firstarg in self.cmd_list) and (argtype != 'scalar_number') and (argtype != 'scalar_text'): - # single argument is a variable length vector of data - arg = readEncodedNumber(self.fo) - argres = self.decodeCMD(arg,argtype) - else : - # num_arg scalar arguments - for i in xrange(num_args): - argres.append(self.formatArg(readEncodedNumber(self.fo), argtype)) - - # build the return tag - result = [] - tkn = self.get_tagpath(0) - result.append(tkn) - result.append(subtagres) - result.append(argtype) - result.append(argres) - self.tag_pop() - return result - - # all tokens that need to be processed should be in the hash - # table if it may indicate a problem, either new token - # or an out of sync condition + if numExtraBytes > 0: # save any bytes that are not block aligned + self.bytesToEncrypt = self.bytesToEncrypt[-numExtraBytes:] else: - result = [] - if (self.debug): - print 'Unknown Token:', token - self.tag_pop() - return result + self.bytesToEncrypt = '' + + if more == None: # last decrypt remove padding + plainText = self.padding.removePad(plainText, self.blockSize) + self.resetDecrypt() + return plainText - # special loop used to process code snippets - # it is NEVER used to format arguments. - # builds the snippetList - def doLoop72(self, argtype): - cnt = readEncodedNumber(self.fo) - if self.debug : - result = 'Set of '+ str(cnt) + ' xml snippets. The overall structure \n' - result += 'of the document is indicated by snippet number sets at the\n' - result += 'end of each snippet. \n' - print result - for i in xrange(cnt): - if self.debug: print 'Snippet:',str(i) - snippet = [] - snippet.append(i) - val = readEncodedNumber(self.fo) - snippet.append(self.procToken(self.dict.lookup(val))) - self.snippetList.append(snippet) - return +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> 1 - x = [] - for i in xrange(cnt): - x.append(readEncodedNumber(self.fo) - adj) - for i in xrange(mode): - for j in xrange(1, cnt): - x[j] = x[j] + x[j - 1] - for i in xrange(cnt): - result.append(self.formatArg(x[i],argtype)) - return result + def _toBlock(self, bs): + """ Convert binary string to array of bytes, state[col][row]""" + assert ( len(bs) == 4*self.Nb ), 'Rijndarl blocks must be of size blockSize' + return [[ord(bs[4*i]),ord(bs[4*i+1]),ord(bs[4*i+2]),ord(bs[4*i+3])] for i in range(self.Nb)] + def _toBString(self, block): + """ Convert block (array of bytes) to binary string """ + l = [] + for col in block: + for rowElement in col: + l.append(chr(rowElement)) + return ''.join(l) +#------------------------------------- +""" Number of rounds Nr = NrTable[Nb][Nk] - # dispatches loop commands bytes with various modes - # The 0x76 style loops are used to build vectors + Nb Nk=4 Nk=5 Nk=6 Nk=7 Nk=8 + ------------------------------------- """ +NrTable = {4: {4:10, 5:11, 6:12, 7:13, 8:14}, + 5: {4:11, 5:11, 6:12, 7:13, 8:14}, + 6: {4:12, 5:12, 6:12, 7:13, 8:14}, + 7: {4:13, 5:13, 6:13, 7:13, 8:14}, + 8: {4:14, 5:14, 6:14, 7:14, 8:14}} +#------------------------------------- +def keyExpansion(algInstance, keyString): + """ Expand a string of size keySize into a larger array """ + Nk, Nb, Nr = algInstance.Nk, algInstance.Nb, algInstance.Nr # for readability + key = [ord(byte) for byte in keyString] # convert string to list + w = [[key[4*i],key[4*i+1],key[4*i+2],key[4*i+3]] for i in range(Nk)] + for i in range(Nk,Nb*(Nr+1)): + temp = w[i-1] # a four byte column + if (i%Nk) == 0 : + temp = temp[1:]+[temp[0]] # RotWord(temp) + temp = [ Sbox[byte] for byte in temp ] + temp[0] ^= Rcon[i/Nk] + elif Nk > 6 and i%Nk == 4 : + temp = [ Sbox[byte] for byte in temp ] # SubWord(temp) + w.append( [ w[i-Nk][byte]^temp[byte] for byte in range(4) ] ) + return w - # This was all derived by trial and error and - # new loop types may exist that are not handled here - # since they did not appear in the test cases +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 decodeCMD(self, cmd, argtype): - if (cmd == 0x76): +#------------------------------------- +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] +#------------------------------------- - # loop with cnt, and mode to control loop styles - cnt = readEncodedNumber(self.fo) - mode = readEncodedNumber(self.fo) +def SubBytes(algInstance): + for column in range(algInstance.Nb): + for row in range(4): + algInstance.state[column][row] = Sbox[algInstance.state[column][row]] - if self.debug : print 'Loop for', cnt, 'with mode', mode, ': ' - return self.doLoop76Mode(argtype, cnt, mode) +def InvSubBytes(algInstance): + for column in range(algInstance.Nb): + for row in range(4): + algInstance.state[column][row] = InvSbox[algInstance.state[column][row]] - if self.dbug: print "Unknown command", cmd - result = [] - return result +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) - - # add full tag path to injected snippets - def updateName(self, tag, prefix): - name = tag[0] - subtagList = tag[1] - argtype = tag[2] - argList = tag[3] - nname = prefix + '.' + name - nsubtaglist = [] - for j in subtagList: - nsubtaglist.append(self.updateName(j,prefix)) - ntag = [] - ntag.append(nname) - ntag.append(nsubtaglist) - ntag.append(argtype) - ntag.append(argList) - return ntag +#------------------------------------- +""" 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] - - # perform depth first injection of specified snippets into this one - def injectSnippets(self, snippet): - snipno, tag = snippet - name = tag[0] - subtagList = tag[1] - argtype = tag[2] - argList = tag[3] - nsubtagList = [] - if len(argList) > 0 : - for j in argList: - asnip = self.snippetList[j] - aso, atag = self.injectSnippets(asnip) - atag = self.updateName(atag, name) - nsubtagList.append(atag) - argtype='number' - argList=[] - if len(nsubtagList) > 0 : - subtagList.extend(nsubtagList) - tag = [] - tag.append(name) - tag.append(subtagList) - tag.append(argtype) - tag.append(argList) - snippet = [] - snippet.append(snipno) - snippet.append(tag) - return snippet - - - - # format the tag for output - def formatTag(self, node): - name = node[0] - subtagList = node[1] - argtype = node[2] - argList = node[3] - fullpathname = name.split('.') - nodename = fullpathname.pop() - ilvl = len(fullpathname) - indent = ' ' * (3 * ilvl) - result = indent + '<' + nodename + '>' - if len(argList) > 0: - argres = '' - for j in argList: - if (argtype == 'text') or (argtype == 'scalar_text') : - argres += j + '|' - else : - argres += str(j) + ',' - argres = argres[0:-1] - if argtype == 'snippets' : - result += 'snippets:' + argres - else : - result += argres - if len(subtagList) > 0 : - result += '\n' - for j in subtagList: - if len(j) > 0 : - result += self.formatTag(j) - result += indent + '\n' - else: - result += '\n' - return result - - - # flatten tag - def flattenTag(self, node): - name = node[0] - subtagList = node[1] - argtype = node[2] - argList = node[3] - result = name - if (len(argList) > 0): - argres = '' - for j in argList: - if (argtype == 'text') or (argtype == 'scalar_text') : - argres += j + '|' - else : - argres += str(j) + '|' - argres = argres[0:-1] - if argtype == 'snippets' : - result += '.snippets=' + argres - else : - result += '=' + argres - result += '\n' - for j in subtagList: - if len(j) > 0 : - result += self.flattenTag(j) - return result - - - # reduce create xml output - def formatDoc(self, flat_xml): - result = '' - for j in self.doc : - if len(j) > 0: - if flat_xml: - result += self.flattenTag(j) - else: - result += self.formatTag(j) - if self.debug : print result - return result - - - - # main loop - parse the page.dat files - # to create structured document and snippets - - # FIXME: value at end of magic appears to be a subtags count - # but for what? For now, inject an 'info" tag as it is in - # every dictionary and seems close to what is meant - # The alternative is to special case the last _ "0x5f" to mean something - - def process(self): - - # peek at the first bytes to see what type of file it is - magic = self.fo.read(9) - if (magic[0:1] == 'p') and (magic[2:9] == 'marker_'): - first_token = 'info' - elif (magic[0:1] == 'p') and (magic[2:9] == '__PAGE_'): - skip = self.fo.read(2) - first_token = 'info' - elif (magic[0:1] == 'p') and (magic[2:8] == '_PAGE_'): - first_token = 'info' - elif (magic[0:1] == 'g') and (magic[2:9] == '__GLYPH'): - skip = self.fo.read(3) - first_token = 'info' - else : - # other0.dat file - first_token = None - self.fo.seek(-9,1) - - - # main loop to read and build the document tree - while True: - - if first_token != None : - # use "inserted" first token 'info' for page and glyph files - tag = self.procToken(first_token) - if len(tag) > 0 : - self.doc.append(tag) - first_token = None - - v = self.getNext() - if (v == None): - break - - if (v == 0x72): - self.doLoop72('number') - elif (v > 0) and (v < self.dict.getSize()) : - tag = self.procToken(self.dict.lookup(v)) - if len(tag) > 0 : - self.doc.append(tag) - else: - if self.debug: - print "Main Loop: Unknown value: %x" % v - if (v == 0): - if (self.peek(1) == 0x5f): - skip = self.fo.read(1) - first_token = 'info' - - # now do snippet injection - if len(self.snippetList) > 0 : - if self.debug : print 'Injecting Snippets:' - snippet = self.injectSnippets(self.snippetList[0]) - snipno = snippet[0] - tag_add = snippet[1] - if self.debug : print self.formatTag(tag_add) - if len(tag_add) > 0: - self.doc.append(tag_add) - - # handle generation of xml output - xmlpage = self.formatDoc(self.flat_xml) - - return xmlpage - - -def fromData(dict, fname): - flat_xml = True - debug = False - pp = PageParser(fname, dict, debug, flat_xml) - xmlpage = pp.process() - return xmlpage - -def getXML(dict, fname): - flat_xml = False - debug = False - pp = PageParser(fname, dict, debug, flat_xml) - xmlpage = pp.process() - return xmlpage - -def usage(): - print 'Usage: ' - print ' convert2xml.py dict0000.dat infile.dat ' - print ' ' - print ' Options:' - print ' -h print this usage help message ' - print ' -d turn on debug output to check for potential errors ' - print ' --flat-xml output the flattened xml page description only ' - print ' ' - print ' This program will attempt to convert a page*.dat file or ' - print ' glyphs*.dat file, using the dict0000.dat file, to its xml description. ' - print ' ' - print ' Use "cmbtc_dump.py" first to unencrypt, uncompress, and dump ' - print ' the *.dat files from a Topaz format e-book.' - -# -# Main -# - -def main(argv): - dictFile = "" - pageFile = "" - debug = False - flat_xml = False - printOutput = False - if len(argv) == 0: - printOutput = True - argv = sys.argv - - try: - opts, args = getopt.getopt(argv[1:], "hd", ["flat-xml"]) - - except getopt.GetoptError, err: - - # print help information and exit: - print str(err) # will print something like "option -a not recognized" - usage() - sys.exit(2) - - if len(opts) == 0 and len(args) == 0 : - usage() - sys.exit(2) - - for o, a in opts: - if o =="-d": - debug=True - if o =="-h": - usage() - sys.exit(0) - if o =="--flat-xml": - flat_xml = True - - dictFile, pageFile = args[0], args[1] - - # read in the string table dictionary - dict = Dictionary(dictFile) - # dict.dumpDict() - - # create a page parser - pp = PageParser(pageFile, dict, debug, flat_xml) - - xmlpage = pp.process() - - if printOutput: - print xmlpage +#------------------------------------- +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 - return xmlpage +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) -if __name__ == '__main__': - sys.exit(main('')) +Alogtable= ( 1, 3, 5, 15, 17, 51, 85, 255, 26, 46, 114, 150, 161, 248, 19, 53, + 95, 225, 56, 72, 216, 115, 149, 164, 247, 2, 6, 10, 30, 34, 102, 170, + 229, 52, 92, 228, 55, 89, 235, 38, 106, 190, 217, 112, 144, 171, 230, 49, + 83, 245, 4, 12, 20, 60, 68, 204, 79, 209, 104, 184, 211, 110, 178, 205, + 76, 212, 103, 169, 224, 59, 77, 215, 98, 166, 241, 8, 24, 40, 120, 136, + 131, 158, 185, 208, 107, 189, 220, 127, 129, 152, 179, 206, 73, 219, 118, 154, + 181, 196, 87, 249, 16, 48, 80, 240, 11, 29, 39, 105, 187, 214, 97, 163, + 254, 25, 43, 125, 135, 146, 173, 236, 47, 113, 147, 174, 233, 32, 96, 160, + 251, 22, 58, 78, 210, 109, 183, 194, 93, 231, 50, 86, 250, 21, 63, 65, + 195, 94, 226, 61, 71, 201, 64, 192, 91, 237, 44, 116, 156, 191, 218, 117, + 159, 186, 213, 100, 172, 239, 42, 126, 130, 157, 188, 223, 122, 142, 137, 128, + 155, 182, 193, 88, 232, 35, 101, 175, 234, 37, 111, 177, 200, 67, 197, 84, + 252, 31, 33, 99, 165, 244, 7, 9, 27, 45, 119, 153, 176, 203, 70, 202, + 69, 207, 74, 222, 121, 139, 134, 145, 168, 227, 62, 66, 198, 81, 243, 14, + 18, 54, 90, 238, 41, 123, 141, 140, 143, 138, 133, 148, 167, 242, 13, 23, + 57, 75, 221, 124, 132, 151, 162, 253, 28, 36, 108, 180, 199, 82, 246, 1) + + + + +""" + AES Encryption Algorithm + The AES algorithm is just Rijndael algorithm restricted to the default + blockSize of 128 bits. +""" + +class AES(Rijndael): + """ The AES algorithm is the Rijndael block cipher restricted to block + sizes of 128 bits and key sizes of 128, 192 or 256 bits + """ + def __init__(self, key = None, padding = padWithPadLen(), keySize=16): + """ Initialize AES, keySize is in bytes """ + if not (keySize == 16 or keySize == 24 or keySize == 32) : + raise BadKeySizeError, 'Illegal AES key size, must be 16, 24, or 32 bytes' + + Rijndael.__init__( self, key, padding=padding, keySize=keySize, blockSize=16 ) + + self.name = 'AES' + + +""" + CBC mode of encryption for block ciphers. + This algorithm mode wraps any BlockCipher to make a + Cipher Block Chaining mode. +""" +from random import Random # should change to crypto.random!!! + + +class CBC(BlockCipher): + """ The CBC class wraps block ciphers to make cipher block chaining (CBC) mode + algorithms. The initialization (IV) is automatic if set to None. Padding + is also automatic based on the Pad class used to initialize the algorithm + """ + def __init__(self, blockCipherInstance, padding = padWithPadLen()): + """ CBC algorithms are created by initializing with a BlockCipher instance """ + self.baseCipher = blockCipherInstance + self.name = self.baseCipher.name + '_CBC' + self.blockSize = self.baseCipher.blockSize + self.keySize = self.baseCipher.keySize + self.padding = padding + self.baseCipher.padding = noPadding() # baseCipher should NOT pad!! + self.r = Random() # for IV generation, currently uses + # mediocre standard distro version <---------------- + import time + newSeed = time.ctime()+str(self.r) # seed with instance location + self.r.seed(newSeed) # to make unique + self.reset() + + def setKey(self, key): + self.baseCipher.setKey(key) + + # Overload to reset both CBC state and the wrapped baseCipher + def resetEncrypt(self): + BlockCipher.resetEncrypt(self) # reset CBC encrypt state (super class) + self.baseCipher.resetEncrypt() # reset base cipher encrypt state + + def resetDecrypt(self): + BlockCipher.resetDecrypt(self) # reset CBC state (super class) + self.baseCipher.resetDecrypt() # reset base cipher decrypt state + + def encrypt(self, plainText, iv=None, more=None): + """ CBC encryption - overloads baseCipher to allow optional explicit IV + when iv=None, iv is auto generated! + """ + if self.encryptBlockCount == 0: + self.iv = iv + else: + assert(iv==None), 'IV used only on first call to encrypt' + + return BlockCipher.encrypt(self,plainText, more=more) + + def decrypt(self, cipherText, iv=None, more=None): + """ CBC decryption - overloads baseCipher to allow optional explicit IV + when iv=None, iv is auto generated! + """ + if self.decryptBlockCount == 0: + self.iv = iv + else: + assert(iv==None), 'IV used only on first call to decrypt' + + return BlockCipher.decrypt(self, cipherText, more=more) + + def encryptBlock(self, plainTextBlock): + """ CBC block encryption, IV is set with 'encrypt' """ + auto_IV = '' + if self.encryptBlockCount == 0: + if self.iv == None: + # generate IV and use + self.iv = ''.join([chr(self.r.randrange(256)) for i in range(self.blockSize)]) + self.prior_encr_CT_block = self.iv + auto_IV = self.prior_encr_CT_block # prepend IV if it's automatic + else: # application provided IV + assert(len(self.iv) == self.blockSize ),'IV must be same length as block' + self.prior_encr_CT_block = self.iv + """ encrypt the prior CT XORed with the PT """ + ct = self.baseCipher.encryptBlock( xor(self.prior_encr_CT_block, plainTextBlock) ) + self.prior_encr_CT_block = ct + return auto_IV+ct + + def decryptBlock(self, encryptedBlock): + """ Decrypt a single block """ + + if self.decryptBlockCount == 0: # first call, process IV + if self.iv == None: # auto decrypt IV? + self.prior_CT_block = encryptedBlock + return '' + else: + assert(len(self.iv)==self.blockSize),"Bad IV size on CBC decryption" + self.prior_CT_block = self.iv + + dct = self.baseCipher.decryptBlock(encryptedBlock) + """ XOR the prior decrypted CT with the prior CT """ + dct_XOR_priorCT = xor( self.prior_CT_block, dct ) + + self.prior_CT_block = encryptedBlock + + return dct_XOR_priorCT + + +""" + AES_CBC Encryption Algorithm +""" + +class AES_CBC(CBC): + """ AES encryption in CBC feedback mode """ + def __init__(self, key=None, padding=padWithPadLen(), keySize=16): + CBC.__init__( self, AES(key, noPadding(), keySize), padding) + self.name = 'AES_CBC' diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/flatxml2html.py b/Calibre_Plugins/K4MobiDeDRM_plugin/flatxml2html.py index 3b32fc0a945bbae23fac4dc0146ad4cad9685752..26d740ddb0ed3be82128b3fbe0e45471b0e04f4b 100644 GIT binary patch literal 70144 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 G!2bbGSZYrI literal 29346 zcmeHQ`*S15asK`OiV;}Kf))spCs~Rp@?7ORTTzv4UzIL$QjChV1eU<^me{S`9@j)7{fEvpV=h9p5+g@pZmAuD0#E+`KqY5Bc3$+gyBc z);#i`@}_Om>!vL${<=$R{yfW`s&uo&H+1-YS*!adQ;&JOR`1Fx{fqixQRh|Ls2{Vs z$;*v;Isfv-i~O!C>sB?}#(rHi5B6*69^R$x+CE)npxeHRR&9yF5VA4 zzu!p9ix-O`Z5s8aT)a)|Cab6A^=-Cj56@mGe3sdby1L3Yd3$v=ZL;EKrf!O~eYz`V zN;(!zi${5p<;$76F3b0C@_MFL%X}fvpR#3sm##8{jwp#`o-fsfDw}x)8Rpk%lWo$w zY^w1b&g-ly(nU5+=F7BAW=V1=%(&jo!)j69Z(86uiCdQC;`_W|gJE;tR7KuSlmFOs zkpT3E{4W_mi)!fCE)$cjZH-+I0pw(`CL{sVb}Ua;d~v_Xpnn4RyZYYa5}=#h8UmXA4~I>F%0=B~PaL~TPe)pc zeDnTFOlGj9&D(-(4mMV>zOlA-4f7u9*KVt!e#>mVt=8G*sxHfRkfkcE(?z*yubMWy z8--wF6M?)CZE(V2GJjiuikWb$d>KriI&1C=Dk({v`ts#Pg8xre#iz?jqCQpV;u`C$ zy{|WV+KC;`3ry4dY6`RgO%iOb+VV=~t0m2+v{f+R5RyfV6QpdXoWC>mKHCNy(sHiB zpv>)?e6yUgU416=j9(olLW+IS3kYPQlG$|(VEE5Bn%X6N>Q9Do_J=Qk%KWEF zOlRwk62M^gJF@xR{w{zC90q=wsUY@VmtOOyWOgz;I`t}CZTEHA_PXTD zeSL)3cW-P@&tSf6AZ;)c@6xBo{-bs0(NpL5w)1-hqce{jEv+nC8N(Fp1Da}Psw$h@ zd`0ZZHjJx{qBG6X#o8!0K+>+$R^6rVvqs%l;wNPW%oO4_d=>t=gZq7wpZvB}Z3(Yx z$*8iX1*(JSY1+Cfo#RWpxcyP5#9sTPD~=-dg@|!Bh;Y| zN1dwCHabq7FoyaT)II8mxLCr4adg&&iiXIV{fH%5{jp0&fnu+=J#ur&W4>(H7s+9) zP;^*3vEvTwEMKi#>EXT4fm*E74T2IwuQnc9l-4Z%-HN3x(qaLtd{P>RJQMm(WHL&< zaDH8$`~;8mB!7iVrC7 z+v>h`fVp+g0H4Ai^XDvi3|&7T%^&N$MLv4|8)UxfK_}}MlhgUh1nD*$MOu`b>|(Mh zC%<_WrJ6#(b^qkepI*QF_qV@e)KYK%^|#-D_gYPkj*tKR`Rn61@7|~%e*9LQ&QFhz zfA`0UnylNlIy*joe0-cgem*bj)$zA=TCMX%a}2;^0;1C~aGjpcm+f-0i!>=}1D<@k ztez&uurAE6hdg`yZTWODIZ-F-(97qnQFc2Jpt^RXNsi-dce(*KZvu~#7 z+NeG_M6Y_LyeP^hn+B7`Bv>ae@ZtRAnn6;=HeD&iMhtGosLFta71BLos-8g5f;8rv zH|4@)jDR~-KAIugOxg7nR%(ah#N_9>_R?kv7Iq*5m}+;J?ULh|CD|pA#9B`8>oYn+^8@l z?%k77vm_T(vQ{iZGfM;bMU^2YAjG3>QQSBAgI-ZlFhjE)EOyXLxNbLCs$(4w{RBx-cVp=ofAX)G1h`Al$Bmdat6sZT=WVa;y3W$~?y0ds(_v;X zcV~IQdP#Z1IEJDV#zAM#5-pjc7+wV{&5sblgtwyjaT7$e;AkD7ANX<�=(;<$ZW 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 L>F90xQK|nM(;<2v diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/flatxml2svg.py b/Calibre_Plugins/K4MobiDeDRM_plugin/flatxml2svg.py index 49cf6f5..e25a0c8 100644 --- a/Calibre_Plugins/K4MobiDeDRM_plugin/flatxml2svg.py +++ b/Calibre_Plugins/K4MobiDeDRM_plugin/flatxml2svg.py @@ -1,250 +1,290 @@ -#! /usr/bin/python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab +#! /usr/bin/env python -import sys -import csv -import os -import getopt +import sys, os +import hmac from struct import pack -from struct import unpack +import hashlib -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')) +# interface to needed routines libalfcrypto +def _load_libalfcrypto(): + import ctypes + from ctypes import CDLL, byref, POINTER, c_void_p, c_char_p, c_int, c_long, \ + Structure, c_ulong, create_string_buffer, addressof, string_at, cast, sizeof - res = [] - startpos = self.posinDoc('info.glyph.x') - for p in startpos: - argres = self.getDataatPos('info.glyph.x', p) - res.extend(argres) - self.gx = res - - res = [] - startpos = self.posinDoc('info.glyph.y') - for p in startpos: - argres = self.getDataatPos('info.glyph.y', p) - res.extend(argres) - self.gy = res - - res = [] - startpos = self.posinDoc('info.glyph.glyphID') - for p in startpos: - argres = self.getDataatPos('info.glyph.glyphID', p) - res.extend(argres) - self.gid = res - - - # return tag at line pos in document - def lineinDoc(self, pos) : - if (pos >= 0) and (pos < self.docSize) : - item = self.flatdoc[pos] - if item.find('=') >= 0: - (name, argres) = item.split('=',1) - else : - name = item - argres = '' - return name, argres - - # find tag in doc if within pos to end inclusive - def findinDoc(self, tagpath, pos, end) : - result = None - if end == -1 : - end = self.docSize + pointer_size = ctypes.sizeof(ctypes.c_voidp) + name_of_lib = None + if sys.platform.startswith('darwin'): + name_of_lib = 'libalfcrypto.dylib' + elif sys.platform.startswith('win'): + if pointer_size == 4: + name_of_lib = 'alfcrypto.dll' else: - end = min(self.docSize, end) - foundat = -1 - for j in xrange(pos, end): - item = self.flatdoc[j] - if item.find('=') >= 0: - (name, argres) = item.split('=',1) - else : - name = item - argres = '' - if name.endswith(tagpath) : - result = argres - foundat = j - break - return foundat, result - - # return list of start positions for the tagpath - def posinDoc(self, tagpath): - startpos = [] - pos = 0 - res = "" - while res != None : - (foundpos, res) = self.findinDoc(tagpath, pos, -1) - if res != None : - startpos.append(foundpos) - pos = foundpos + 1 - return startpos - - def getData(self, path): - result = None - cnt = len(self.flatdoc) - for j in xrange(cnt): - item = self.flatdoc[j] - if item.find('=') >= 0: - (name, argt) = item.split('=') - argres = argt.split('|') - else: - name = item - argres = [] - if (name.endswith(path)): - result = argres - break - if (len(argres) > 0) : - for j in xrange(0,len(argres)): - argres[j] = int(argres[j]) - return result - - def getDataatPos(self, path, pos): - result = None - item = self.flatdoc[pos] - if item.find('=') >= 0: - (name, argt) = item.split('=') - argres = argt.split('|') - else: - name = item - argres = [] - if (len(argres) > 0) : - for j in xrange(0,len(argres)): - argres[j] = int(argres[j]) - if (name.endswith(path)): - result = argres - return result - - def getDataTemp(self, path): - result = None - cnt = len(self.temp) - for j in xrange(cnt): - item = self.temp[j] - if item.find('=') >= 0: - (name, argt) = item.split('=') - argres = argt.split('|') - else: - name = item - argres = [] - if (name.endswith(path)): - result = argres - self.temp.pop(j) - break - if (len(argres) > 0) : - for j in xrange(0,len(argres)): - argres[j] = int(argres[j]) - return result - - def getImages(self): - result = [] - self.temp = self.flatdoc - while (self.getDataTemp('img') != None): - h = self.getDataTemp('img.h')[0] - w = self.getDataTemp('img.w')[0] - x = self.getDataTemp('img.x')[0] - y = self.getDataTemp('img.y')[0] - src = self.getDataTemp('img.src')[0] - result.append('\n' % (src, x, y, w, h)) - return result - - def getGlyphs(self): - result = [] - if (self.gid != None) and (len(self.gid) > 0): - glyphs = [] - for j in set(self.gid): - glyphs.append(j) - glyphs.sort() - for gid in glyphs: - id='id="gl%d"' % gid - path = self.gd.lookup(id) - if path: - result.append(id + ' ' + path) - return result - - -def convert2SVG(gdict, flat_xml, pageid, previd, nextid, svgDir, raw, meta_array, scaledpi): - ml = '' - pp = PParser(gdict, flat_xml, meta_array) - ml += '\n' - if (raw): - ml += '\n' - ml += '\n' % (pp.pw / scaledpi, pp.ph / scaledpi, pp.pw -1, pp.ph -1) - ml += 'Page %d - %s by %s\n' % (pageid, meta_array['Title'],meta_array['Authors']) + name_of_lib = 'alfcrypto64.dll' else: - ml += '\n' - ml += '\n' - ml += 'Page %d - %s by %s\n' % (pageid, meta_array['Title'],meta_array['Authors']) - ml += '\n' - ml += '\n' - ml += '\n' - ml += '\n' - ml += '\n' - ml += '\n' - ml += '\n' - return ml + name_of_lib = 'libalfcrypto64.so' + + libalfcrypto = sys.path[0] + os.sep + name_of_lib + + if not os.path.isfile(libalfcrypto): + raise Exception('libalfcrypto not found') + + libalfcrypto = CDLL(libalfcrypto) + + c_char_pp = POINTER(c_char_p) + c_int_p = POINTER(c_int) + + + def F(restype, name, argtypes): + func = getattr(libalfcrypto, name) + func.restype = restype + func.argtypes = argtypes + return func + + # aes cbc decryption + # + # struct aes_key_st { + # unsigned long rd_key[4 *(AES_MAXNR + 1)]; + # int rounds; + # }; + # + # typedef struct aes_key_st AES_KEY; + # + # int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key); + # + # + # void AES_cbc_encrypt(const unsigned char *in, unsigned char *out, + # const unsigned long length, const AES_KEY *key, + # unsigned char *ivec, const int enc); + + AES_MAXNR = 14 + + class AES_KEY(Structure): + _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)] + + AES_KEY_p = POINTER(AES_KEY) + AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p, c_int]) + AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',[c_char_p, c_int, AES_KEY_p]) + + + + # Pukall 1 Cipher + # unsigned char *PC1(const unsigned char *key, unsigned int klen, const unsigned char *src, + # unsigned char *dest, unsigned int len, int decryption); + + PC1 = F(c_char_p, 'PC1', [c_char_p, c_ulong, c_char_p, c_char_p, c_ulong, c_ulong]) + + # Topaz Encryption + # typedef struct _TpzCtx { + # unsigned int v[2]; + # } TpzCtx; + # + # void topazCryptoInit(TpzCtx *ctx, const unsigned char *key, int klen); + # void topazCryptoDecrypt(const TpzCtx *ctx, const unsigned char *in, unsigned char *out, int len); + + class TPZ_CTX(Structure): + _fields_ = [('v', c_long * 2)] + + TPZ_CTX_p = POINTER(TPZ_CTX) + topazCryptoInit = F(None, 'topazCryptoInit', [TPZ_CTX_p, c_char_p, c_ulong]) + topazCryptoDecrypt = F(None, 'topazCryptoDecrypt', [TPZ_CTX_p, c_char_p, c_char_p, c_ulong]) + + + class AES_CBC(object): + def __init__(self): + self._blocksize = 0 + self._keyctx = None + self._iv = 0 + + def set_decrypt_key(self, userkey, iv): + self._blocksize = len(userkey) + if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : + raise Exception('AES CBC improper key used') + return + keyctx = self._keyctx = AES_KEY() + self._iv = iv + rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx) + if rv < 0: + raise Exception('Failed to initialize AES CBC key') + + def decrypt(self, data): + out = create_string_buffer(len(data)) + mutable_iv = create_string_buffer(self._iv, len(self._iv)) + rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, mutable_iv, 0) + if rv == 0: + raise Exception('AES CBC decryption failed') + return out.raw + + class Pukall_Cipher(object): + def __init__(self): + self.key = None + + def PC1(self, key, src, decryption=True): + self.key = key + out = create_string_buffer(len(src)) + de = 0 + if decryption: + de = 1 + rv = PC1(key, len(key), src, out, len(src), de) + return out.raw + + class Topaz_Cipher(object): + def __init__(self): + self._ctx = None + + def ctx_init(self, key): + tpz_ctx = self._ctx = TPZ_CTX() + topazCryptoInit(tpz_ctx, key, len(key)) + return tpz_ctx + + def decrypt(self, data, ctx=None): + if ctx == None: + ctx = self._ctx + out = create_string_buffer(len(data)) + topazCryptoDecrypt(ctx, data, out, len(data)) + return out.raw + + print "Using Library AlfCrypto DLL/DYLIB/SO" + return (AES_CBC, Pukall_Cipher, Topaz_Cipher) + + +def _load_python_alfcrypto(): + + import aescbc + + class Pukall_Cipher(object): + def __init__(self): + self.key = None + + def PC1(self, key, src, decryption=True): + sum1 = 0; + sum2 = 0; + keyXorVal = 0; + if len(key)!=16: + print "Bad key length!" + return None + wkey = [] + for i in xrange(8): + wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1])) + dst = "" + for i in xrange(len(src)): + temp1 = 0; + byteXorVal = 0; + for j in xrange(8): + temp1 ^= wkey[j] + sum2 = (sum2+j)*20021 + sum1 + sum1 = (temp1*346)&0xFFFF + sum2 = (sum2+sum1)&0xFFFF + temp1 = (temp1*20021+1)&0xFFFF + byteXorVal ^= temp1 ^ sum2 + curByte = ord(src[i]) + if not decryption: + keyXorVal = curByte * 257; + curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF + if decryption: + keyXorVal = curByte * 257; + for j in xrange(8): + wkey[j] ^= keyXorVal; + dst+=chr(curByte) + return dst + + class Topaz_Cipher(object): + def __init__(self): + self._ctx = None + + def ctx_init(self, key): + ctx1 = 0x0CAFFE19E + for keyChar in key: + keyByte = ord(keyChar) + ctx2 = ctx1 + ctx1 = ((((ctx1 >>2) * (ctx1 >>7))&0xFFFFFFFF) ^ (keyByte * keyByte * 0x0F902007)& 0xFFFFFFFF ) + self._ctx = [ctx1, ctx2] + return [ctx1,ctx2] + + def decrypt(self, data, ctx=None): + if ctx == None: + ctx = self._ctx + ctx1 = ctx[0] + ctx2 = ctx[1] + plainText = "" + for dataChar in data: + dataByte = ord(dataChar) + m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF + ctx2 = ctx1 + ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF) + plainText += chr(m) + return plainText + + class AES_CBC(object): + def __init__(self): + self._key = None + self._iv = None + self.aes = None + + def set_decrypt_key(self, userkey, iv): + self._key = userkey + self._iv = iv + self.aes = aescbc.AES_CBC(userkey, aescbc.noPadding(), len(userkey)) + + def decrypt(self, data): + iv = self._iv + cleartext = self.aes.decrypt(iv + data) + return cleartext + + return (AES_CBC, Pukall_Cipher, Topaz_Cipher) + + +def _load_crypto(): + AES_CBC = Pukall_Cipher = Topaz_Cipher = None + cryptolist = (_load_libalfcrypto, _load_python_alfcrypto) + for loader in cryptolist: + try: + AES_CBC, Pukall_Cipher, Topaz_Cipher = loader() + break + except (ImportError, Exception): + pass + return AES_CBC, Pukall_Cipher, Topaz_Cipher + +AES_CBC, Pukall_Cipher, Topaz_Cipher = _load_crypto() + + +class KeyIVGen(object): + # this only exists in openssl so we will use pure python implementation instead + # PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1', + # [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p]) + def pbkdf2(self, passwd, salt, iter, keylen): + + def xorstr( a, b ): + if len(a) != len(b): + raise Exception("xorstr(): lengths differ") + return ''.join((chr(ord(x)^ord(y)) for x, y in zip(a, b))) + + def prf( h, data ): + hm = h.copy() + hm.update( data ) + return hm.digest() + + def pbkdf2_F( h, salt, itercount, blocknum ): + U = prf( h, salt + pack('>i',blocknum ) ) + T = U + for i in range(2, itercount+1): + U = prf( h, U ) + T = xorstr( T, U ) + return T + + sha = hashlib.sha1 + digest_size = sha().digest_size + # l - number of output blocks to produce + l = keylen / digest_size + if keylen % digest_size != 0: + l += 1 + h = hmac.new( passwd, None, sha ) + T = "" + for i in range(1, l+1): + T += pbkdf2_F( h, salt, iter, i ) + return T[0: keylen] + diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/genbook.py b/Calibre_Plugins/K4MobiDeDRM_plugin/genbook.py index 9ad87ea1800240af4a7f29881573d8ad1d0b65d5..7bef68eac0d0a751b0c7090f5b5427a1d5ab1a59 100644 GIT binary patch literal 52224 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 NdK>>N@IQ70{vUg3cj*8C literal 24295 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 diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/k4mobidedrm_orig.py b/Calibre_Plugins/K4MobiDeDRM_plugin/k4mobidedrm_orig.py index d962a029e69fc24286df87ccf19b359b03c337e5..269810cfa24541f8be10050e7192fb6211a45a98 100644 GIT binary patch literal 17393 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 diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/kgenpids.py b/Calibre_Plugins/K4MobiDeDRM_plugin/kgenpids.py index abfc7e4..e5647f4 100644 --- a/Calibre_Plugins/K4MobiDeDRM_plugin/kgenpids.py +++ b/Calibre_Plugins/K4MobiDeDRM_plugin/kgenpids.py @@ -1,270 +1,793 @@ -#!/usr/bin/env python +#! /usr/bin/python +# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab +# For use with Topaz Scripts Version 2.6 -from __future__ import with_statement import sys -import os, csv -import binascii -import zlib -import re -from struct import pack, unpack, unpack_from - -class DrmException(Exception): - pass - -global charMap1 -global charMap3 -global charMap4 - -if 'calibre' in sys.modules: - inCalibre = True -else: - inCalibre = False - -if inCalibre: - if sys.platform.startswith('win'): - from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString - - if sys.platform.startswith('darwin'): - from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString -else: - if sys.platform.startswith('win'): - from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString - - if sys.platform.startswith('darwin'): - from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString +import csv +import os +import math +import getopt +from struct import pack +from struct import unpack -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() +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 = [] -# 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) + def getGlyph(self, gid): + result = '' + id='id="gl%d"' % gid + return self.gdict.lookup(id) -# 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 + def glyphs_to_image(self, glyphList): -# 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 + def extract(path, key): + b = path.find(key) + len(key) + e = path.find(' ',b) + return int(path[b:e]) -# 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 + 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 : - 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 + name = item + argres = '' + return name, argres -# 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 + # find tag in doc if within pos to end inclusive + def findinDoc(self, tagpath, pos, end) : + result = None + if end == -1 : + end = self.docSize + else: + end = min(self.docSize, end) + foundat = -1 + for j in xrange(pos, end): + item = self.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 -# Parse the EXTH header records and use the Kindle serial number to calculate the book pid. -def getKindlePid(pidlst, rec209, token, serialnum): - # Compute book PID - pidHash = SHA1(serialnum+rec209+token) - bookPID = encodePID(pidHash) - bookPID = checksumPid(bookPID) - pidlst.append(bookPID) - - # compute fixed pid for old pre 2.5 firmware update pid as well - bookPID = pidFromSerial(serialnum, 7) + "*" - bookPID = checksumPid(bookPID) - pidlst.append(bookPID) - - return pidlst + # 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 -# parse the Kindleinfo file to calculate the book pid. + # 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 -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(pidlst, rec209, token, kInfoFile): - global charMap1 - kindleDatabase = None - try: - kindleDatabase = getDBfromFile(kInfoFile) - except Exception, message: - print(message) - kindleDatabase = None - pass - - if kindleDatabase == None : - return pidlst - - try: - # Get the Mazama Random number - MazamaRandomNumber = kindleDatabase["MazamaRandomNumber"] + # get the class + def getClass(self, pclass): + nclass = pclass - # Get the kindle account token - kindleAccountToken = kindleDatabase["kindle.account.tokens"] - except KeyError: - print "Keys not found in " + kInfoFile - return pidlst - - # Get the ID string used - encodedIDString = encodeHash(GetIDString(),charMap1) + # 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! - # Get the current user name - encodedUsername = encodeHash(GetUserName(),charMap1) + # 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 - # concat, hash and encode to calculate the DSN - DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1) - - # Compute the device PID (for which I can tell, is used for nothing). - table = generatePidEncryptionTable() - devicePID = generateDevicePID(table,DSN,4) - devicePID = checksumPid(devicePID) - pidlst.append(devicePID) + # 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 - # Compute book PIDs - # book pid - pidHash = SHA1(DSN+kindleAccountToken+rec209+token) - bookPID = encodePID(pidHash) - bookPID = checksumPid(bookPID) - pidlst.append(bookPID) + # develop a sorted description of the starting positions of + # groups and regions on the page, as well as the page type + def PageDescription(self): - # variant 1 - pidHash = SHA1(kindleAccountToken+rec209+token) - bookPID = encodePID(pidHash) - bookPID = checksumPid(bookPID) - pidlst.append(bookPID) + def compare(x, y): + (xtype, xval) = x + (ytype, yval) = y + if xval > yval: + return 1 + if xval == yval: + return 0 + return -1 - # variant 2 - pidHash = SHA1(DSN+rec209+token) - bookPID = encodePID(pidHash) - bookPID = checksumPid(bookPID) - pidlst.append(bookPID) + result = [] + (pos, pagetype) = self.findinDoc('page.type',0,-1) - return pidlst + 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) -def getPidList(md1, md2, k4, pids, serials, kInfoFiles): - pidlst = [] - if kInfoFiles is None: - kInfoFiles = [] - if k4: - kInfoFiles = getKindleInfoFiles(kInfoFiles) - for infoFile in kInfoFiles: - pidlst = getK4Pids(pidlst, md1, md2, infoFile) - for serialnum in serials: - pidlst = getKindlePid(pidlst, md1, md2, serialnum) - for pid in pids: - pidlst.append(pid) - return pidlst + # 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 = [] + + # 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'): + (cname, space) = argres.split('-',1) + if space == '' : space = '0' + if (cname == 'spaceafter') and (int(space) > 0) : + word_class = 'sa' + + 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('|') + 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] + + + # 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/Calibre_Plugins/K4MobiDeDRM_plugin/plugin-import-name-k4mobidedrm.txt b/Calibre_Plugins/K4MobiDeDRM_plugin/plugin-import-name-k4mobidedrm.txt index e69de29..e66e9f3 100644 --- a/Calibre_Plugins/K4MobiDeDRM_plugin/plugin-import-name-k4mobidedrm.txt +++ b/Calibre_Plugins/K4MobiDeDRM_plugin/plugin-import-name-k4mobidedrm.txt @@ -0,0 +1,726 @@ +# standlone set of Mac OSX specific routines needed for KindleBooks + +from __future__ import with_statement + +import sys +import os +import os.path +import re +import copy +import subprocess +from struct import pack, unpack, unpack_from + +class DrmException(Exception): + pass + + +# interface to needed routines in openssl's libcrypto +def _load_crypto_libcrypto(): + from ctypes import CDLL, byref, POINTER, c_void_p, c_char_p, c_int, c_long, \ + Structure, c_ulong, create_string_buffer, addressof, string_at, cast + from ctypes.util import find_library + + libcrypto = find_library('crypto') + if libcrypto is None: + raise DrmException('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('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('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('AES decryption failed') + return out.raw + + def keyivgen(self, passwd, salt, iter, keylen): + saltlen = len(salt) + passlen = len(passwd) + out = create_string_buffer(keylen) + rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out) + return out.raw + return LibCrypto + +def _load_crypto(): + LibCrypto = None + try: + LibCrypto = _load_crypto_libcrypto() + except (ImportError, DrmException): + pass + return LibCrypto + +LibCrypto = _load_crypto() + +# +# Utility Routines +# + +# crypto digestroutines +import hashlib + +def MD5(message): + ctx = hashlib.md5() + ctx.update(message) + return ctx.digest() + +def SHA1(message): + ctx = hashlib.sha1() + ctx.update(message) + return ctx.digest() + +def SHA256(message): + ctx = hashlib.sha256() + ctx.update(message) + return ctx.digest() + +# Various character maps used to decrypt books. Probably supposed to act as obfuscation +charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" +charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM" + +# For kinf approach of K4Mac 1.6.X or later +# On K4PC charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE" +# For Mac they seem to re-use charMap2 here +charMap5 = charMap2 + +# new in K4M 1.9.X +testMap8 = "YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD" + + +def encode(data, map): + result = "" + for char in data: + value = ord(char) + Q = (value ^ 0x80) // len(map) + R = value % len(map) + result += map[Q] + result += map[R] + return result + +# Hash the bytes in data and then encode the digest with the characters in map +def encodeHash(data,map): + return encode(MD5(data),map) + +# Decode the string in data with the characters in map. Returns the decoded bytes +def decode(data,map): + result = "" + for i in range (0,len(data)-1,2): + high = map.find(data[i]) + low = map.find(data[i+1]) + if (high == -1) or (low == -1) : + break + value = (((high * len(map)) ^ 0x80) & 0xFF) + low + result += pack("B",value) + return result + +# For K4M 1.6.X and later +# generate table of prime number less than or equal to int n +def primes(n): + if n==2: return [2] + elif n<2: return [] + s=range(3,n+1,2) + mroot = n ** 0.5 + half=(n+1)/2-1 + i=0 + m=3 + while m <= mroot: + if s[i]: + j=(m*m-3)/2 + s[j]=0 + while j 7: + return mungedmac + sernum = GetVolumeSerialNumber() + if len(sernum) > 7: + return sernum + diskpart = GetUserHomeAppSupKindleDirParitionName() + uuidnum = GetDiskPartitionUUID(diskpart) + if len(uuidnum) > 7: + return uuidnum + mungedmac = GetMACAddressMunged() + if len(mungedmac) > 7: + return mungedmac + return '9999999999' + + +# implements an Pseudo Mac Version of Windows built-in Crypto routine +# used by Kindle for Mac versions < 1.6.0 +class CryptUnprotectData(object): + def __init__(self): + sernum = GetVolumeSerialNumber() + if sernum == '': + sernum = '9999999999' + sp = sernum + '!@#' + GetUserName() + passwdData = encode(SHA256(sp),charMap1) + salt = '16743' + self.crp = LibCrypto() + iter = 0x3e8 + keylen = 0x80 + key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen) + self.key = key_iv[0:32] + self.iv = key_iv[32:48] + self.crp.set_decrypt_key(self.key, self.iv) + + def decrypt(self, encryptedData): + cleartext = self.crp.decrypt(encryptedData) + cleartext = decode(cleartext,charMap1) + return cleartext + + +# implements an Pseudo Mac Version of Windows built-in Crypto routine +# used for Kindle for Mac Versions >= 1.6.0 +class CryptUnprotectDataV2(object): + def __init__(self): + sp = GetUserName() + ':&%:' + GetIDString() + passwdData = encode(SHA256(sp),charMap5) + # salt generation as per the code + salt = 0x0512981d * 2 * 1 * 1 + salt = str(salt) + GetUserName() + salt = encode(salt,charMap5) + self.crp = LibCrypto() + iter = 0x800 + keylen = 0x400 + key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen) + self.key = key_iv[0:32] + self.iv = key_iv[32:48] + self.crp.set_decrypt_key(self.key, self.iv) + + def decrypt(self, encryptedData): + cleartext = self.crp.decrypt(encryptedData) + cleartext = decode(cleartext, charMap5) + return cleartext + + +# unprotect the new header blob in .kinf2011 +# used in Kindle for Mac Version >= 1.9.0 +def UnprotectHeaderData(encryptedData): + passwdData = 'header_key_data' + salt = 'HEADER.2011' + iter = 0x80 + keylen = 0x100 + crp = LibCrypto() + key_iv = crp.keyivgen(passwdData, salt, iter, keylen) + key = key_iv[0:32] + iv = key_iv[32:48] + crp.set_decrypt_key(key,iv) + cleartext = crp.decrypt(encryptedData) + return cleartext + + +# implements an Pseudo Mac Version of Windows built-in Crypto routine +# used for Kindle for Mac Versions >= 1.9.0 +class CryptUnprotectDataV3(object): + def __init__(self, entropy): + sp = GetUserName() + '+@#$%+' + GetIDString() + passwdData = encode(SHA256(sp),charMap2) + salt = entropy + self.crp = LibCrypto() + iter = 0x800 + keylen = 0x400 + key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen) + self.key = key_iv[0:32] + self.iv = key_iv[32:48] + self.crp.set_decrypt_key(self.key, self.iv) + + def decrypt(self, encryptedData): + cleartext = self.crp.decrypt(encryptedData) + cleartext = decode(cleartext, charMap2) + return cleartext + + +# Locate the .kindle-info files +def getKindleInfoFiles(kInfoFiles): + # first search for current .kindle-info files + home = os.getenv('HOME') + cmdline = 'find "' + home + '/Library/Application Support" -name ".kindle-info"' + cmdline = cmdline.encode(sys.getfilesystemencoding()) + p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) + out1, out2 = p1.communicate() + reslst = out1.split('\n') + kinfopath = 'NONE' + found = False + for resline in reslst: + if os.path.isfile(resline): + kInfoFiles.append(resline) + found = True + # add any .rainier*-kinf files + cmdline = 'find "' + home + '/Library/Application Support" -name ".rainier*-kinf"' + cmdline = cmdline.encode(sys.getfilesystemencoding()) + p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) + out1, out2 = p1.communicate() + reslst = out1.split('\n') + for resline in reslst: + if os.path.isfile(resline): + kInfoFiles.append(resline) + found = True + # add any .kinf2011 files + cmdline = 'find "' + home + '/Library/Application Support" -name ".kinf2011"' + cmdline = cmdline.encode(sys.getfilesystemencoding()) + p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) + out1, out2 = p1.communicate() + reslst = out1.split('\n') + for resline in reslst: + if os.path.isfile(resline): + kInfoFiles.append(resline) + found = True + if not found: + print('No kindle-info files have been found.') + return kInfoFiles + +# determine type of kindle info provided and return a +# database of keynames and values +def getDBfromFile(kInfoFile): + names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber", "max_date", "SIGVERIF"] + DB = {} + cnt = 0 + infoReader = open(kInfoFile, 'r') + hdr = infoReader.read(1) + data = infoReader.read() + + if data.find('[') != -1 : + + # older style kindle-info file + cud = CryptUnprotectData() + items = data.split('[') + for item in items: + if item != '': + keyhash, rawdata = item.split(':') + keyname = "unknown" + for name in names: + if encodeHash(name,charMap2) == keyhash: + keyname = name + break + if keyname == "unknown": + keyname = keyhash + encryptedValue = decode(rawdata,charMap2) + cleartext = cud.decrypt(encryptedValue) + DB[keyname] = cleartext + cnt = cnt + 1 + if cnt == 0: + DB = None + return DB + + if hdr == '/': + + # else newer style .kinf file used by K4Mac >= 1.6.0 + # the .kinf file uses "/" to separate it into records + # so remove the trailing "/" to make it easy to use split + data = data[:-1] + items = data.split('/') + cud = CryptUnprotectDataV2() + + # loop through the item records until all are processed + while len(items) > 0: + + # get the first item record + item = items.pop(0) + + # the first 32 chars of the first record of a group + # is the MD5 hash of the key name encoded by charMap5 + keyhash = item[0:32] + keyname = "unknown" + + # the raw keyhash string is also used to create entropy for the actual + # CryptProtectData Blob that represents that keys contents + # "entropy" not used for K4Mac only K4PC + # entropy = SHA1(keyhash) + + # the remainder of the first record when decoded with charMap5 + # has the ':' split char followed by the string representation + # of the number of records that follow + # and make up the contents + srcnt = decode(item[34:],charMap5) + rcnt = int(srcnt) + + # read and store in rcnt records of data + # that make up the contents value + edlst = [] + for i in xrange(rcnt): + item = items.pop(0) + edlst.append(item) + + keyname = "unknown" + for name in names: + if encodeHash(name,charMap5) == keyhash: + keyname = name + break + if keyname == "unknown": + keyname = keyhash + + # the charMap5 encoded contents data has had a length + # of chars (always odd) cut off of the front and moved + # to the end to prevent decoding using charMap5 from + # working properly, and thereby preventing the ensuing + # CryptUnprotectData call from succeeding. + + # The offset into the charMap5 encoded contents seems to be: + # len(contents) - largest prime number less than or equal to int(len(content)/3) + # (in other words split "about" 2/3rds of the way through) + + # move first offsets chars to end to align for decode by charMap5 + encdata = "".join(edlst) + contlen = len(encdata) + + # now properly split and recombine + # by moving noffset chars from the start of the + # string to the end of the string + noffset = contlen - primes(int(contlen/3))[-1] + pfx = encdata[0:noffset] + encdata = encdata[noffset:] + encdata = encdata + pfx + + # decode using charMap5 to get the CryptProtect Data + encryptedValue = decode(encdata,charMap5) + cleartext = cud.decrypt(encryptedValue) + DB[keyname] = cleartext + cnt = cnt + 1 + + if cnt == 0: + DB = None + return DB + + # the latest .kinf2011 version for K4M 1.9.1 + # put back the hdr char, it is needed + data = hdr + data + data = data[:-1] + items = data.split('/') + + # the headerblob is the encrypted information needed to build the entropy string + headerblob = items.pop(0) + encryptedValue = decode(headerblob, charMap1) + cleartext = UnprotectHeaderData(encryptedValue) + + # now extract the pieces in the same way + # this version is different from K4PC it scales the build number by multipying by 735 + pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) + for m in re.finditer(pattern, cleartext): + entropy = str(int(m.group(2)) * 0x2df) + m.group(4) + + cud = CryptUnprotectDataV3(entropy) + + # loop through the item records until all are processed + while len(items) > 0: + + # get the first item record + item = items.pop(0) + + # the first 32 chars of the first record of a group + # is the MD5 hash of the key name encoded by charMap5 + keyhash = item[0:32] + keyname = "unknown" + + # unlike K4PC the keyhash is not used in generating entropy + # entropy = SHA1(keyhash) + added_entropy + # entropy = added_entropy + + # the remainder of the first record when decoded with charMap5 + # has the ':' split char followed by the string representation + # of the number of records that follow + # and make up the contents + srcnt = decode(item[34:],charMap5) + rcnt = int(srcnt) + + # read and store in rcnt records of data + # that make up the contents value + edlst = [] + for i in xrange(rcnt): + item = items.pop(0) + edlst.append(item) + + keyname = "unknown" + for name in names: + if encodeHash(name,testMap8) == keyhash: + keyname = name + break + if keyname == "unknown": + keyname = keyhash + + # the testMap8 encoded contents data has had a length + # of chars (always odd) cut off of the front and moved + # to the end to prevent decoding using testMap8 from + # working properly, and thereby preventing the ensuing + # CryptUnprotectData call from succeeding. + + # The offset into the testMap8 encoded contents seems to be: + # len(contents) - largest prime number less than or equal to int(len(content)/3) + # (in other words split "about" 2/3rds of the way through) + + # move first offsets chars to end to align for decode by testMap8 + encdata = "".join(edlst) + contlen = len(encdata) + + # now properly split and recombine + # by moving noffset chars from the start of the + # string to the end of the string + noffset = contlen - primes(int(contlen/3))[-1] + pfx = encdata[0:noffset] + encdata = encdata[noffset:] + encdata = encdata + pfx + + # decode using testMap8 to get the CryptProtect Data + encryptedValue = decode(encdata,testMap8) + cleartext = cud.decrypt(encryptedValue) + # print keyname + # print cleartext + DB[keyname] = cleartext + cnt = cnt + 1 + + if cnt == 0: + DB = None + return DB diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/stylexml2css.py b/Calibre_Plugins/K4MobiDeDRM_plugin/stylexml2css.py index e30abfa..a412a7b 100644 --- a/Calibre_Plugins/K4MobiDeDRM_plugin/stylexml2css.py +++ b/Calibre_Plugins/K4MobiDeDRM_plugin/stylexml2css.py @@ -1,50 +1,134 @@ #! /usr/bin/python # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab -# For use with Topaz Scripts Version 2.6 + +class Unbuffered: + def __init__(self, stream): + self.stream = stream + def write(self, data): + self.stream.write(data) + self.stream.flush() + def __getattr__(self, attr): + return getattr(self.stream, attr) + +import sys +sys.stdout=Unbuffered(sys.stdout) import csv -import sys import os import getopt from struct import pack from struct import unpack +class TpzDRMError(Exception): + pass -class DocParser(object): - def __init__(self, flatxml, fontsize, ph, pw): +# local support routines +if 'calibre' in sys.modules: + inCalibre = True +else: + inCalibre = False + +if inCalibre : + from calibre_plugins.k4mobidedrm import convert2xml + from calibre_plugins.k4mobidedrm import flatxml2html + from calibre_plugins.k4mobidedrm import flatxml2svg + from calibre_plugins.k4mobidedrm import stylexml2css +else : + import convert2xml + import flatxml2html + import flatxml2svg + import stylexml2css + +# global switch +buildXML = False + +# Get a 7 bit encoded number from a file +def readEncodedNumber(file): + flag = False + c = file.read(1) + if (len(c) == 0): + return None + data = ord(c) + if data == 0xFF: + flag = True + c = file.read(1) + if (len(c) == 0): + return None + data = ord(c) + if data >= 0x80: + datax = (data & 0x7F) + while data >= 0x80 : + c = file.read(1) + if (len(c) == 0): + return None + data = ord(c) + datax = (datax <<7) + (data & 0x7F) + data = datax + if flag: + data = -data + return data + +# Get a length prefixed string from the file +def lengthPrefixString(data): + return encodeNumber(len(data))+data + +def readString(file): + stringLength = readEncodedNumber(file) + if (stringLength == None): + return None + sv = file.read(stringLength) + if (len(sv) != stringLength): + return "" + return unpack(str(stringLength)+"s",sv)[0] + +def getMetaArray(metaFile): + # parse the meta file + result = {} + fo = file(metaFile,'rb') + size = readEncodedNumber(fo) + for i in xrange(size): + tag = readString(fo) + value = readString(fo) + result[tag] = value + # print tag, value + fo.close() + return result + + +# dictionary of all text strings by index value +class Dictionary(object): + def __init__(self, dictFile): + self.filename = dictFile + self.size = 0 + self.fo = file(dictFile,'rb') + self.stable = [] + self.size = readEncodedNumber(self.fo) + for i in xrange(self.size): + self.stable.append(self.escapestr(readString(self.fo))) + self.pos = 0 + def escapestr(self, str): + str = str.replace('&','&') + str = str.replace('<','<') + str = str.replace('>','>') + str = str.replace('=','=') + return str + def lookup(self,val): + if ((val >= 0) and (val < self.size)) : + self.pos = val + return self.stable[self.pos] + else: + print "Error - %d outside of string table limits" % val + raise TpzDRMError('outside or string table limits') + # sys.exit(-1) + def getSize(self): + return self.size + def getPos(self): + return self.pos + + +class PageDimParser(object): + def __init__(self, flatxml): self.flatdoc = flatxml.split('\n') - self.fontsize = int(fontsize) - self.ph = int(ph) * 1.0 - self.pw = int(pw) * 1.0 - - stags = { - 'paragraph' : 'p', - 'graphic' : '.graphic' - } - - attr_val_map = { - 'hang' : 'text-indent: ', - 'indent' : 'text-indent: ', - 'line-space' : 'line-height: ', - 'margin-bottom' : 'margin-bottom: ', - 'margin-left' : 'margin-left: ', - 'margin-right' : 'margin-right: ', - 'margin-top' : 'margin-top: ', - 'space-after' : 'padding-bottom: ', - } - - attr_str_map = { - 'align-center' : 'text-align: center; margin-left: auto; margin-right: auto;', - 'align-left' : 'text-align: left;', - 'align-right' : 'text-align: right;', - 'align-justify' : 'text-align: justify;', - 'display-inline' : 'display: inline;', - 'pos-left' : 'text-align: left;', - 'pos-right' : 'text-align: right;', - 'pos-center' : 'text-align: center; margin-left: auto; margin-right: auto;', - } - - # find tag if within pos to end inclusive def findinDoc(self, tagpath, pos, end) : result = None @@ -58,198 +142,568 @@ class DocParser(object): for j in xrange(pos, end): item = docList[j] if item.find('=') >= 0: - (name, argres) = item.split('=',1) - else : + (name, argres) = item.split('=') + else : name = item argres = '' - if name.endswith(tagpath) : + 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 - 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 - classlst = '' - csspage = '.cl-center { text-align: center; margin-left: auto; margin-right: auto; }\n' - csspage += '.cl-right { text-align: right; }\n' - csspage += '.cl-left { text-align: left; }\n' - csspage += '.cl-justify { text-align: justify; }\n' - - # generate a list of each